StyleJS fixes

This commit is contained in:
StyleJS-Bot 2017-10-11 17:01:48 +00:00
parent c5b0ba98e8
commit 59eaa7923a
62 changed files with 2096 additions and 1662 deletions

View file

@ -1,10 +1,10 @@
import './static-loader' import "./static-loader";
import Vue from 'vue' import Vue from "vue";
import App from './app.vue' import App from "./app.vue";
import { event } from './utils' import { event } from "./utils";
import { http } from './services' import { http } from "./services";
import { VirtualScroller } from 'vue-virtual-scroller/dist/vue-virtual-scroller' import { VirtualScroller } from "vue-virtual-scroller/dist/vue-virtual-scroller";
Vue.component('virtual-scroller', VirtualScroller) Vue.component("virtual-scroller", VirtualScroller);
/** /**
* For Ancelot, the ancient cross of war * For Ancelot, the ancient cross of war
@ -13,10 +13,10 @@ Vue.component('virtual-scroller', VirtualScroller)
* in this dawn of victory * in this dawn of victory
*/ */
new Vue({ new Vue({
el: '#app', el: "#app",
render: h => h(App), render: h => h(App),
created () { created() {
event.init() event.init();
http.init() http.init();
} }
}) });

View file

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

View file

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

View file

@ -2,7 +2,7 @@
* A simple directive to set focus into an input field when it's shown. * A simple directive to set focus into an input field when it's shown.
*/ */
export const focusDirective = { export const focusDirective = {
inserted (el) { inserted(el) {
el.focus() el.focus();
} }
} };

View file

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

View file

@ -1,3 +1,143 @@
/*! modernizr 3.2.0 (Custom Build) | MIT * /*! modernizr 3.2.0 (Custom Build) | MIT *
* http://modernizr.com/download/?-touchevents-setclasses !*/ * http://modernizr.com/download/?-touchevents-setclasses !*/
!function(e,n,t){function o(e,n){return typeof e===n}function s(){var e,n,t,s,a,i,r;for(var l in c)if(c.hasOwnProperty(l)){if(e=[],n=c[l],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t<n.options.aliases.length;t++)e.push(n.options.aliases[t].toLowerCase());for(s=o(n.fn,"function")?n.fn():n.fn,a=0;a<e.length;a++)i=e[a],r=i.split("."),1===r.length?Modernizr[r[0]]=s:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=s),f.push((s?"":"no-")+r.join("-"))}}function a(e){var n=u.className,t=Modernizr._config.classPrefix||"";if(p&&(n=n.baseVal),Modernizr._config.enableJSClass){var o=new RegExp("(^|\\s)"+t+"no-js(\\s|$)");n=n.replace(o,"$1"+t+"js$2")}Modernizr._config.enableClasses&&(n+=" "+t+e.join(" "+t),p?u.className.baseVal=n:u.className=n)}function i(){return"function"!=typeof n.createElement?n.createElement(arguments[0]):p?n.createElementNS.call(n,"http://www.w3.org/2000/svg",arguments[0]):n.createElement.apply(n,arguments)}function r(){var e=n.body;return e||(e=i(p?"svg":"body"),e.fake=!0),e}function l(e,t,o,s){var a,l,f,c,d="modernizr",p=i("div"),h=r();if(parseInt(o,10))for(;o--;)f=i("div"),f.id=s?s[o]:d+(o+1),p.appendChild(f);return a=i("style"),a.type="text/css",a.id="s"+d,(h.fake?h:p).appendChild(a),h.appendChild(p),a.styleSheet?a.styleSheet.cssText=e:a.appendChild(n.createTextNode(e)),p.id=d,h.fake&&(h.style.background="",h.style.overflow="hidden",c=u.style.overflow,u.style.overflow="hidden",u.appendChild(h)),l=t(p,e),h.fake?(h.parentNode.removeChild(h),u.style.overflow=c,u.offsetHeight):p.parentNode.removeChild(p),!!l}var f=[],c=[],d={_version:"3.2.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,n){var t=this;setTimeout(function(){n(t[e])},0)},addTest:function(e,n,t){c.push({name:e,fn:n,options:t})},addAsyncTest:function(e){c.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=d,Modernizr=new Modernizr;var u=n.documentElement,p="svg"===u.nodeName.toLowerCase(),h=d._config.usePrefixes?" -webkit- -moz- -o- -ms- ".split(" "):[];d._prefixes=h;var m=d.testStyles=l;Modernizr.addTest("touchevents",function(){var t;if("ontouchstart"in e||e.DocumentTouch&&n instanceof DocumentTouch)t=!0;else{var o=["@media (",h.join("touch-enabled),("),"heartz",")","{#modernizr{top:9px;position:absolute}}"].join("");m(o,function(e){t=9===e.offsetTop})}return t}),s(),a(f),delete d.addTest,delete d.addAsyncTest;for(var v=0;v<Modernizr._q.length;v++)Modernizr._q[v]();e.Modernizr=Modernizr}(window,document); !(function(e, n, t) {
function o(e, n) {
return typeof e === n;
}
function s() {
var e, n, t, s, a, i, r;
for (var l in c)
if (c.hasOwnProperty(l)) {
if (
((e = []),
(n = c[l]),
n.name &&
(e.push(n.name.toLowerCase()),
n.options && n.options.aliases && n.options.aliases.length))
)
for (t = 0; t < n.options.aliases.length; t++)
e.push(n.options.aliases[t].toLowerCase());
for (s = o(n.fn, "function") ? n.fn() : n.fn, a = 0; a < e.length; a++)
(i = e[a]),
(r = i.split(".")),
1 === r.length
? (Modernizr[r[0]] = s)
: (!Modernizr[r[0]] ||
Modernizr[r[0]] instanceof Boolean ||
(Modernizr[r[0]] = new Boolean(Modernizr[r[0]])),
(Modernizr[r[0]][r[1]] = s)),
f.push((s ? "" : "no-") + r.join("-"));
}
}
function a(e) {
var n = u.className,
t = Modernizr._config.classPrefix || "";
if ((p && (n = n.baseVal), Modernizr._config.enableJSClass)) {
var o = new RegExp("(^|\\s)" + t + "no-js(\\s|$)");
n = n.replace(o, "$1" + t + "js$2");
}
Modernizr._config.enableClasses &&
((n += " " + t + e.join(" " + t)),
p ? (u.className.baseVal = n) : (u.className = n));
}
function i() {
return "function" != typeof n.createElement
? n.createElement(arguments[0])
: p
? n.createElementNS.call(n, "http://www.w3.org/2000/svg", arguments[0])
: n.createElement.apply(n, arguments);
}
function r() {
var e = n.body;
return e || ((e = i(p ? "svg" : "body")), (e.fake = !0)), e;
}
function l(e, t, o, s) {
var a,
l,
f,
c,
d = "modernizr",
p = i("div"),
h = r();
if (parseInt(o, 10))
for (; o--; )
(f = i("div")), (f.id = s ? s[o] : d + (o + 1)), p.appendChild(f);
return (
(a = i("style")),
(a.type = "text/css"),
(a.id = "s" + d),
(h.fake ? h : p).appendChild(a),
h.appendChild(p),
a.styleSheet
? (a.styleSheet.cssText = e)
: a.appendChild(n.createTextNode(e)),
(p.id = d),
h.fake &&
((h.style.background = ""),
(h.style.overflow = "hidden"),
(c = u.style.overflow),
(u.style.overflow = "hidden"),
u.appendChild(h)),
(l = t(p, e)),
h.fake
? (h.parentNode.removeChild(h), (u.style.overflow = c), u.offsetHeight)
: p.parentNode.removeChild(p),
!!l
);
}
var f = [],
c = [],
d = {
_version: "3.2.0",
_config: {
classPrefix: "",
enableClasses: !0,
enableJSClass: !0,
usePrefixes: !0
},
_q: [],
on: function(e, n) {
var t = this;
setTimeout(function() {
n(t[e]);
}, 0);
},
addTest: function(e, n, t) {
c.push({ name: e, fn: n, options: t });
},
addAsyncTest: function(e) {
c.push({ name: null, fn: e });
}
},
Modernizr = function() {};
(Modernizr.prototype = d), (Modernizr = new Modernizr());
var u = n.documentElement,
p = "svg" === u.nodeName.toLowerCase(),
h = d._config.usePrefixes ? " -webkit- -moz- -o- -ms- ".split(" ") : [];
d._prefixes = h;
var m = (d.testStyles = l);
Modernizr.addTest("touchevents", function() {
var t;
if ("ontouchstart" in e || (e.DocumentTouch && n instanceof DocumentTouch))
t = !0;
else {
var o = [
"@media (",
h.join("touch-enabled),("),
"heartz",
")",
"{#modernizr{top:9px;position:absolute}}"
].join("");
m(o, function(e) {
t = 9 === e.offsetTop;
});
}
return t;
}),
s(),
a(f),
delete d.addTest,
delete d.addAsyncTest;
for (var v = 0; v < Modernizr._q.length; v++) Modernizr._q[v]();
e.Modernizr = Modernizr;
})(window, document);

View file

@ -1,15 +1,15 @@
import { secondsToHis } from '@/utils' import { secondsToHis } from "@/utils";
export default { export default {
computed: { computed: {
length () { length() {
return this.album.songs.reduce((acc, song) => { return this.album.songs.reduce((acc, song) => {
return acc + song.length return acc + song.length;
}, 0) }, 0);
}, },
fmtLength () { fmtLength() {
return secondsToHis(this.length) return secondsToHis(this.length);
} }
} }
} };

View file

@ -1,33 +1,33 @@
import { secondsToHis } from '@/utils' import { secondsToHis } from "@/utils";
import config from '@/config' import config from "@/config";
export default { export default {
computed: { computed: {
length () { length() {
return this.artist.songs.reduce((acc, song) => { return this.artist.songs.reduce((acc, song) => {
return acc + song.length return acc + song.length;
}, 0) }, 0);
}, },
fmtLength () { fmtLength() {
return secondsToHis(this.length) return secondsToHis(this.length);
}, },
image () { image() {
if (!this.artist.image) { if (!this.artist.image) {
this.artist.image = config.unknownCover this.artist.image = config.unknownCover;
this.artist.albums.every(album => { this.artist.albums.every(album => {
// If there's a "real" cover, use it. // If there's a "real" cover, use it.
if (album.image !== config.unknownCover) { if (album.image !== config.unknownCover) {
this.artist.image = album.cover this.artist.image = album.cover;
// I want to break free. // I want to break free.
return false return false;
} }
}) });
} }
return this.artist.image return this.artist.image;
} }
} }
} };

View file

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

View file

@ -1,4 +1,4 @@
import toTopButton from '@/components/shared/to-top-button.vue' import toTopButton from "@/components/shared/to-top-button.vue";
/** /**
* Add a "infinite scroll" functionality to any component using this mixin. * Add a "infinite scroll" functionality to any component using this mixin.
@ -8,27 +8,27 @@ import toTopButton from '@/components/shared/to-top-button.vue'
export default { export default {
components: { toTopButton }, components: { toTopButton },
data () { data() {
return { return {
numOfItems: 30, // Number of currently loaded and displayed items numOfItems: 30, // Number of currently loaded and displayed items
perPage: 30 // Number of items to be loaded per "page" perPage: 30 // Number of items to be loaded per "page"
} };
}, },
methods: { methods: {
scrolling ({ target: { scrollTop, clientHeight, scrollHeight }}) { scrolling({ target: { scrollTop, clientHeight, scrollHeight } }) {
// Here we check if the user has scrolled to the end of the wrapper (or 32px to the end). // Here we check if the user has scrolled to the end of the wrapper (or 32px to the end).
// If that's true, load more items. // If that's true, load more items.
if (scrollTop + clientHeight >= scrollHeight - 32) { if (scrollTop + clientHeight >= scrollHeight - 32) {
this.displayMore() this.displayMore();
} }
}, },
/** /**
* Load and display more items into the scrollable area. * Load and display more items into the scrollable area.
*/ */
displayMore () { displayMore() {
this.numOfItems += this.perPage this.numOfItems += this.perPage;
} }
} }
} };

View file

@ -1,6 +1,6 @@
import { each } from 'lodash' import { each } from "lodash";
import { queueStore, playlistStore, favoriteStore } from '@/stores' import { queueStore, playlistStore, favoriteStore } from "@/stores";
/** /**
* Includes the methods triggerable on a song (context) menu. * Includes the methods triggerable on a song (context) menu.
@ -9,57 +9,57 @@ import { queueStore, playlistStore, favoriteStore } from '@/stores'
* for example close() and open(). * for example close() and open().
*/ */
export default { export default {
data () { data() {
return { return {
shown: false, shown: false,
top: 0, top: 0,
left: 0 left: 0
} };
}, },
methods: { methods: {
open () {}, open() {},
/** /**
* Close all submenus. * Close all submenus.
*/ */
close () { close() {
each(Array.from(this.$el.querySelectorAll('.submenu')), el => { each(Array.from(this.$el.querySelectorAll(".submenu")), el => {
el.style.display = 'none' el.style.display = "none";
}) });
this.shown = false this.shown = false;
}, },
/** /**
* Queue select songs after the current song. * Queue select songs after the current song.
*/ */
queueSongsAfterCurrent () { queueSongsAfterCurrent() {
queueStore.queueAfterCurrent(this.songs) queueStore.queueAfterCurrent(this.songs);
this.close() this.close();
}, },
/** /**
* Queue selected songs to bottom of queue. * Queue selected songs to bottom of queue.
*/ */
queueSongsToBottom () { queueSongsToBottom() {
queueStore.queue(this.songs) queueStore.queue(this.songs);
this.close() this.close();
}, },
/** /**
* Queue selected songs to top of queue. * Queue selected songs to top of queue.
*/ */
queueSongsToTop () { queueSongsToTop() {
queueStore.queue(this.songs, false, true) queueStore.queue(this.songs, false, true);
this.close() this.close();
}, },
/** /**
* Add the selected songs into Favorites. * Add the selected songs into Favorites.
*/ */
addSongsToFavorite () { addSongsToFavorite() {
favoriteStore.like(this.songs) favoriteStore.like(this.songs);
this.close() this.close();
}, },
/** /**
@ -67,9 +67,9 @@ export default {
* *
* @param {Object} playlist The playlist. * @param {Object} playlist The playlist.
*/ */
addSongsToExistingPlaylist (playlist) { addSongsToExistingPlaylist(playlist) {
playlistStore.addSongs(playlist, this.songs) playlistStore.addSongs(playlist, this.songs);
this.close() this.close();
} }
} }
} };

View file

@ -1,12 +1,12 @@
import './static-loader' import "./static-loader";
import Vue from 'vue' import Vue from "vue";
import { http } from '@/services' import { http } from "@/services";
import App from './app.vue' import App from "./app.vue";
new Vue({ new Vue({
el: '#app', el: "#app",
render: h => h(App), render: h => h(App),
created () { created() {
http.init() http.init();
} }
}) });

View file

@ -1,5 +1,5 @@
import 'babel-polyfill/dist/polyfill.min.js' import "babel-polyfill/dist/polyfill.min.js";
import '@/libs/modernizr-custom.js' import "@/libs/modernizr-custom.js";
import '@/../css/meyer-reset.min.css' import "@/../css/meyer-reset.min.css";
import 'nouislider/distribute/nouislider.min.css' import "nouislider/distribute/nouislider.min.css";
import 'font-awesome/css/font-awesome.min.css' import "font-awesome/css/font-awesome.min.css";

View file

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

View file

@ -1,7 +1,7 @@
import { reduce } from 'lodash' import { reduce } from "lodash";
import { playlistStore, favoriteStore } from '@/stores' import { playlistStore, favoriteStore } from "@/stores";
import { ls } from '.' import { ls } from ".";
export const download = { export const download = {
/** /**
@ -9,10 +9,10 @@ export const download = {
* *
* @param {Array.<Object>|Object} songs * @param {Array.<Object>|Object} songs
*/ */
fromSongs (songs) { fromSongs(songs) {
songs = [].concat(songs) songs = [].concat(songs);
const query = reduce(songs, (q, song) => `songs[]=${song.id}&${q}`, '') const query = reduce(songs, (q, song) => `songs[]=${song.id}&${q}`, "");
return this.trigger(`songs?${query}`) return this.trigger(`songs?${query}`);
}, },
/** /**
@ -20,8 +20,8 @@ export const download = {
* *
* @param {Object} album * @param {Object} album
*/ */
fromAlbum (album) { fromAlbum(album) {
return this.trigger(`album/${album.id}`) return this.trigger(`album/${album.id}`);
}, },
/** /**
@ -29,11 +29,11 @@ export const download = {
* *
* @param {Object} artist * @param {Object} artist
*/ */
fromArtist (artist) { fromArtist(artist) {
// It's safe to assume an artist always has songs. // It's safe to assume an artist always has songs.
// After all, what's an artist without her songs? // After all, what's an artist without her songs?
// (See what I did there? Yes, I'm advocating for women's rights). // (See what I did there? Yes, I'm advocating for women's rights).
return this.trigger(`artist/${artist.id}`) return this.trigger(`artist/${artist.id}`);
}, },
/** /**
@ -41,24 +41,24 @@ export const download = {
* *
* @param {Object} playlist * @param {Object} playlist
*/ */
fromPlaylist (playlist) { fromPlaylist(playlist) {
if (!playlistStore.getSongs(playlist).length) { if (!playlistStore.getSongs(playlist).length) {
return return;
} }
return this.trigger(`playlist/${playlist.id}`) return this.trigger(`playlist/${playlist.id}`);
}, },
/** /**
* Download all favorite songs. * Download all favorite songs.
*/ */
fromFavorites () { fromFavorites() {
if (!favoriteStore.all.length) { if (!favoriteStore.all.length) {
console.warn("You don't like any song? Come on, don't be that grumpy.") console.warn("You don't like any song? Come on, don't be that grumpy.");
return return;
} }
return this.trigger('favorites') return this.trigger("favorites");
}, },
/** /**
@ -67,11 +67,14 @@ export const download = {
* @param {string} uri The uri segment, corresponding to the song(s), * @param {string} uri The uri segment, corresponding to the song(s),
* artist, playlist, or album. * artist, playlist, or album.
*/ */
trigger (uri) { trigger(uri) {
const sep = uri.indexOf('?') === -1 ? '?' : '&' const sep = uri.indexOf("?") === -1 ? "?" : "&";
const iframe = document.createElement('iframe') const iframe = document.createElement("iframe");
iframe.style.display = 'none' iframe.style.display = "none";
iframe.setAttribute('src', `/api/download/${uri}${sep}jwt-token=${ls.get('jwt-token')}`) iframe.setAttribute(
document.body.appendChild(iframe) "src",
`/api/download/${uri}${sep}jwt-token=${ls.get("jwt-token")}`
);
document.body.appendChild(iframe);
} }
} };

View file

@ -1,72 +1,84 @@
import axios from 'axios' import axios from "axios";
import NProgress from 'nprogress' import NProgress from "nprogress";
import { event } from '@/utils' import { event } from "@/utils";
import { ls } from '@/services' import { ls } from "@/services";
/** /**
* Responsible for all HTTP requests. * Responsible for all HTTP requests.
*/ */
export const http = { export const http = {
request (method, url, data, successCb = null, errorCb = null) { request(method, url, data, successCb = null, errorCb = null) {
axios.request({ axios
url, .request({
data, url,
method: method.toLowerCase() data,
}).then(successCb).catch(errorCb) method: method.toLowerCase()
})
.then(successCb)
.catch(errorCb);
}, },
get (url, successCb = null, errorCb = null) { get(url, successCb = null, errorCb = null) {
return this.request('get', url, {}, successCb, errorCb) return this.request("get", url, {}, successCb, errorCb);
}, },
post (url, data, successCb = null, errorCb = null) { post(url, data, successCb = null, errorCb = null) {
return this.request('post', url, data, successCb, errorCb) return this.request("post", url, data, successCb, errorCb);
}, },
put (url, data, successCb = null, errorCb = null) { put(url, data, successCb = null, errorCb = null) {
return this.request('put', url, data, successCb, errorCb) return this.request("put", url, data, successCb, errorCb);
}, },
delete (url, data = {}, successCb = null, errorCb = null) { delete(url, data = {}, successCb = null, errorCb = null) {
return this.request('delete', url, data, successCb, errorCb) return this.request("delete", url, data, successCb, errorCb);
}, },
/** /**
* Init the service. * Init the service.
*/ */
init () { init() {
axios.defaults.baseURL = '/api' axios.defaults.baseURL = "/api";
// Intercept the request to make sure the token is injected into the header. // Intercept the request to make sure the token is injected into the header.
axios.interceptors.request.use(config => { axios.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${ls.get('jwt-token')}` config.headers.Authorization = `Bearer ${ls.get("jwt-token")}`;
return config return config;
}) });
// Intercept the response and… // Intercept the response and…
axios.interceptors.response.use(response => { axios.interceptors.response.use(
NProgress.done() response => {
NProgress.done();
// …get the token from the header or response data if exists, and save it. // …get the token from the header or response data if exists, and save it.
const token = response.headers['Authorization'] || response.data['token'] const token =
if (token) { response.headers["Authorization"] || response.data["token"];
ls.set('jwt-token', token) if (token) {
} ls.set("jwt-token", token);
return response
}, error => {
NProgress.done()
// Also, if we receive a Bad Request / Unauthorized error
if (error.response.status === 400 || error.response.status === 401) {
// and we're not trying to login
if (!(error.config.method === 'post' && /\/api\/me\/?$/.test(error.config.url))) {
// the token must have expired. Log out.
event.emit('logout')
} }
}
return Promise.reject(error) return response;
}) },
error => {
NProgress.done();
// Also, if we receive a Bad Request / Unauthorized error
if (error.response.status === 400 || error.response.status === 401) {
// and we're not trying to login
if (
!(
error.config.method === "post" &&
/\/api\/me\/?$/.test(error.config.url)
)
) {
// the token must have expired. Log out.
event.emit("logout");
}
}
return Promise.reject(error);
}
);
} }
} };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,118 +1,140 @@
import { shuffle, orderBy } from 'lodash' import { shuffle, orderBy } from "lodash";
import plyr from 'plyr' import plyr from "plyr";
import Vue from 'vue' import Vue from "vue";
import isMobile from 'ismobilejs' import isMobile from "ismobilejs";
import { event, isMediaSessionSupported } from '@/utils' import { event, isMediaSessionSupported } from "@/utils";
import { queueStore, sharedStore, userStore, songStore, preferenceStore as preferences } from '@/stores' import {
import { socket } from '@/services' queueStore,
import config from '@/config' sharedStore,
import router from '@/router' userStore,
songStore,
preferenceStore as preferences
} from "@/stores";
import { socket } from "@/services";
import config from "@/config";
import router from "@/router";
export const playback = { export const playback = {
player: null, player: null,
volumeInput: null, volumeInput: null,
repeatModes: ['NO_REPEAT', 'REPEAT_ALL', 'REPEAT_ONE'], repeatModes: ["NO_REPEAT", "REPEAT_ALL", "REPEAT_ONE"],
initialized: false, initialized: false,
/** /**
* Initialize the playback service for this whole Koel app. * 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. // We don't need to init this service twice, or the media events will be duplicated.
if (this.initialized) { if (this.initialized) {
return return;
} }
this.player = plyr.setup({ this.player = plyr.setup({
controls: [] controls: []
})[0] })[0];
this.audio = document.querySelector('audio') this.audio = document.querySelector("audio");
this.volumeInput = document.getElementById('volumeRange') this.volumeInput = document.getElementById("volumeRange");
const player = document.querySelector('.plyr') const player = document.querySelector(".plyr");
/** /**
* Listen to 'error' event on the audio player and play the next song if any. * Listen to 'error' event on the audio player and play the next song if any.
*/ */
player.addEventListener('error', () => this.playNext(), true) player.addEventListener("error", () => this.playNext(), true);
/** /**
* Listen to 'ended' event on the audio player and play the next song in the queue. * Listen to 'ended' event on the audio player and play the next song in the queue.
*/ */
player.addEventListener('ended', e => { player.addEventListener("ended", e => {
if (sharedStore.state.useLastfm && userStore.current.preferences.lastfm_session_key) { if (
songStore.scrobble(queueStore.current) sharedStore.state.useLastfm &&
userStore.current.preferences.lastfm_session_key
) {
songStore.scrobble(queueStore.current);
} }
preferences.repeatMode === 'REPEAT_ONE' ? this.restart() : this.playNext() preferences.repeatMode === "REPEAT_ONE"
}) ? this.restart()
: this.playNext();
});
/** /**
* Attempt to preload the next song. * Attempt to preload the next song.
*/ */
player.addEventListener('canplaythrough', e => { player.addEventListener("canplaythrough", e => {
const nextSong = queueStore.next const nextSong = queueStore.next;
if (!nextSong || nextSong.preloaded || (isMobile.any && preferences.transcodeOnMobile)) { if (
!nextSong ||
nextSong.preloaded ||
(isMobile.any && preferences.transcodeOnMobile)
) {
// Don't preload if // Don't preload if
// - there's no next song // - there's no next song
// - next song has already been preloaded // - next song has already been preloaded
// - we're on mobile and "transcode" option is checked // - we're on mobile and "transcode" option is checked
return return;
} }
const audio = document.createElement('audio') const audio = document.createElement("audio");
audio.setAttribute('src', songStore.getSourceUrl(nextSong)) audio.setAttribute("src", songStore.getSourceUrl(nextSong));
audio.setAttribute('preload', 'auto') audio.setAttribute("preload", "auto");
audio.load() audio.load();
nextSong.preloaded = true nextSong.preloaded = true;
}) });
player.addEventListener('timeupdate', e => { player.addEventListener("timeupdate", e => {
const song = queueStore.current const song = queueStore.current;
if (this.player.media.currentTime > 10 && !song.registeredPlayCount) { if (this.player.media.currentTime > 10 && !song.registeredPlayCount) {
// After 10 seconds, register a play count and add it into "recently played" list // After 10 seconds, register a play count and add it into "recently played" list
songStore.addRecentlyPlayed(song) songStore.addRecentlyPlayed(song);
songStore.registerPlay(song) songStore.registerPlay(song);
song.registeredPlayCount = true song.registeredPlayCount = true;
} }
}) });
// On init, set the volume to the value found in the local storage. // On init, set the volume to the value found in the local storage.
this.setVolume(preferences.volume) this.setVolume(preferences.volume);
// Init the equalizer if supported. // Init the equalizer if supported.
event.emit('equalizer:init', this.player.media) event.emit("equalizer:init", this.player.media);
if (isMediaSessionSupported()) { if (isMediaSessionSupported()) {
navigator.mediaSession.setActionHandler('play', () => this.resume()) navigator.mediaSession.setActionHandler("play", () => this.resume());
navigator.mediaSession.setActionHandler('pause', () => this.pause()) navigator.mediaSession.setActionHandler("pause", () => this.pause());
navigator.mediaSession.setActionHandler('previoustrack', () => this.playPrev()) navigator.mediaSession.setActionHandler("previoustrack", () =>
navigator.mediaSession.setActionHandler('nexttrack', () => this.playNext()) this.playPrev()
);
navigator.mediaSession.setActionHandler("nexttrack", () =>
this.playNext()
);
} }
socket.listen('playback:toggle', () => this.toggle()) socket
.listen('playback:next', () => this.playNext()) .listen("playback:toggle", () => this.toggle())
.listen('playback:prev', () => this.playPrev()) .listen("playback:next", () => this.playNext())
.listen('status:get', () => { .listen("playback:prev", () => this.playPrev())
const data = queueStore.current ? songStore.generateDataToBroadcast(queueStore.current) : {} .listen("status:get", () => {
data.volume = this.volumeInput.value const data = queueStore.current
socket.broadcast('status', data) ? songStore.generateDataToBroadcast(queueStore.current)
: {};
data.volume = this.volumeInput.value;
socket.broadcast("status", data);
}) })
.listen('song:getcurrent', () => { .listen("song:getcurrent", () => {
socket.broadcast( socket.broadcast(
'song', "song",
queueStore.current queueStore.current
? songStore.generateDataToBroadcast(queueStore.current) ? songStore.generateDataToBroadcast(queueStore.current)
: { song: null } : { song: null }
) );
}) })
.listen('volume:set', ({ volume }) => this.setVolume(volume)) .listen("volume:set", ({ volume }) => this.setVolume(volume));
this.initialized = true this.initialized = true;
}, },
/** /**
@ -125,29 +147,31 @@ export const playback = {
* *
* @param {Object} song The song to play * @param {Object} song The song to play
*/ */
play (song) { play(song) {
if (!song) { if (!song) {
return return;
} }
if (queueStore.current) { if (queueStore.current) {
queueStore.current.playbackState = 'stopped' queueStore.current.playbackState = "stopped";
} }
song.playbackState = 'playing' song.playbackState = "playing";
// Set the song as the current song // Set the song as the current song
queueStore.current = song queueStore.current = song;
// Manually set the `src` attribute of the audio to prevent plyr from resetting // Manually set the `src` attribute of the audio to prevent plyr from resetting
// the audio media object and cause our equalizer to malfunction. // the audio media object and cause our equalizer to malfunction.
this.player.media.src = songStore.getSourceUrl(song) this.player.media.src = songStore.getSourceUrl(song);
document.title = `${song.title}${config.appTitle}` document.title = `${song.title}${config.appTitle}`;
document.querySelector('.plyr audio').setAttribute('title', `${song.artist.name} - ${song.title}`) document
.querySelector(".plyr audio")
.setAttribute("title", `${song.artist.name} - ${song.title}`);
// We'll just "restart" playing the song, which will handle notification, scrobbling etc. // We'll just "restart" playing the song, which will handle notification, scrobbling etc.
this.restart() this.restart();
}, },
/** /**
@ -155,59 +179,59 @@ export const playback = {
* *
* @param {Object} song * @param {Object} song
*/ */
showNotification (song) { showNotification(song) {
// Show the notification if we're allowed to // Show the notification if we're allowed to
if (!window.Notification || !preferences.notify) { if (!window.Notification || !preferences.notify) {
return return;
} }
try { try {
const notif = new window.Notification(`${song.title}`, { const notif = new window.Notification(`${song.title}`, {
icon: song.album.cover, icon: song.album.cover,
body: `${song.album.name} ${song.artist.name}` body: `${song.album.name} ${song.artist.name}`
}) });
notif.onclick = () => window.focus() notif.onclick = () => window.focus();
// Close the notif after 5 secs. // Close the notif after 5 secs.
window.setTimeout(() => notif.close(), 5000) window.setTimeout(() => notif.close(), 5000);
} catch (e) { } catch (e) {
// Notification fails. // Notification fails.
// @link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification // @link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification
} }
if ('mediaSession' in navigator) { if ("mediaSession" in navigator) {
/* global MediaMetadata */ /* global MediaMetadata */
navigator.mediaSession.metadata = new MediaMetadata({ navigator.mediaSession.metadata = new MediaMetadata({
title: song.title, title: song.title,
artist: song.artist.name, artist: song.artist.name,
album: song.album.name, album: song.album.name,
artwork: [ artwork: [
{ src: song.album.cover, sizes: '256x256', type: 'image/png' } { src: song.album.cover, sizes: "256x256", type: "image/png" }
] ]
}) });
} }
}, },
/** /**
* Restart playing a song. * Restart playing a song.
*/ */
restart () { restart() {
const song = queueStore.current const song = queueStore.current;
this.showNotification(song) this.showNotification(song);
// Record the UNIX timestamp the song start playing, for scrobbling purpose // Record the UNIX timestamp the song start playing, for scrobbling purpose
song.playStartTime = Math.floor(Date.now() / 1000) song.playStartTime = Math.floor(Date.now() / 1000);
song.registeredPlayCount = false song.registeredPlayCount = false;
event.emit('song:played', song) event.emit("song:played", song);
socket.broadcast('song', songStore.generateDataToBroadcast(song)) socket.broadcast("song", songStore.generateDataToBroadcast(song));
this.player.restart() this.player.restart();
this.player.play() this.player.play();
}, },
/** /**
@ -216,13 +240,13 @@ export const playback = {
* *
* @return {Object} The song * @return {Object} The song
*/ */
get next () { get next() {
if (queueStore.next) { if (queueStore.next) {
return queueStore.next return queueStore.next;
} }
if (preferences.repeatMode === 'REPEAT_ALL') { if (preferences.repeatMode === "REPEAT_ALL") {
return queueStore.first return queueStore.first;
} }
}, },
@ -232,13 +256,13 @@ export const playback = {
* *
* @return {Object} The song * @return {Object} The song
*/ */
get previous () { get previous() {
if (queueStore.previous) { if (queueStore.previous) {
return queueStore.previous return queueStore.previous;
} }
if (preferences.repeatMode === 'REPEAT_ALL') { if (preferences.repeatMode === "REPEAT_ALL") {
return queueStore.last return queueStore.last;
} }
}, },
@ -246,44 +270,44 @@ export const playback = {
* Circle through the repeat mode. * Circle through the repeat mode.
* The selected mode will be stored into local storage as well. * The selected mode will be stored into local storage as well.
*/ */
changeRepeatMode () { changeRepeatMode() {
let index = this.repeatModes.indexOf(preferences.repeatMode) + 1 let index = this.repeatModes.indexOf(preferences.repeatMode) + 1;
if (index >= this.repeatModes.length) { if (index >= this.repeatModes.length) {
index = 0 index = 0;
} }
preferences.repeatMode = this.repeatModes[index] preferences.repeatMode = this.repeatModes[index];
}, },
/** /**
* Play the prev song in the queue, if one is found. * 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. * 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, // If the song's duration is greater than 5 seconds and we've passed 5 seconds into it,
// restart playing instead. // restart playing instead.
if (this.player.media.currentTime > 5 && queueStore.current.length > 5) { if (this.player.media.currentTime > 5 && queueStore.current.length > 5) {
this.player.restart() this.player.restart();
return return;
} }
const prev = this.previous const prev = this.previous;
!prev && preferences.repeatMode === 'NO_REPEAT' !prev && preferences.repeatMode === "NO_REPEAT"
? this.stop() ? this.stop()
: this.play(prev) : this.play(prev);
}, },
/** /**
* Play the next song in the queue, if one is found. * 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. * If the next song is not found and the current mode is NO_REPEAT, we stop completely.
*/ */
playNext () { playNext() {
const next = this.next const next = this.next;
!next && preferences.repeatMode === 'NO_REPEAT' !next && preferences.repeatMode === "NO_REPEAT"
? this.stop() // Nothing lasts forever, even cold November rain. ? this.stop() // Nothing lasts forever, even cold November rain.
: this.play(next) : this.play(next);
}, },
/** /**
@ -292,84 +316,90 @@ export const playback = {
* @param {Number} volume 0-10 * @param {Number} volume 0-10
* @param {Boolean=true} persist Whether the volume should be saved into local storage * @param {Boolean=true} persist Whether the volume should be saved into local storage
*/ */
setVolume (volume, persist = true) { setVolume(volume, persist = true) {
this.player.setVolume(volume) this.player.setVolume(volume);
if (persist) { if (persist) {
preferences.volume = volume preferences.volume = volume;
} }
this.volumeInput.value = volume this.volumeInput.value = volume;
}, },
/** /**
* Mute playback. * Mute playback.
*/ */
mute () { mute() {
this.setVolume(0, false) this.setVolume(0, false);
}, },
/** /**
* Unmute playback. * Unmute playback.
*/ */
unmute () { unmute() {
// If the saved volume is 0, we unmute to the default level (7). // If the saved volume is 0, we unmute to the default level (7).
if (preferences.volume === '0' || preferences.volume === 0) { if (preferences.volume === "0" || preferences.volume === 0) {
preferences.volume = 7 preferences.volume = 7;
} }
this.setVolume(preferences.volume) this.setVolume(preferences.volume);
}, },
/** /**
* Completely stop playback. * Completely stop playback.
*/ */
stop () { stop() {
document.title = config.appTitle document.title = config.appTitle;
this.player.pause() this.player.pause();
this.player.seek(0) this.player.seek(0);
if (queueStore.current) { if (queueStore.current) {
queueStore.current.playbackState = 'stopped' queueStore.current.playbackState = "stopped";
} }
socket.broadcast('playback:stopped') socket.broadcast("playback:stopped");
}, },
/** /**
* Pause playback. * Pause playback.
*/ */
pause () { pause() {
this.player.pause() this.player.pause();
queueStore.current.playbackState = 'paused' queueStore.current.playbackState = "paused";
socket.broadcast('song', songStore.generateDataToBroadcast(queueStore.current)) socket.broadcast(
"song",
songStore.generateDataToBroadcast(queueStore.current)
);
}, },
/** /**
* Resume playback. * Resume playback.
*/ */
resume () { resume() {
this.player.play() this.player.play();
queueStore.current.playbackState = 'playing' queueStore.current.playbackState = "playing";
event.emit('song:played', queueStore.current) event.emit("song:played", queueStore.current);
socket.broadcast('song', songStore.generateDataToBroadcast(queueStore.current)) socket.broadcast(
"song",
songStore.generateDataToBroadcast(queueStore.current)
);
}, },
/** /**
* Toggle playback. * Toggle playback.
*/ */
toggle () { toggle() {
if (!queueStore.current) { if (!queueStore.current) {
this.playFirstInQueue() this.playFirstInQueue();
return return;
} }
if (queueStore.current.playbackState !== 'playing') { if (queueStore.current.playbackState !== "playing") {
this.resume() this.resume();
return return;
} }
this.pause() this.pause();
}, },
/** /**
@ -378,35 +408,35 @@ export const playback = {
* @param {?Array.<Object>} songs An array of song objects. Defaults to all songs if null. * @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. * @param {Boolean=false} shuffled Whether to shuffle the songs before playing.
*/ */
queueAndPlay (songs = null, shuffled = false) { queueAndPlay(songs = null, shuffled = false) {
if (!songs) { if (!songs) {
songs = songStore.all songs = songStore.all;
} }
if (!songs.length) { if (!songs.length) {
return return;
} }
if (shuffled) { if (shuffled) {
songs = shuffle(songs) songs = shuffle(songs);
} }
queueStore.queue(songs, true) queueStore.queue(songs, true);
// Wrap this inside a nextTick() to wait for the DOM to complete updating // Wrap this inside a nextTick() to wait for the DOM to complete updating
// and then play the first song in the queue. // and then play the first song in the queue.
Vue.nextTick(() => { Vue.nextTick(() => {
router.go('queue') router.go("queue");
this.play(queueStore.first) this.play(queueStore.first);
}) });
}, },
/** /**
* Play the first song in the queue. * Play the first song in the queue.
* If the current queue is empty, try creating it by shuffling all songs. * If the current queue is empty, try creating it by shuffling all songs.
*/ */
playFirstInQueue () { playFirstInQueue() {
queueStore.all.length ? this.play(queueStore.first) : this.queueAndPlay() queueStore.all.length ? this.play(queueStore.first) : this.queueAndPlay();
}, },
/** /**
@ -415,10 +445,10 @@ export const playback = {
* @param {Object} artist The artist object * @param {Object} artist The artist object
* @param {Boolean=true} shuffled Whether to shuffle the songs * @param {Boolean=true} shuffled Whether to shuffle the songs
*/ */
playAllByArtist ({ songs }, shuffled = true) { playAllByArtist({ songs }, shuffled = true) {
shuffled shuffled
? this.queueAndPlay(songs, true) ? this.queueAndPlay(songs, true)
: this.queueAndPlay(orderBy(songs, 'album_id', 'track')) : this.queueAndPlay(orderBy(songs, "album_id", "track"));
}, },
/** /**
@ -427,9 +457,9 @@ export const playback = {
* @param {Object} album The album object * @param {Object} album The album object
* @param {Boolean=true} shuffled Whether to shuffle the songs * @param {Boolean=true} shuffled Whether to shuffle the songs
*/ */
playAllInAlbum ({ songs }, shuffled = true) { playAllInAlbum({ songs }, shuffled = true) {
shuffled shuffled
? this.queueAndPlay(songs, true) ? this.queueAndPlay(songs, true)
: this.queueAndPlay(orderBy(songs, 'track')) : this.queueAndPlay(orderBy(songs, "track"));
} }
} };

View file

@ -1,37 +1,37 @@
import Pusher from 'pusher-js' import Pusher from "pusher-js";
import { userStore } from '@/stores' import { userStore } from "@/stores";
import { ls } from '.' import { ls } from ".";
export const socket = { export const socket = {
pusher: null, pusher: null,
channel: null, channel: null,
async init () { async init() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!window.PUSHER_APP_KEY) { if (!window.PUSHER_APP_KEY) {
return resolve() return resolve();
} }
this.pusher = new Pusher(window.PUSHER_APP_KEY, { this.pusher = new Pusher(window.PUSHER_APP_KEY, {
authEndpoint: '/api/broadcasting/auth', authEndpoint: "/api/broadcasting/auth",
auth: { auth: {
headers: { headers: {
Authorization: `Bearer ${ls.get('jwt-token')}` Authorization: `Bearer ${ls.get("jwt-token")}`
} }
}, },
cluster: window.PUSHER_APP_CLUSTER, cluster: window.PUSHER_APP_CLUSTER,
encrypted: true encrypted: true
}) });
this.channel = this.pusher.subscribe('private-koel') this.channel = this.pusher.subscribe("private-koel");
this.channel.bind('pusher:subscription_succeeded', () => { this.channel.bind("pusher:subscription_succeeded", () => {
return resolve() return resolve();
}) });
this.channel.bind('pusher:subscription_error', () => { this.channel.bind("pusher:subscription_error", () => {
return resolve() return resolve();
}) });
}) });
}, },
/** /**
@ -39,10 +39,11 @@ export const socket = {
* @param {string} eventName The event's name * @param {string} eventName The event's name
* @param {Object} data The event's data * @param {Object} data The event's data
*/ */
broadcast (eventName, data = {}) { broadcast(eventName, data = {}) {
this.channel && this.channel.trigger(`client-${eventName}.${userStore.current.id}`, data) this.channel &&
this.channel.trigger(`client-${eventName}.${userStore.current.id}`, data);
return this return this;
}, },
/** /**
@ -50,9 +51,12 @@ export const socket = {
* @param {string} eventName The event's name * @param {string} eventName The event's name
* @param {Function} cb * @param {Function} cb
*/ */
listen (eventName, cb) { listen(eventName, cb) {
this.channel && this.channel.bind(`client-${eventName}.${userStore.current.id}`, data => cb(data)) this.channel &&
this.channel.bind(`client-${eventName}.${userStore.current.id}`, data =>
cb(data)
);
return this return this;
} }
} };

View file

@ -1,6 +1,6 @@
import { http } from '.' import { http } from ".";
import { event } from '@/utils' import { event } from "@/utils";
import router from '@/router' import router from "@/router";
export const youtube = { export const youtube = {
/** /**
@ -8,21 +8,23 @@ export const youtube = {
* *
* @param {Object} song * @param {Object} song
*/ */
searchVideosRelatedToSong (song) { searchVideosRelatedToSong(song) {
if (!song.youtube) { if (!song.youtube) {
song.youtube = {} song.youtube = {};
} }
const pageToken = song.youtube.nextPageToken || '' const pageToken = song.youtube.nextPageToken || "";
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.get(`youtube/search/song/${song.id}?pageToken=${pageToken}`, http.get(
({ data: { nextPageToken, items }}) => { `youtube/search/song/${song.id}?pageToken=${pageToken}`,
song.youtube.nextPageToken = nextPageToken ({ data: { nextPageToken, items } }) => {
song.youtube.items.push(...items) song.youtube.nextPageToken = nextPageToken;
resolve() song.youtube.items.push(...items);
}, error => reject(error) resolve();
) },
}) error => reject(error)
);
});
}, },
/** /**
@ -30,11 +32,11 @@ export const youtube = {
* *
* @param {Object} vide The video object * @param {Object} vide The video object
*/ */
play (video) { play(video) {
event.emit('youtube:play', { event.emit("youtube:play", {
id: video.id.videoId, id: video.id.videoId,
title: video.snippet.title title: video.snippet.title
}) });
router.go('youtube') router.go("youtube");
} }
} };

View file

@ -1,7 +1,7 @@
import 'babel-polyfill/dist/polyfill.min.js' import "babel-polyfill/dist/polyfill.min.js";
import 'plyr/dist/plyr.js' import "plyr/dist/plyr.js";
import './libs/modernizr-custom.js' import "./libs/modernizr-custom.js";
import '../css/meyer-reset.min.css' import "../css/meyer-reset.min.css";
import 'nouislider/distribute/nouislider.min.css' import "nouislider/distribute/nouislider.min.css";
import 'intersection-observer' import "intersection-observer";
import 'font-awesome/css/font-awesome.min.css' import "font-awesome/css/font-awesome.min.css";

View file

@ -1,10 +1,10 @@
/*eslint camelcase: ["error", {properties: "never"}]*/ /*eslint camelcase: ["error", {properties: "never"}]*/
import Vue from 'vue' import Vue from "vue";
import { reduce, each, union, difference, take, filter, orderBy } from 'lodash' import { reduce, each, union, difference, take, filter, orderBy } from "lodash";
import stub from '@/stubs/album' import stub from "@/stubs/album";
import { artistStore } from '.' import { artistStore } from ".";
export const albumStore = { export const albumStore = {
stub, stub,
@ -19,24 +19,24 @@ export const albumStore = {
* *
* @param {Array.<Object>} albums The array of album objects * @param {Array.<Object>} albums The array of album objects
*/ */
init (albums) { init(albums) {
// Traverse through the artists array and add their albums into our master album list. // Traverse through the artists array and add their albums into our master album list.
this.all = albums this.all = albums;
each(this.all, album => this.setupAlbum(album)) each(this.all, album => this.setupAlbum(album));
}, },
setupAlbum (album) { setupAlbum(album) {
const artist = artistStore.byId(album.artist_id) const artist = artistStore.byId(album.artist_id);
artist.albums = union(artist.albums, [album]) artist.albums = union(artist.albums, [album]);
Vue.set(album, 'artist', artist) Vue.set(album, "artist", artist);
Vue.set(album, 'info', null) Vue.set(album, "info", null);
Vue.set(album, 'songs', []) Vue.set(album, "songs", []);
Vue.set(album, 'playCount', 0) Vue.set(album, "playCount", 0);
this.cache[album.id] = album this.cache[album.id] = album;
return album return album;
}, },
/** /**
@ -44,8 +44,8 @@ export const albumStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all() {
return this.state.albums return this.state.albums;
}, },
/** /**
@ -53,12 +53,12 @@ export const albumStore = {
* *
* @param {Array.<Object>} value * @param {Array.<Object>} value
*/ */
set all (value) { set all(value) {
this.state.albums = value this.state.albums = value;
}, },
byId (id) { byId(id) {
return this.cache[id] return this.cache[id];
}, },
/** /**
@ -66,31 +66,35 @@ export const albumStore = {
* *
* @param {Array.<Object>|Object} albums * @param {Array.<Object>|Object} albums
*/ */
add (albums) { add(albums) {
albums = [].concat(albums) albums = [].concat(albums);
each(albums, album => { each(albums, album => {
this.setupAlbum(album, album.artist) this.setupAlbum(album, album.artist);
album.playCount = reduce(album.songs, (count, song) => count + song.playCount, 0) album.playCount = reduce(
}) album.songs,
(count, song) => count + song.playCount,
0
);
});
this.all = union(this.all, albums) this.all = union(this.all, albums);
}, },
purify () { purify() {
this.compact() this.compact();
}, },
/** /**
* Remove empty albums from the store. * Remove empty albums from the store.
*/ */
compact () { compact() {
const emptyAlbums = filter(this.all, album => album.songs.length === 0) const emptyAlbums = filter(this.all, album => album.songs.length === 0);
if (!emptyAlbums.length) { if (!emptyAlbums.length) {
return return;
} }
this.all = difference(this.all, emptyAlbums) this.all = difference(this.all, emptyAlbums);
each(emptyAlbums, album => delete this.cache[album.id]) each(emptyAlbums, album => delete this.cache[album.id]);
}, },
/** /**
@ -100,10 +104,13 @@ export const albumStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
getMostPlayed (n = 6) { getMostPlayed(n = 6) {
// Only non-unknown albums with actually play count are applicable. // Only non-unknown albums with actually play count are applicable.
const applicable = filter(this.all, album => album.playCount && album.id !== 1) const applicable = filter(
return take(orderBy(applicable, 'playCount', 'desc'), n) this.all,
album => album.playCount && album.id !== 1
);
return take(orderBy(applicable, "playCount", "desc"), n);
}, },
/** /**
@ -113,8 +120,8 @@ export const albumStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
getRecentlyAdded (n = 6) { getRecentlyAdded(n = 6) {
const applicable = filter(this.all, album => album.id !== 1) const applicable = filter(this.all, album => album.id !== 1);
return take(orderBy(applicable, 'created_at', 'desc'), n) return take(orderBy(applicable, "created_at", "desc"), n);
} }
} };

View file

@ -1,12 +1,12 @@
/*eslint camelcase: ["error", {properties: "never"}]*/ /*eslint camelcase: ["error", {properties: "never"}]*/
import Vue from 'vue' import Vue from "vue";
import { reduce, each, union, difference, take, filter, orderBy } from 'lodash' import { reduce, each, union, difference, take, filter, orderBy } from "lodash";
import stub from '@/stubs/artist' import stub from "@/stubs/artist";
const UNKNOWN_ARTIST_ID = 1 const UNKNOWN_ARTIST_ID = 1;
const VARIOUS_ARTISTS_ID = 2 const VARIOUS_ARTISTS_ID = 2;
export const artistStore = { export const artistStore = {
stub, stub,
@ -21,11 +21,11 @@ export const artistStore = {
* *
* @param {Array.<Object>} artists The array of artists we got from the server. * @param {Array.<Object>} artists The array of artists we got from the server.
*/ */
init (artists) { init(artists) {
this.all = artists this.all = artists;
// Traverse through artists array to get the cover and number of songs for each. // Traverse through artists array to get the cover and number of songs for each.
each(this.all, artist => this.setupArtist(artist)) each(this.all, artist => this.setupArtist(artist));
}, },
/** /**
@ -33,15 +33,15 @@ export const artistStore = {
* *
* @param {Object} artist * @param {Object} artist
*/ */
setupArtist (artist) { setupArtist(artist) {
Vue.set(artist, 'playCount', 0) Vue.set(artist, "playCount", 0);
Vue.set(artist, 'info', null) Vue.set(artist, "info", null);
Vue.set(artist, 'albums', []) Vue.set(artist, "albums", []);
Vue.set(artist, 'songs', []) Vue.set(artist, "songs", []);
this.cache[artist.id] = artist this.cache[artist.id] = artist;
return artist return artist;
}, },
/** /**
@ -49,8 +49,8 @@ export const artistStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all() {
return this.state.artists return this.state.artists;
}, },
/** /**
@ -58,8 +58,8 @@ export const artistStore = {
* *
* @param {Array.<Object>} value * @param {Array.<Object>} value
*/ */
set all (value) { set all(value) {
this.state.artists = value this.state.artists = value;
}, },
/** /**
@ -67,8 +67,8 @@ export const artistStore = {
* *
* @param {Number} id * @param {Number} id
*/ */
byId (id) { byId(id) {
return this.cache[id] return this.cache[id];
}, },
/** /**
@ -76,31 +76,35 @@ export const artistStore = {
* *
* @param {Array.<Object>|Object} artists * @param {Array.<Object>|Object} artists
*/ */
add (artists) { add(artists) {
artists = [].concat(artists) artists = [].concat(artists);
each(artists, artist => { each(artists, artist => {
this.setupArtist(artist) this.setupArtist(artist);
artist.playCount = reduce(artist.songs, (count, song) => count + song.playCount, 0) artist.playCount = reduce(
}) artist.songs,
(count, song) => count + song.playCount,
0
);
});
this.all = union(this.all, artists) this.all = union(this.all, artists);
}, },
purify () { purify() {
this.compact() this.compact();
}, },
/** /**
* Remove empty artists from the store. * Remove empty artists from the store.
*/ */
compact () { compact() {
const emptyArtists = filter(this.all, artist => artist.songs.length === 0) const emptyArtists = filter(this.all, artist => artist.songs.length === 0);
if (!emptyArtists.length) { if (!emptyArtists.length) {
return return;
} }
this.all = difference(this.all, emptyArtists) this.all = difference(this.all, emptyArtists);
each(emptyArtists, artist => delete this.cache[artist.id]) each(emptyArtists, artist => delete this.cache[artist.id]);
}, },
/** /**
@ -110,8 +114,8 @@ export const artistStore = {
* *
* @return {Boolean} * @return {Boolean}
*/ */
isVariousArtists (artist) { isVariousArtists(artist) {
return artist.id === VARIOUS_ARTISTS_ID return artist.id === VARIOUS_ARTISTS_ID;
}, },
/** /**
@ -121,8 +125,8 @@ export const artistStore = {
* *
* @return {Boolean} * @return {Boolean}
*/ */
isUnknownArtist (artist) { isUnknownArtist(artist) {
return artist.id === UNKNOWN_ARTIST_ID return artist.id === UNKNOWN_ARTIST_ID;
}, },
/** /**
@ -132,8 +136,8 @@ export const artistStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
getSongsByArtist (artist) { getSongsByArtist(artist) {
return artist.songs return artist.songs;
}, },
/** /**
@ -143,15 +147,17 @@ export const artistStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
getMostPlayed (n = 6) { getMostPlayed(n = 6) {
// Only non-unknown artists with actually play count are applicable. // Only non-unknown artists with actually play count are applicable.
// Also, "Various Artists" doesn't count. // Also, "Various Artists" doesn't count.
const applicable = filter(this.all, artist => { const applicable = filter(this.all, artist => {
return artist.playCount && return (
artist.playCount &&
!this.isUnknownArtist(artist) && !this.isUnknownArtist(artist) &&
!this.isVariousArtists(artist) !this.isVariousArtists(artist)
}) );
});
return take(orderBy(applicable, 'playCount', 'desc'), n) return take(orderBy(applicable, "playCount", "desc"), n);
} }
} };

View file

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

View file

@ -1,14 +1,14 @@
import { each, map, difference, union } from 'lodash' import { each, map, difference, union } from "lodash";
import NProgress from 'nprogress' import NProgress from "nprogress";
import { http } from '@/services' import { http } from "@/services";
import { alerts, pluralize } from '@/utils' import { alerts, pluralize } from "@/utils";
export const favoriteStore = { export const favoriteStore = {
state: { state: {
songs: [], songs: [],
length: 0, length: 0,
fmtLength: '' fmtLength: ""
}, },
/** /**
@ -16,8 +16,8 @@ export const favoriteStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all() {
return this.state.songs return this.state.songs;
}, },
/** /**
@ -25,8 +25,8 @@ export const favoriteStore = {
* *
* @param {Array.<Object>} value * @param {Array.<Object>} value
*/ */
set all (value) { set all(value) {
this.state.songs = value this.state.songs = value;
}, },
/** /**
@ -35,20 +35,25 @@ export const favoriteStore = {
* *
* @param {Object} song * @param {Object} song
*/ */
toggleOne (song) { toggleOne(song) {
// Don't wait for the HTTP response to update the status, just toggle right away. // Don't wait for the HTTP response to update the status, just toggle right away.
// This may cause a minor problem if the request fails somehow, but do we care? // This may cause a minor problem if the request fails somehow, but do we care?
song.liked = !song.liked song.liked = !song.liked;
song.liked ? this.add(song) : this.remove(song) song.liked ? this.add(song) : this.remove(song);
NProgress.start() NProgress.start();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('interaction/like', { song: song.id }, ({ data }) => { http.post(
// We don't really need to notify just for one song. "interaction/like",
resolve(data) { song: song.id },
}, error => reject(error)) ({ data }) => {
}) // We don't really need to notify just for one song.
resolve(data);
},
error => reject(error)
);
});
}, },
/** /**
@ -56,8 +61,8 @@ export const favoriteStore = {
* *
* @param {Array.<Object>|Object} songs * @param {Array.<Object>|Object} songs
*/ */
add (songs) { add(songs) {
this.all = union(this.all, [].concat(songs)) this.all = union(this.all, [].concat(songs));
}, },
/** /**
@ -65,15 +70,15 @@ export const favoriteStore = {
* *
* @param {Array.<Object>|Object} songs * @param {Array.<Object>|Object} songs
*/ */
remove (songs) { remove(songs) {
this.all = difference(this.all, [].concat(songs)) this.all = difference(this.all, [].concat(songs));
}, },
/** /**
* Remove all favorites. * Remove all favorites.
*/ */
clear () { clear() {
this.all = [] this.all = [];
}, },
/** /**
@ -81,22 +86,29 @@ export const favoriteStore = {
* *
* @param {Array.<Object>} songs * @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. // Don't wait for the HTTP response to update the status, just set them to Liked right away.
// This may cause a minor problem if the request fails somehow, but do we care? // This may cause a minor problem if the request fails somehow, but do we care?
each(songs, song => { each(songs, song => {
song.liked = true song.liked = true;
}) });
this.add(songs) this.add(songs);
NProgress.start() NProgress.start();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('interaction/batch/like', { songs: map(songs, 'id') }, ({ data }) => { http.post(
alerts.success(`Added ${pluralize(songs.length, 'song')} into Favorites.`) "interaction/batch/like",
resolve(data) { songs: map(songs, "id") },
}, error => reject(error)) ({ data }) => {
}) alerts.success(
`Added ${pluralize(songs.length, "song")} into Favorites.`
);
resolve(data);
},
error => reject(error)
);
});
}, },
/** /**
@ -104,19 +116,26 @@ export const favoriteStore = {
* *
* @param {Array.<Object>} songs * @param {Array.<Object>} songs
*/ */
unlike (songs) { unlike(songs) {
each(songs, song => { each(songs, song => {
song.liked = false song.liked = false;
}) });
this.remove(songs) this.remove(songs);
NProgress.start() NProgress.start();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('interaction/batch/unlike', { songs: map(songs, 'id') }, ({ data }) => { http.post(
alerts.success(`Removed ${pluralize(songs.length, 'song')} from Favorites.`) "interaction/batch/unlike",
resolve(data) { songs: map(songs, "id") },
}, error => reject(error)) ({ data }) => {
}) alerts.success(
`Removed ${pluralize(songs.length, "song")} from Favorites.`
);
resolve(data);
},
error => reject(error)
);
});
} }
} };

View file

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

View file

@ -1,10 +1,10 @@
import { each, find, map, difference, union } from 'lodash' import { each, find, map, difference, union } from "lodash";
import NProgress from 'nprogress' import NProgress from "nprogress";
import stub from '@/stubs/playlist' import stub from "@/stubs/playlist";
import { http } from '@/services' import { http } from "@/services";
import { alerts, pluralize } from '@/utils' import { alerts, pluralize } from "@/utils";
import { songStore } from '.' import { songStore } from ".";
export const playlistStore = { export const playlistStore = {
stub, stub,
@ -13,9 +13,9 @@ export const playlistStore = {
playlists: [] playlists: []
}, },
init (playlists) { init(playlists) {
this.all = playlists this.all = playlists;
each(this.all, this.objectifySongs) each(this.all, this.objectifySongs);
}, },
/** /**
@ -23,8 +23,8 @@ export const playlistStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all() {
return this.state.playlists return this.state.playlists;
}, },
/** /**
@ -32,8 +32,8 @@ export const playlistStore = {
* *
* @param {Array.<Object>} value * @param {Array.<Object>} value
*/ */
set all (value) { set all(value) {
this.state.playlists = value this.state.playlists = value;
}, },
/** /**
@ -43,8 +43,8 @@ export const playlistStore = {
* *
* @return {Object} * @return {Object}
*/ */
byId (id) { byId(id) {
return find(this.all, { id }) return find(this.all, { id });
}, },
/** /**
@ -53,8 +53,8 @@ export const playlistStore = {
* *
* @param {Object} playlist * @param {Object} playlist
*/ */
objectifySongs (playlist) { objectifySongs(playlist) {
playlist.songs = songStore.byIds(playlist.songs) playlist.songs = songStore.byIds(playlist.songs);
}, },
/** /**
@ -64,8 +64,8 @@ export const playlistStore = {
* *
* return {Array.<Object>} * return {Array.<Object>}
*/ */
getSongs (playlist) { getSongs(playlist) {
return playlist.songs return playlist.songs;
}, },
/** /**
@ -73,8 +73,8 @@ export const playlistStore = {
* *
* @param {Array.<Object>|Object} playlists * @param {Array.<Object>|Object} playlists
*/ */
add (playlists) { add(playlists) {
this.all = union(this.all, [].concat(playlists)) this.all = union(this.all, [].concat(playlists));
}, },
/** /**
@ -82,8 +82,8 @@ export const playlistStore = {
* *
* @param {Array.<Object>|Object} playlist * @param {Array.<Object>|Object} playlist
*/ */
remove (playlists) { remove(playlists) {
this.all = difference(this.all, [].concat(playlists)) this.all = difference(this.all, [].concat(playlists));
}, },
/** /**
@ -92,23 +92,28 @@ export const playlistStore = {
* @param {String} name Name of the playlist * @param {String} name Name of the playlist
* @param {Array.<Object>} songs An array of song objects * @param {Array.<Object>} songs An array of song objects
*/ */
store (name, songs = []) { store(name, songs = []) {
if (songs.length) { if (songs.length) {
// Extract the IDs from the song objects. // Extract the IDs from the song objects.
songs = map(songs, 'id') songs = map(songs, "id");
} }
NProgress.start() NProgress.start();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('playlist', { name, songs }, ({ data: playlist }) => { http.post(
playlist.songs = songs "playlist",
this.objectifySongs(playlist) { name, songs },
this.add(playlist) ({ data: playlist }) => {
alerts.success(`Created playlist &quot;${playlist.name}&quot;.`) playlist.songs = songs;
resolve(playlist) this.objectifySongs(playlist);
}, error => reject(error)) this.add(playlist);
}) alerts.success(`Created playlist &quot;${playlist.name}&quot;.`);
resolve(playlist);
},
error => reject(error)
);
});
}, },
/** /**
@ -116,16 +121,21 @@ export const playlistStore = {
* *
* @param {Object} playlist * @param {Object} playlist
*/ */
delete (playlist) { delete(playlist) {
NProgress.start() NProgress.start();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.delete(`playlist/${playlist.id}`, {}, ({ data }) => { http.delete(
this.remove(playlist) `playlist/${playlist.id}`,
alerts.success(`Deleted playlist &quot;${playlist.name}&quot;.`) {},
resolve(data) ({ data }) => {
}, error => reject(error)) this.remove(playlist);
}) alerts.success(`Deleted playlist &quot;${playlist.name}&quot;.`);
resolve(data);
},
error => reject(error)
);
});
}, },
/** /**
@ -134,23 +144,33 @@ export const playlistStore = {
* @param {Object} playlist * @param {Object} playlist
* @param {Array.<Object>} songs * @param {Array.<Object>} songs
*/ */
addSongs (playlist, songs) { addSongs(playlist, songs) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const count = playlist.songs.length const count = playlist.songs.length;
playlist.songs = union(playlist.songs, songs) playlist.songs = union(playlist.songs, songs);
if (count === playlist.songs.length) { if (count === playlist.songs.length) {
resolve(playlist) resolve(playlist);
return return;
} }
NProgress.start() NProgress.start();
http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') }, () => { http.put(
alerts.success(`Added ${pluralize(songs.length, 'song')} into &quot;${playlist.name}&quot;.`) `playlist/${playlist.id}/sync`,
resolve(playlist) { songs: map(playlist.songs, "id") },
}, error => reject(error)) () => {
}) alerts.success(
`Added ${pluralize(
songs.length,
"song"
)} into &quot;${playlist.name}&quot;.`
);
resolve(playlist);
},
error => reject(error)
);
});
}, },
/** /**
@ -159,17 +179,27 @@ export const playlistStore = {
* @param {Object} playlist * @param {Object} playlist
* @param {Array.<Object>} songs * @param {Array.<Object>} songs
*/ */
removeSongs (playlist, songs) { removeSongs(playlist, songs) {
NProgress.start() NProgress.start();
playlist.songs = difference(playlist.songs, songs) playlist.songs = difference(playlist.songs, songs);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') }, () => { http.put(
alerts.success(`Removed ${pluralize(songs.length, 'song')} from &quot;${playlist.name}&quot;.`) `playlist/${playlist.id}/sync`,
resolve(playlist) { songs: map(playlist.songs, "id") },
}, error => reject(error)) () => {
}) alerts.success(
`Removed ${pluralize(
songs.length,
"song"
)} from &quot;${playlist.name}&quot;.`
);
resolve(playlist);
},
error => reject(error)
);
});
}, },
/** /**
@ -177,8 +207,8 @@ export const playlistStore = {
* *
* @param {Object} playlist * @param {Object} playlist
*/ */
update (playlist) { update(playlist) {
NProgress.start() NProgress.start();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.put( http.put(
@ -186,7 +216,7 @@ export const playlistStore = {
{ name: playlist.name }, { name: playlist.name },
() => resolve(playlist), () => resolve(playlist),
error => reject(error) error => reject(error)
) );
}) });
} }
} };

View file

@ -1,15 +1,15 @@
import { extend, has, each } from 'lodash' import { extend, has, each } from "lodash";
import { userStore } from '.' import { userStore } from ".";
import { ls } from '@/services' import { ls } from "@/services";
export const preferenceStore = { export const preferenceStore = {
storeKey: '', storeKey: "",
state: { state: {
volume: 7, volume: 7,
notify: true, notify: true,
repeatMode: 'NO_REPEAT', repeatMode: "NO_REPEAT",
showExtraPanel: true, showExtraPanel: true,
confirmClosing: false, confirmClosing: false,
equalizer: { equalizer: {
@ -27,39 +27,39 @@ export const preferenceStore = {
* *
* @param {Object} user The user whose preferences we are managing. * @param {Object} user The user whose preferences we are managing.
*/ */
init (user = null) { init(user = null) {
if (!user) { if (!user) {
user = userStore.current user = userStore.current;
} }
this.storeKey = `preferences_${user.id}` this.storeKey = `preferences_${user.id}`;
extend(this.state, ls.get(this.storeKey, this.state)) extend(this.state, ls.get(this.storeKey, this.state));
this.setupProxy() this.setupProxy();
}, },
/** /**
* Proxy the state properties, so that each can be directly accessed using the key. * Proxy the state properties, so that each can be directly accessed using the key.
*/ */
setupProxy () { setupProxy() {
each(Object.keys(this.state), key => { each(Object.keys(this.state), key => {
Object.defineProperty(this, key, { Object.defineProperty(this, key, {
get: () => this.get(key), get: () => this.get(key),
set: value => this.set(key, value), set: value => this.set(key, value),
configurable: true configurable: true
}) });
}) });
}, },
set (key, val) { set(key, val) {
this.state[key] = val this.state[key] = val;
this.save() this.save();
}, },
get (key) { get(key) {
return has(this.state, key) ? this.state[key] : null return has(this.state, key) ? this.state[key] : null;
}, },
save () { save() {
ls.set(this.storeKey, this.state) ls.set(this.storeKey, this.state);
} }
} };

View file

@ -1,4 +1,14 @@
import { head, last, each, includes, union, difference, map, shuffle as _shuffle, first } from 'lodash' import {
head,
last,
each,
includes,
union,
difference,
map,
shuffle as _shuffle,
first
} from "lodash";
export const queueStore = { export const queueStore = {
state: { state: {
@ -6,7 +16,7 @@ export const queueStore = {
current: null current: null
}, },
init () { init() {
// We don't have anything to do here yet. // We don't have anything to do here yet.
// How about another song then? // How about another song then?
// //
@ -36,8 +46,8 @@ export const queueStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all() {
return this.state.songs return this.state.songs;
}, },
/** /**
@ -45,8 +55,8 @@ export const queueStore = {
* *
* @param {Array.<Object>} * @param {Array.<Object>}
*/ */
set all (songs) { set all(songs) {
this.state.songs = songs this.state.songs = songs;
}, },
/** /**
@ -54,8 +64,8 @@ export const queueStore = {
* *
* @return {?Object} * @return {?Object}
*/ */
get first () { get first() {
return head(this.all) return head(this.all);
}, },
/** /**
@ -63,8 +73,8 @@ export const queueStore = {
* *
* @return {?Object} * @return {?Object}
*/ */
get last () { get last() {
return last(this.all) return last(this.all);
}, },
/** /**
@ -74,8 +84,8 @@ export const queueStore = {
* *
* @return {Boolean} * @return {Boolean}
*/ */
contains (song) { contains(song) {
return includes(this.all, song) return includes(this.all, song);
}, },
/** /**
@ -86,13 +96,13 @@ export const queueStore = {
* @param {Boolean} replace Whether to replace the current queue * @param {Boolean} replace Whether to replace the current queue
* @param {Boolean} toTop Whether to prepend or append to the queue * @param {Boolean} toTop Whether to prepend or append to the queue
*/ */
queue (songs, replace = false, toTop = false) { queue(songs, replace = false, toTop = false) {
songs = [].concat(songs) songs = [].concat(songs);
if (replace) { if (replace) {
this.all = songs this.all = songs;
} else { } else {
this.all = toTop ? union(songs, this.all) : union(this.all, songs) this.all = toTop ? union(songs, this.all) : union(this.all, songs);
} }
}, },
@ -101,18 +111,18 @@ export const queueStore = {
* *
* @param {Array.<Object>|Object} songs * @param {Array.<Object>|Object} songs
*/ */
queueAfterCurrent (songs) { queueAfterCurrent(songs) {
songs = [].concat(songs) songs = [].concat(songs);
if (!this.current || !this.all.length) { if (!this.current || !this.all.length) {
return this.queue(songs) return this.queue(songs);
} }
// First we unqueue the songs to make sure there are no duplicates. // First we unqueue the songs to make sure there are no duplicates.
this.unqueue(songs) this.unqueue(songs);
const head = this.all.splice(0, this.indexOf(this.current) + 1) const head = this.all.splice(0, this.indexOf(this.current) + 1);
this.all = head.concat(songs, this.all) this.all = head.concat(songs, this.all);
}, },
/** /**
@ -120,8 +130,8 @@ export const queueStore = {
* *
* @param {Object|String|Array.<Object>} songs The song(s) to unqueue * @param {Object|String|Array.<Object>} songs The song(s) to unqueue
*/ */
unqueue (songs) { unqueue(songs) {
this.all = difference(this.all, [].concat(songs)) this.all = difference(this.all, [].concat(songs));
}, },
/** /**
@ -130,20 +140,20 @@ export const queueStore = {
* @param {Array.<Object>} songs Songs to move * @param {Array.<Object>} songs Songs to move
* @param {Object} target The target song object * @param {Object} target The target song object
*/ */
move (songs, target) { move(songs, target) {
const $targetIndex = this.indexOf(target) const $targetIndex = this.indexOf(target);
each(songs, song => { each(songs, song => {
this.all.splice(this.indexOf(song), 1) this.all.splice(this.indexOf(song), 1);
this.all.splice($targetIndex, 0, song) this.all.splice($targetIndex, 0, song);
}) });
}, },
/** /**
* Clear the current queue. * Clear the current queue.
*/ */
clear () { clear() {
this.all = [] this.all = [];
}, },
/** /**
@ -153,8 +163,8 @@ export const queueStore = {
* *
* @return {?Integer} * @return {?Integer}
*/ */
indexOf (song) { indexOf(song) {
return this.all.indexOf(song) return this.all.indexOf(song);
}, },
/** /**
@ -162,14 +172,14 @@ export const queueStore = {
* *
* @return {?Object} * @return {?Object}
*/ */
get next () { get next() {
if (!this.current) { if (!this.current) {
return first(this.all) return first(this.all);
} }
const index = map(this.all, 'id').indexOf(this.current.id) + 1 const index = map(this.all, "id").indexOf(this.current.id) + 1;
return index >= this.all.length ? null : this.all[index] return index >= this.all.length ? null : this.all[index];
}, },
/** /**
@ -177,14 +187,14 @@ export const queueStore = {
* *
* @return {?Object} * @return {?Object}
*/ */
get previous () { get previous() {
if (!this.current) { if (!this.current) {
return last(this.all) return last(this.all);
} }
const index = map(this.all, 'id').indexOf(this.current.id) - 1 const index = map(this.all, "id").indexOf(this.current.id) - 1;
return index < 0 ? null : this.all[index] return index < 0 ? null : this.all[index];
}, },
/** /**
@ -192,8 +202,8 @@ export const queueStore = {
* *
* @return {Object} * @return {Object}
*/ */
get current () { get current() {
return this.state.current return this.state.current;
}, },
/** /**
@ -203,9 +213,9 @@ export const queueStore = {
* *
* @return {Object} The queued song. * @return {Object} The queued song.
*/ */
set current (song) { set current(song) {
this.state.current = song this.state.current = song;
return this.state.current return this.state.current;
}, },
/** /**
@ -213,8 +223,8 @@ export const queueStore = {
* *
* @return {Array.<Object>} The shuffled array of song objects * @return {Array.<Object>} The shuffled array of song objects
*/ */
shuffle () { shuffle() {
this.all = _shuffle(this.all) this.all = _shuffle(this.all);
return this.all return this.all;
} }
} };

View file

@ -1,6 +1,6 @@
import { http } from '@/services' import { http } from "@/services";
import { alerts } from '@/utils' import { alerts } from "@/utils";
import stub from '@/stubs/settings' import stub from "@/stubs/settings";
export const settingStore = { export const settingStore = {
stub, stub,
@ -9,20 +9,25 @@ export const settingStore = {
settings: [] settings: []
}, },
init (settings) { init(settings) {
this.state.settings = settings this.state.settings = settings;
}, },
get all () { get all() {
return this.state.settings return this.state.settings;
}, },
update () { update() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('settings', this.all, ({ data }) => { http.post(
alerts.success('Settings saved.') "settings",
resolve(data) this.all,
}, error => reject(error)) ({ data }) => {
}) alerts.success("Settings saved.");
resolve(data);
},
error => reject(error)
);
});
} }
} };

View file

@ -1,8 +1,17 @@
import { assign } from 'lodash' import { assign } from "lodash";
import isMobile from 'ismobilejs' import isMobile from "ismobilejs";
import { http } from '@/services' import { http } from "@/services";
import { userStore, preferenceStore, artistStore, albumStore, songStore, playlistStore, queueStore, settingStore } from '.' import {
userStore,
preferenceStore,
artistStore,
albumStore,
songStore,
playlistStore,
queueStore,
settingStore
} from ".";
export const sharedStore = { export const sharedStore = {
state: { state: {
@ -20,42 +29,46 @@ export const sharedStore = {
useYouTube: false, useYouTube: false,
useiTunes: false, useiTunes: false,
allowDownload: false, allowDownload: false,
currentVersion: '', currentVersion: "",
latestVersion: '', latestVersion: "",
cdnUrl: '', cdnUrl: "",
originalMediaPath: '' originalMediaPath: ""
}, },
init () { init() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.get('data', ({ data }) => { http.get(
assign(this.state, data) "data",
// Don't allow downloading on mobile devices ({ data }) => {
this.state.allowDownload = this.state.allowDownload && !isMobile.any assign(this.state, data);
// Don't allow downloading on mobile devices
this.state.allowDownload = this.state.allowDownload && !isMobile.any;
// Always disable YouTube integration on mobile. // Always disable YouTube integration on mobile.
this.state.useYouTube = this.state.useYouTube && !isMobile.phone this.state.useYouTube = this.state.useYouTube && !isMobile.phone;
// If this is a new user, initialize his preferences to be an empty object. // If this is a new user, initialize his preferences to be an empty object.
if (!this.state.currentUser.preferences) { if (!this.state.currentUser.preferences) {
this.state.currentUser.preferences = {} this.state.currentUser.preferences = {};
} }
userStore.init(this.state.users, this.state.currentUser) userStore.init(this.state.users, this.state.currentUser);
preferenceStore.init(this.state.preferences) preferenceStore.init(this.state.preferences);
artistStore.init(this.state.artists) artistStore.init(this.state.artists);
albumStore.init(this.state.albums) albumStore.init(this.state.albums);
songStore.init(this.state.songs) songStore.init(this.state.songs);
songStore.initInteractions(this.state.interactions) songStore.initInteractions(this.state.interactions);
playlistStore.init(this.state.playlists) playlistStore.init(this.state.playlists);
queueStore.init() queueStore.init();
settingStore.init(this.state.settings) settingStore.init(this.state.settings);
// Keep a copy of the media path. We'll need this to properly warn the user later. // Keep a copy of the media path. We'll need this to properly warn the user later.
this.state.originalMediaPath = this.state.settings.media_path this.state.originalMediaPath = this.state.settings.media_path;
resolve(this.state) resolve(this.state);
}, error => reject(error)) },
}) error => reject(error)
);
});
} }
} };

View file

@ -1,12 +1,28 @@
import Vue from 'vue' import Vue from "vue";
import slugify from 'slugify' import slugify from "slugify";
import { assign, without, map, take, remove, orderBy, each, unionBy, compact } from 'lodash' import {
import isMobile from 'ismobilejs' assign,
without,
map,
take,
remove,
orderBy,
each,
unionBy,
compact
} from "lodash";
import isMobile from "ismobilejs";
import { secondsToHis, alerts, pluralize } from '@/utils' import { secondsToHis, alerts, pluralize } from "@/utils";
import { http, ls } from '@/services' import { http, ls } from "@/services";
import { sharedStore, favoriteStore, albumStore, artistStore, preferenceStore } from '.' import {
import stub from '@/stubs/song' sharedStore,
favoriteStore,
albumStore,
artistStore,
preferenceStore
} from ".";
import stub from "@/stubs/song";
export const songStore = { export const songStore = {
stub, stub,
@ -34,39 +50,39 @@ export const songStore = {
* *
* @param {Array.<Object>} songs The array of song objects * @param {Array.<Object>} songs The array of song objects
*/ */
init (songs) { init(songs) {
this.all = songs this.all = songs;
each(this.all, song => this.setupSong(song)) each(this.all, song => this.setupSong(song));
this.state.recentlyPlayed = this.gatherRecentlyPlayedFromLocalStorage() this.state.recentlyPlayed = this.gatherRecentlyPlayedFromLocalStorage();
}, },
setupSong (song) { setupSong(song) {
song.fmtLength = secondsToHis(song.length) song.fmtLength = secondsToHis(song.length);
const album = albumStore.byId(song.album_id) const album = albumStore.byId(song.album_id);
const artist = artistStore.byId(song.artist_id) const artist = artistStore.byId(song.artist_id);
// Manually set these additional properties to be reactive // Manually set these additional properties to be reactive
Vue.set(song, 'playCount', song.playCount || 0) Vue.set(song, "playCount", song.playCount || 0);
Vue.set(song, 'album', album) Vue.set(song, "album", album);
Vue.set(song, 'artist', artist) Vue.set(song, "artist", artist);
Vue.set(song, 'liked', song.liked || false) Vue.set(song, "liked", song.liked || false);
Vue.set(song, 'lyrics', song.lyrics || null) Vue.set(song, "lyrics", song.lyrics || null);
Vue.set(song, 'playbackState', song.playbackState || 'stopped') Vue.set(song, "playbackState", song.playbackState || "stopped");
artist.songs = unionBy(artist.songs || [], [song], 'id') artist.songs = unionBy(artist.songs || [], [song], "id");
album.songs = unionBy(album.songs || [], [song], 'id') album.songs = unionBy(album.songs || [], [song], "id");
// now if the song is part of a compilation album, the album must be added // now if the song is part of a compilation album, the album must be added
// into its artist as well // into its artist as well
if (album.is_compilation) { if (album.is_compilation) {
artist.albums = unionBy(artist.albums, [album], 'id') artist.albums = unionBy(artist.albums, [album], "id");
} }
// Cache the song, so that byId() is faster // Cache the song, so that byId() is faster
this.cache[song.id] = song this.cache[song.id] = song;
return song return song;
}, },
/** /**
@ -74,23 +90,23 @@ export const songStore = {
* *
* @param {Array.<Object>} interactions The array of interactions of the current user * @param {Array.<Object>} interactions The array of interactions of the current user
*/ */
initInteractions (interactions) { initInteractions(interactions) {
favoriteStore.clear() favoriteStore.clear();
each(interactions, interaction => { each(interactions, interaction => {
const song = this.byId(interaction.song_id) const song = this.byId(interaction.song_id);
if (!song) { if (!song) {
return return;
} }
song.liked = interaction.liked song.liked = interaction.liked;
song.playCount = interaction.play_count song.playCount = interaction.play_count;
song.album.playCount += song.playCount song.album.playCount += song.playCount;
song.artist.playCount += song.playCount song.artist.playCount += song.playCount;
song.liked && favoriteStore.add(song) song.liked && favoriteStore.add(song);
}) });
}, },
/** /**
@ -101,10 +117,10 @@ export const songStore = {
* *
* @return {Float|String} * @return {Float|String}
*/ */
getLength (songs, toHis) { getLength(songs, toHis) {
const duration = songs.reduce((length, song) => length + song.length, 0) const duration = songs.reduce((length, song) => length + song.length, 0);
return toHis ? secondsToHis(duration) : duration return toHis ? secondsToHis(duration) : duration;
}, },
/** /**
@ -112,8 +128,8 @@ export const songStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all() {
return this.state.songs return this.state.songs;
}, },
/** /**
@ -121,8 +137,8 @@ export const songStore = {
* *
* @param {Array.<Object>} value * @param {Array.<Object>} value
*/ */
set all (value) { set all(value) {
this.state.songs = value this.state.songs = value;
}, },
/** /**
@ -132,8 +148,8 @@ export const songStore = {
* *
* @return {Object} * @return {Object}
*/ */
byId (id) { byId(id) {
return this.cache[id] return this.cache[id];
}, },
/** /**
@ -143,8 +159,8 @@ export const songStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
byIds (ids) { byIds(ids) {
return ids.map(id => this.byId(id)) return ids.map(id => this.byId(id));
}, },
/** /**
@ -156,16 +172,16 @@ export const songStore = {
* *
* @return {Object|false} * @return {Object|false}
*/ */
guess (title, album) { guess(title, album) {
title = slugify(title.toLowerCase()) title = slugify(title.toLowerCase());
let found = false let found = false;
each(album.songs, song => { each(album.songs, song => {
if (slugify(song.title.toLowerCase()) === title) { if (slugify(song.title.toLowerCase()) === title) {
found = song found = song;
} }
}) });
return found return found;
}, },
/** /**
@ -173,19 +189,24 @@ export const songStore = {
* *
* @param {Object} song * @param {Object} song
*/ */
registerPlay (song) { registerPlay(song) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const oldCount = song.playCount const oldCount = song.playCount;
http.post('interaction/play', { song: song.id }, ({ data }) => { http.post(
// Use the data from the server to make sure we don't miss a play from another device. "interaction/play",
song.playCount = data.play_count { song: song.id },
song.album.playCount += song.playCount - oldCount ({ data }) => {
song.artist.playCount += song.playCount - oldCount // 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;
resolve(data) resolve(data);
}, error => reject(error)) },
}) error => reject(error)
);
});
}, },
/** /**
@ -193,15 +214,15 @@ export const songStore = {
* *
* @param {Object} song * @param {Object} song
*/ */
addRecentlyPlayed (song) { addRecentlyPlayed(song) {
remove(this.state.recentlyPlayed, s => s.id === song.id) remove(this.state.recentlyPlayed, s => s.id === song.id);
// Then we prepend the song into the list. // Then we prepend the song into the list.
this.state.recentlyPlayed.unshift(song) this.state.recentlyPlayed.unshift(song);
// Only take first 7 songs // Only take first 7 songs
this.state.recentlyPlayed.splice(7) this.state.recentlyPlayed.splice(7);
// Save to local storage as well // Save to local storage as well
preferenceStore.set('recent-songs', map(this.state.recentlyPlayed, 'id')) preferenceStore.set("recent-songs", map(this.state.recentlyPlayed, "id"));
}, },
/** /**
@ -209,12 +230,17 @@ export const songStore = {
* *
* @param {Object} song * @param {Object} song
*/ */
scrobble (song) { scrobble(song) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post(`${song.id}/scrobble/${song.playStartTime}`, {}, ({ data }) => { http.post(
resolve(data) `${song.id}/scrobble/${song.playStartTime}`,
}, error => reject(error)) {},
}) ({ data }) => {
resolve(data);
},
error => reject(error)
);
});
}, },
/** /**
@ -223,41 +249,58 @@ export const songStore = {
* @param {Array.<Object>} songs An array of song * @param {Array.<Object>} songs An array of song
* @param {Object} data * @param {Object} data
*/ */
update (songs, data) { update(songs, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.put('songs', { http.put(
data, "songs",
songs: map(songs, 'id') {
}, ({ data: { songs, artists, albums }}) => { data,
// Add the artist and album into stores if they're new songs: map(songs, "id")
each(artists, artist => !artistStore.byId(artist.id) && artistStore.add(artist)) },
each(albums, album => !albumStore.byId(album.id) && albumStore.add(album)) ({ data: { songs, artists, albums } }) => {
// Add the artist and album into stores if they're new
each(
artists,
artist => !artistStore.byId(artist.id) && artistStore.add(artist)
);
each(
albums,
album => !albumStore.byId(album.id) && albumStore.add(album)
);
each(songs, song => { each(songs, song => {
const originalSong = this.byId(song.id) const originalSong = this.byId(song.id);
if (originalSong.album_id !== song.album_id) { if (originalSong.album_id !== song.album_id) {
// album has been changed. Remove the song from its old album. // album has been changed. Remove the song from its old album.
originalSong.album.songs = without(originalSong.album.songs, originalSong) originalSong.album.songs = without(
} originalSong.album.songs,
originalSong
);
}
if (originalSong.artist_id !== song.artist_id) { if (originalSong.artist_id !== song.artist_id) {
// artist has been changed. Remove the song from its old artist // artist has been changed. Remove the song from its old artist
originalSong.artist.songs = without(originalSong.artist.songs, originalSong) originalSong.artist.songs = without(
} originalSong.artist.songs,
originalSong
);
}
assign(originalSong, song) assign(originalSong, song);
// re-setup the song // re-setup the song
this.setupSong(originalSong) this.setupSong(originalSong);
}) });
artistStore.compact() artistStore.compact();
albumStore.compact() albumStore.compact();
alerts.success(`Updated ${pluralize(songs.length, 'song')}.`) alerts.success(`Updated ${pluralize(songs.length, "song")}.`);
resolve(songs) resolve(songs);
}, error => reject(error)) },
}) error => reject(error)
);
});
}, },
/** /**
@ -267,11 +310,14 @@ export const songStore = {
* *
* @return {string} The source URL, with JWT token appended. * @return {string} The source URL, with JWT token appended.
*/ */
getSourceUrl (song) { getSourceUrl(song) {
if (isMobile.any && preferenceStore.transcodeOnMobile) { if (isMobile.any && preferenceStore.transcodeOnMobile) {
return `${sharedStore.state.cdnUrl}api/${song.id}/play/1/128?jwt-token=${ls.get('jwt-token')}` return `${sharedStore.state
.cdnUrl}api/${song.id}/play/1/128?jwt-token=${ls.get("jwt-token")}`;
} }
return `${sharedStore.state.cdnUrl}api/${song.id}/play?jwt-token=${ls.get('jwt-token')}` return `${sharedStore.state.cdnUrl}api/${song.id}/play?jwt-token=${ls.get(
"jwt-token"
)}`;
}, },
/** /**
@ -282,24 +328,24 @@ export const songStore = {
* *
* @return {string} * @return {string}
*/ */
getShareableUrl (song) { getShareableUrl(song) {
return `${window.location.origin}/#!/song/${song.id}` return `${window.location.origin}/#!/song/${song.id}`;
}, },
/** /**
* The recently played songs. * The recently played songs.
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get recentlyPlayed () { get recentlyPlayed() {
return this.state.recentlyPlayed return this.state.recentlyPlayed;
}, },
/** /**
* Gather the recently played songs from local storage. * Gather the recently played songs from local storage.
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
gatherRecentlyPlayedFromLocalStorage () { gatherRecentlyPlayedFromLocalStorage() {
return compact(this.byIds(preferenceStore.get('recent-songs') || [])) return compact(this.byIds(preferenceStore.get("recent-songs") || []));
}, },
/** /**
@ -309,13 +355,13 @@ export const songStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
getMostPlayed (n = 10) { getMostPlayed(n = 10) {
const songs = take(orderBy(this.all, 'playCount', 'desc'), n) const songs = take(orderBy(this.all, "playCount", "desc"), n);
// Remove those with playCount=0 // Remove those with playCount=0
remove(songs, song => !song.playCount) remove(songs, song => !song.playCount);
return songs return songs;
}, },
/** /**
@ -323,8 +369,8 @@ export const songStore = {
* @param {Number} n * @param {Number} n
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
getRecentlyAdded (n = 10) { getRecentlyAdded(n = 10) {
return take(orderBy(this.all, 'created_at', 'desc'), n) return take(orderBy(this.all, "created_at", "desc"), n);
}, },
/** /**
@ -332,7 +378,7 @@ export const songStore = {
* @param {Object} song * @param {Object} song
* @return {Object} * @return {Object}
*/ */
generateDataToBroadcast (song) { generateDataToBroadcast(song) {
return { return {
song: { song: {
id: song.id, id: song.id,
@ -347,6 +393,6 @@ export const songStore = {
name: song.artist.name name: song.artist.name
} }
} }
} };
} }
} };

View file

@ -1,11 +1,11 @@
import { each, find, without } from 'lodash' import { each, find, without } from "lodash";
import md5 from 'blueimp-md5' import md5 from "blueimp-md5";
import Vue from 'vue' import Vue from "vue";
import NProgress from 'nprogress' import NProgress from "nprogress";
import { http } from '@/services' import { http } from "@/services";
import { alerts } from '@/utils' import { alerts } from "@/utils";
import stub from '@/stubs/user' import stub from "@/stubs/user";
export const userStore = { export const userStore = {
stub, stub,
@ -21,15 +21,15 @@ export const userStore = {
* @param {Array.<Object>} users The users in the system. Empty array if current user is not an admin. * @param {Array.<Object>} users The users in the system. Empty array if current user is not an admin.
* @param {Object} currentUser The current user. * @param {Object} currentUser The current user.
*/ */
init (users, currentUser) { init(users, currentUser) {
this.all = users this.all = users;
this.current = currentUser this.current = currentUser;
// Set the avatar for each of the users… // Set the avatar for each of the users…
each(this.all, this.setAvatar) each(this.all, this.setAvatar);
// …and the current user as well. // …and the current user as well.
this.setAvatar() this.setAvatar();
}, },
/** /**
@ -37,8 +37,8 @@ export const userStore = {
* *
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all() {
return this.state.users return this.state.users;
}, },
/** /**
@ -46,8 +46,8 @@ export const userStore = {
* *
* @param {Array.<Object>} value * @param {Array.<Object>} value
*/ */
set all (value) { set all(value) {
this.state.users = value this.state.users = value;
}, },
/** /**
@ -57,8 +57,8 @@ export const userStore = {
* *
* @return {Object} * @return {Object}
*/ */
byId (id) { byId(id) {
return find(this.all, { id }) return find(this.all, { id });
}, },
/** /**
@ -66,8 +66,8 @@ export const userStore = {
* *
* @return {Object} * @return {Object}
*/ */
get current () { get current() {
return this.state.current return this.state.current;
}, },
/** /**
@ -77,9 +77,9 @@ export const userStore = {
* *
* @return {Object} * @return {Object}
*/ */
set current (user) { set current(user) {
this.state.current = user this.state.current = user;
return this.state.current return this.state.current;
}, },
/** /**
@ -87,12 +87,16 @@ export const userStore = {
* *
* @param {?Object} user The user. If null, the current user. * @param {?Object} user The user. If null, the current user.
*/ */
setAvatar (user = null) { setAvatar(user = null) {
if (!user) { if (!user) {
user = this.current user = this.current;
} }
Vue.set(user, 'avatar', `https://www.gravatar.com/avatar/${md5(user.email)}?s=256`) Vue.set(
user,
"avatar",
`https://www.gravatar.com/avatar/${md5(user.email)}?s=256`
);
}, },
/** /**
@ -101,36 +105,50 @@ export const userStore = {
* @param {String} email * @param {String} email
* @param {String} password * @param {String} password
*/ */
login (email, password) { login(email, password) {
NProgress.start() NProgress.start();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('me', { email, password }, ({ data }) => { http.post(
resolve(data) "me",
}, error => reject(error)) { email, password },
}) ({ data }) => {
resolve(data);
},
error => reject(error)
);
});
}, },
/** /**
* Log the current user out. * Log the current user out.
*/ */
logout () { logout() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.delete('me', {}, ({ data }) => { http.delete(
resolve(data) "me",
}, error => reject(error)) {},
}) ({ data }) => {
resolve(data);
},
error => reject(error)
);
});
}, },
/** /**
* Get the current user's profile. * Get the current user's profile.
*/ */
getProfile () { getProfile() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.get('me', ({ data }) => { http.get(
resolve(data) "me",
}, error => reject(error)) ({ data }) => {
}) resolve(data);
},
error => reject(error)
);
});
}, },
/** /**
@ -138,21 +156,25 @@ export const userStore = {
* *
* @param {string} password Can be an empty string if the user is not changing his password. * @param {string} password Can be an empty string if the user is not changing his password.
*/ */
updateProfile (password) { updateProfile(password) {
NProgress.start() NProgress.start();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.put('me', { http.put(
password, "me",
name: this.current.name, {
email: this.current.email password,
}, () => { name: this.current.name,
this.setAvatar() email: this.current.email
alerts.success('Profile updated.') },
resolve(this.current) () => {
}, this.setAvatar();
error => reject(error)) alerts.success("Profile updated.");
}) resolve(this.current);
},
error => reject(error)
);
});
}, },
/** /**
@ -162,17 +184,22 @@ export const userStore = {
* @param {string} email * @param {string} email
* @param {string} password * @param {string} password
*/ */
store (name, email, password) { store(name, email, password) {
NProgress.start() NProgress.start();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('user', { name, email, password }, ({ data: user }) => { http.post(
this.setAvatar(user) "user",
this.all.unshift(user) { name, email, password },
alerts.success(`New user &quot;${name}&quot; created.`) ({ data: user }) => {
resolve(user) this.setAvatar(user);
}, error => reject(error)) this.all.unshift(user);
}) alerts.success(`New user &quot;${name}&quot; created.`);
resolve(user);
},
error => reject(error)
);
});
}, },
/** /**
@ -183,17 +210,22 @@ export const userStore = {
* @param {String} email * @param {String} email
* @param {String} password * @param {String} password
*/ */
update (user, name, email, password) { update(user, name, email, password) {
NProgress.start() NProgress.start();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.put(`user/${user.id}`, { name, email, password }, () => { http.put(
this.setAvatar(user); `user/${user.id}`,
[user.name, user.email, user.password] = [name, email, ''] { name, email, password },
alerts.success('User profile updated.') () => {
resolve(user) this.setAvatar(user);
}, error => reject(error)) [user.name, user.email, user.password] = [name, email, ""];
}) alerts.success("User profile updated.");
resolve(user);
},
error => reject(error)
);
});
}, },
/** /**
@ -201,38 +233,43 @@ export const userStore = {
* *
* @param {Object} user * @param {Object} user
*/ */
destroy (user) { destroy(user) {
NProgress.start() NProgress.start();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.delete(`user/${user.id}`, {}, ({ data }) => { http.delete(
this.all = without(this.all, user) `user/${user.id}`,
alerts.success(`User &quot;${user.name}&quot; deleted.`) {},
({ data }) => {
this.all = without(this.all, user);
alerts.success(`User &quot;${user.name}&quot; deleted.`);
// Mama, just killed a man // Mama, just killed a man
// Put a gun against his head // Put a gun against his head
// Pulled my trigger, now he's dead // Pulled my trigger, now he's dead
// Mama, life had just begun // Mama, life had just begun
// But now I've gone and thrown it all away // But now I've gone and thrown it all away
// Mama, oooh // Mama, oooh
// Didn't mean to make you cry // Didn't mean to make you cry
// If I'm not back again this time tomorrow // If I'm not back again this time tomorrow
// Carry on, carry on, as if nothing really matters // Carry on, carry on, as if nothing really matters
// //
// Too late, my time has come // Too late, my time has come
// Sends shivers down my spine // Sends shivers down my spine
// Body's aching all the time // Body's aching all the time
// Goodbye everybody - I've got to go // Goodbye everybody - I've got to go
// Gotta leave you all behind and face the truth // Gotta leave you all behind and face the truth
// Mama, oooh // Mama, oooh
// I don't want to die // I don't want to die
// I sometimes wish I'd never been born at all // I sometimes wish I'd never been born at all
/** /**
* Brian May enters the stage. * Brian May enters the stage.
*/ */
resolve(data) resolve(data);
}, error => reject(error)) },
}) error => reject(error)
);
});
} }
} };

View file

@ -1,14 +1,14 @@
import config from '@/config' import config from "@/config";
import artist from './artist' import artist from "./artist";
export default { export default {
artist, artist,
id: 0, id: 0,
artist_id: 0, artist_id: 0,
name: '', name: "",
cover: config.unknownCover, cover: config.unknownCover,
playCount: 0, playCount: 0,
length: 0, length: 0,
fmtLength: '00:00', fmtLength: "00:00",
songs: [] songs: []
} };

View file

@ -1,8 +1,8 @@
export default { export default {
id: 0, id: 0,
name: '', name: "",
image: null, image: null,
playCount: 0, playCount: 0,
albums: [], albums: [],
songs: [] songs: []
} };

View file

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

View file

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

View file

@ -1,16 +1,16 @@
import album from './album' import album from "./album";
import artist from './artist' import artist from "./artist";
export default { export default {
album, album,
artist, artist,
id: null, id: null,
album_id: 0, album_id: 0,
title: '', title: "",
length: 0, length: 0,
fmtLength: '00:00', fmtLength: "00:00",
lyrics: '', lyrics: "",
liked: false, liked: false,
playCount: 0, playCount: 0,
playbackState: 'stopped' playbackState: "stopped"
} };

View file

@ -1,7 +1,7 @@
export default { export default {
id: 0, id: 0,
name: '', name: "",
email: '', email: "",
avatar: '', avatar: "",
is_admin: false is_admin: false
} };

View file

@ -2,222 +2,222 @@ export default {
artists: [ artists: [
{ {
id: 1, id: 1,
name: 'Unknown Artist' name: "Unknown Artist"
}, },
{ {
id: 2, id: 2,
name: 'Various Artists' name: "Various Artists"
}, },
{ {
id: 3, id: 3,
name: 'All-4-One' name: "All-4-One"
}, },
{ {
id: 4, id: 4,
name: 'Boy Dylan' name: "Boy Dylan"
}, },
{ {
id: 5, id: 5,
name: 'James Blunt' name: "James Blunt"
} }
], ],
albums: [ albums: [
{ {
id: 1193, id: 1193,
artist_id: 3, artist_id: 3,
name: 'All-4-One', name: "All-4-One",
cover: '/public/img/covers/565c0f7067425.jpeg' cover: "/public/img/covers/565c0f7067425.jpeg"
}, },
{ {
id: 1194, id: 1194,
artist_id: 3, artist_id: 3,
name: 'And The Music Speaks', name: "And The Music Speaks",
cover: '/public/img/covers/unknown-album.png' cover: "/public/img/covers/unknown-album.png"
}, },
{ {
id: 1195, id: 1195,
artist_id: 3, artist_id: 3,
name: 'Space Jam', name: "Space Jam",
cover: '/public/img/covers/565c0f7115e0f.png' cover: "/public/img/covers/565c0f7115e0f.png"
}, },
{ {
id: 1217, id: 1217,
artist_id: 4, artist_id: 4,
name: 'Highway 61 Revisited', name: "Highway 61 Revisited",
cover: '/public/img/covers/565c0f76dc6e8.jpeg' cover: "/public/img/covers/565c0f76dc6e8.jpeg"
}, },
{ {
id: 1218, id: 1218,
artist_id: 4, artist_id: 4,
name: 'Pat Garrett & Billy the Kid', name: "Pat Garrett & Billy the Kid",
cover: '/public/img/covers/unknown-album.png' cover: "/public/img/covers/unknown-album.png"
}, },
{ {
id: 1219, id: 1219,
artist_id: 4, artist_id: 4,
name: "The Times They Are A-Changin", name: "The Times They Are A-Changin",
cover: '/public/img/covers/unknown-album.png' cover: "/public/img/covers/unknown-album.png"
}, },
{ {
id: 1268, id: 1268,
artist_id: 5, artist_id: 5,
name: 'Back To Bedlam', name: "Back To Bedlam",
cover: '/public/img/covers/unknown-album.png' cover: "/public/img/covers/unknown-album.png"
} }
], ],
songs: [ songs: [
{ {
id: '39189f4545f9d5671fb3dc964f0080a0', id: "39189f4545f9d5671fb3dc964f0080a0",
album_id: 1193, album_id: 1193,
artist_id: 3, artist_id: 3,
title: 'I Swear', title: "I Swear",
length: 259.92, length: 259.92,
playCount: 4 playCount: 4
}, },
{ {
id: 'a6a550f7d950d2a2520f9bf1a60f025a', id: "a6a550f7d950d2a2520f9bf1a60f025a",
album_id: 1194, album_id: 1194,
artist_id: 3, artist_id: 3,
title: 'I can love you like that', title: "I can love you like that",
length: 262.61, length: 262.61,
playCount: 2 playCount: 2
}, },
{ {
id: 'd86c30fd34f13c1aff8db59b7fc9c610', id: "d86c30fd34f13c1aff8db59b7fc9c610",
album_id: 1195, album_id: 1195,
artist_id: 3, artist_id: 3,
title: 'I turn to you', title: "I turn to you",
length: 293.04 length: 293.04
}, },
{ {
id: 'e6d3977f3ffa147801ca5d1fdf6fa55e', id: "e6d3977f3ffa147801ca5d1fdf6fa55e",
album_id: 1217, album_id: 1217,
artist_id: 4, artist_id: 4,
title: 'Like a rolling stone', title: "Like a rolling stone",
length: 373.63 length: 373.63
}, },
{ {
id: 'aa16bbef6a9710eb9a0f41ecc534fad5', id: "aa16bbef6a9710eb9a0f41ecc534fad5",
album_id: 1218, album_id: 1218,
artist_id: 4, artist_id: 4,
title: "Knockin' on heaven's door", title: "Knockin' on heaven's door",
length: 151.9 length: 151.9
}, },
{ {
id: 'cb7edeac1f097143e65b1b2cde102482', id: "cb7edeac1f097143e65b1b2cde102482",
album_id: 1219, album_id: 1219,
artist_id: 4, artist_id: 4,
title: "The times they are a-changin'", title: "The times they are a-changin'",
length: 196 length: 196
}, },
{ {
id: '0ba9fb128427b32683b9eb9140912a70', id: "0ba9fb128427b32683b9eb9140912a70",
album_id: 1268, album_id: 1268,
artist_id: 5, artist_id: 5,
title: 'No bravery', title: "No bravery",
length: 243.12 length: 243.12
}, },
{ {
id: '123fd1ad32240ecab28a4e86ed5173', id: "123fd1ad32240ecab28a4e86ed5173",
album_id: 1268, album_id: 1268,
artist_id: 5, artist_id: 5,
title: 'So long, Jimmy', title: "So long, Jimmy",
length: 265.04 length: 265.04
}, },
{ {
id: '6a54c674d8b16732f26df73f59c63e21', id: "6a54c674d8b16732f26df73f59c63e21",
album_id: 1268, album_id: 1268,
artist_id: 5, artist_id: 5,
title: 'Wisemen', title: "Wisemen",
length: 223.14 length: 223.14
}, },
{ {
id: '6df7d82a9a8701e40d1c291cf14a16bc', id: "6df7d82a9a8701e40d1c291cf14a16bc",
album_id: 1268, album_id: 1268,
artist_id: 5, artist_id: 5,
title: 'Goodbye my lover', title: "Goodbye my lover",
length: 258.61 length: 258.61
}, },
{ {
id: '74a2000d343e4587273d3ad14e2fd741', id: "74a2000d343e4587273d3ad14e2fd741",
album_id: 1268, album_id: 1268,
artist_id: 5, artist_id: 5,
title: 'High', title: "High",
length: 245.86 length: 245.86
}, },
{ {
id: '7900ab518f51775fe6cf06092c074ee5', id: "7900ab518f51775fe6cf06092c074ee5",
album_id: 1268, album_id: 1268,
artist_id: 5, artist_id: 5,
title: "You're beautiful", title: "You're beautiful",
length: 213.29 length: 213.29
}, },
{ {
id: '803910a51f9893347e087af851e38777', id: "803910a51f9893347e087af851e38777",
album_id: 1268, album_id: 1268,
artist_id: 5, artist_id: 5,
title: 'Cry', title: "Cry",
length: 246.91 length: 246.91
}, },
{ {
id: 'd82b0d4d4803ebbcb61000a5b6a868f5', id: "d82b0d4d4803ebbcb61000a5b6a868f5",
album_id: 1268, album_id: 1268,
artist_id: 5, artist_id: 5,
title: 'Tears and rain', title: "Tears and rain",
length: 244.45 length: 244.45
} }
], ],
interactions: [ interactions: [
{ {
id: 1, id: 1,
song_id: '7900ab518f51775fe6cf06092c074ee5', song_id: "7900ab518f51775fe6cf06092c074ee5",
liked: false, liked: false,
play_count: 1 play_count: 1
}, },
{ {
id: 2, id: 2,
song_id: '95c0ffc33c08c8c14ea5de0a44d5df3c', song_id: "95c0ffc33c08c8c14ea5de0a44d5df3c",
liked: false, liked: false,
play_count: 2 play_count: 2
}, },
{ {
id: 3, id: 3,
song_id: 'c83b201502eb36f1084f207761fa195c', song_id: "c83b201502eb36f1084f207761fa195c",
liked: false, liked: false,
play_count: 1 play_count: 1
}, },
{ {
id: 4, id: 4,
song_id: 'cb7edeac1f097143e65b1b2cde102482', song_id: "cb7edeac1f097143e65b1b2cde102482",
liked: true, liked: true,
play_count: 3 play_count: 3
}, },
{ {
id: 5, id: 5,
song_id: 'ccc38cc14bb95aefdf6da4b34adcf548', song_id: "ccc38cc14bb95aefdf6da4b34adcf548",
liked: false, liked: false,
play_count: 4 play_count: 4
} }
], ],
currentUser: { currentUser: {
id: 1, id: 1,
name: 'Phan An', name: "Phan An",
email: 'me@phanan.net', email: "me@phanan.net",
is_admin: true is_admin: true
}, },
users: [ users: [
{ {
id: 1, id: 1,
name: 'Phan An', name: "Phan An",
email: 'me@phanan.net', email: "me@phanan.net",
is_admin: true is_admin: true
}, },
{ {
id: 2, id: 2,
name: 'John Doe', name: "John Doe",
email: 'john@doe.tld', email: "john@doe.tld",
is_admin: false is_admin: false
} }
] ]
} };

View file

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

View file

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

View file

@ -1,48 +1,48 @@
require('chai').should() require("chai").should();
import { cloneDeep, last } from 'lodash' import { cloneDeep, last } from "lodash";
import { albumStore, artistStore } from '../../stores' import { albumStore, artistStore } from "../../stores";
import data from '../blobs/data' import data from "../blobs/data";
const { artists, albums } = data const { artists, albums } = data;
describe('stores/album', () => { describe("stores/album", () => {
beforeEach(() => { beforeEach(() => {
artistStore.init(cloneDeep(artists)) artistStore.init(cloneDeep(artists));
albumStore.init(cloneDeep(albums)) albumStore.init(cloneDeep(albums));
}) });
afterEach(() => { afterEach(() => {
artistStore.state.artists = [] artistStore.state.artists = [];
albumStore.state.albums = [] albumStore.state.albums = [];
}) });
describe('#init', () => { describe("#init", () => {
it('correctly gathers albums', () => { it("correctly gathers albums", () => {
albumStore.state.albums.length.should.equal(7) albumStore.state.albums.length.should.equal(7);
}) });
it('correctly sets album artists', () => { it("correctly sets album artists", () => {
albumStore.state.albums[0].artist.id.should.equal(3) albumStore.state.albums[0].artist.id.should.equal(3);
}) });
}) });
describe('#byId', () => { describe("#byId", () => {
it('correctly gets an album by ID', () => { it("correctly gets an album by ID", () => {
albumStore.byId(1193).name.should.equal('All-4-One') albumStore.byId(1193).name.should.equal("All-4-One");
}) });
}) });
describe('#compact', () => { describe("#compact", () => {
it('correctly compacts albums', () => { it("correctly compacts albums", () => {
albumStore.compact() albumStore.compact();
albumStore.state.albums.length.should.equal(0) albumStore.state.albums.length.should.equal(0);
}) });
}) });
describe('#all', () => { describe("#all", () => {
it('correctly returns all albums', () => { it("correctly returns all albums", () => {
albumStore.all.length.should.equal(7) albumStore.all.length.should.equal(7);
}) });
}) });
}) });

View file

@ -1,32 +1,32 @@
require('chai').should() require("chai").should();
import { cloneDeep, last } from 'lodash' import { cloneDeep, last } from "lodash";
import { artistStore } from '../../stores' import { artistStore } from "../../stores";
import data from '../blobs/data' import data from "../blobs/data";
const artists = data.artists const artists = data.artists;
describe('stores/artist', () => { describe("stores/artist", () => {
beforeEach(() => artistStore.init(cloneDeep(artists))) beforeEach(() => artistStore.init(cloneDeep(artists)));
afterEach(() => artistStore.state.artists = []) afterEach(() => (artistStore.state.artists = []));
describe('#init', () => { describe("#init", () => {
it('correctly gathers artists', () => { it("correctly gathers artists", () => {
artistStore.state.artists.length.should.equal(5) artistStore.state.artists.length.should.equal(5);
}) });
}) });
describe('#byId', () => { describe("#byId", () => {
it('correctly gets an artist by ID', () => { it("correctly gets an artist by ID", () => {
artistStore.byId(3).name.should.equal('All-4-One') artistStore.byId(3).name.should.equal("All-4-One");
}) });
}) });
describe('#compact', () => { describe("#compact", () => {
it('correctly compact artists', () => { it("correctly compact artists", () => {
artistStore.compact() artistStore.compact();
// because we've not processed songs/albums, all artists here have no songs // because we've not processed songs/albums, all artists here have no songs
// and should be removed after compact()ing // and should be removed after compact()ing
artistStore.state.artists.length.should.equal(0) artistStore.state.artists.length.should.equal(0);
}) });
}) });
}) });

View file

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

View file

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

View file

@ -1,77 +1,97 @@
require('chai').should() require("chai").should();
import { cloneDeep, last } from 'lodash' import { cloneDeep, last } from "lodash";
import { songStore, albumStore, artistStore, preferenceStore } from '../../stores' import {
import data from '../blobs/data' songStore,
albumStore,
artistStore,
preferenceStore
} from "../../stores";
import data from "../blobs/data";
const { songs, artists, albums, interactions } = data const { songs, artists, albums, interactions } = data;
describe('stores/song', () => { describe("stores/song", () => {
beforeEach(() => { beforeEach(() => {
artistStore.init(artists) artistStore.init(artists);
albumStore.init(albums) albumStore.init(albums);
songStore.init(songs) songStore.init(songs);
}) });
describe('#init', () => { describe("#init", () => {
it('correctly gathers all songs', () => { it("correctly gathers all songs", () => {
songStore.state.songs.length.should.equal(14) songStore.state.songs.length.should.equal(14);
}) });
it ('coverts lengths to formatted lengths', () => { it("coverts lengths to formatted lengths", () => {
songStore.state.songs[0].fmtLength.should.be.a.string songStore.state.songs[0].fmtLength.should.be.a.string;
}) });
it('correctly sets albums', () => { it("correctly sets albums", () => {
songStore.state.songs[0].album.id.should.equal(1193) songStore.state.songs[0].album.id.should.equal(1193);
}) });
}) });
describe('#all', () => { describe("#all", () => {
it('correctly returns all songs', () => { it("correctly returns all songs", () => {
songStore.all.length.should.equal(14) songStore.all.length.should.equal(14);
}) });
}) });
describe('#byId', () => { describe("#byId", () => {
it('correctly gets a song by ID', () => { it("correctly gets a song by ID", () => {
songStore.byId('e6d3977f3ffa147801ca5d1fdf6fa55e').title.should.equal('Like a rolling stone') songStore
}) .byId("e6d3977f3ffa147801ca5d1fdf6fa55e")
}) .title.should.equal("Like a rolling stone");
});
});
describe('#byIds', () => { describe("#byIds", () => {
it('correctly gets multiple songs by IDs', () => { it("correctly gets multiple songs by IDs", () => {
const songs = songStore.byIds(['e6d3977f3ffa147801ca5d1fdf6fa55e', 'aa16bbef6a9710eb9a0f41ecc534fad5']) const songs = songStore.byIds([
songs[0].title.should.equal('Like a rolling stone') "e6d3977f3ffa147801ca5d1fdf6fa55e",
songs[1].title.should.equal("Knockin' on heaven's door") "aa16bbef6a9710eb9a0f41ecc534fad5"
}) ]);
}) songs[0].title.should.equal("Like a rolling stone");
songs[1].title.should.equal("Knockin' on heaven's door");
});
});
describe('#initInteractions', () => { describe("#initInteractions", () => {
beforeEach(() => songStore.initInteractions(interactions)) beforeEach(() => songStore.initInteractions(interactions));
it('correctly sets interaction status', () => { it("correctly sets interaction status", () => {
const song = songStore.byId('cb7edeac1f097143e65b1b2cde102482') const song = songStore.byId("cb7edeac1f097143e65b1b2cde102482");
song.liked.should.be.true song.liked.should.be.true;
song.playCount.should.equal(3) song.playCount.should.equal(3);
}) });
}) });
describe('#addRecentlyPlayed', () => { describe("#addRecentlyPlayed", () => {
it('correctly adds a recently played song', () => { it("correctly adds a recently played song", () => {
songStore.addRecentlyPlayed(songStore.byId('cb7edeac1f097143e65b1b2cde102482')) songStore.addRecentlyPlayed(
songStore.recentlyPlayed[0].id.should.equal('cb7edeac1f097143e65b1b2cde102482') songStore.byId("cb7edeac1f097143e65b1b2cde102482")
preferenceStore.get('recent-songs')[0].should.equal('cb7edeac1f097143e65b1b2cde102482') );
}) songStore.recentlyPlayed[0].id.should.equal(
"cb7edeac1f097143e65b1b2cde102482"
);
preferenceStore
.get("recent-songs")[0]
.should.equal("cb7edeac1f097143e65b1b2cde102482");
});
it('correctly gathers the songs from local storage', () => { it("correctly gathers the songs from local storage", () => {
songStore.gatherRecentlyPlayedFromLocalStorage()[0].id.should.equal('cb7edeac1f097143e65b1b2cde102482') songStore
}) .gatherRecentlyPlayedFromLocalStorage()[0]
}) .id.should.equal("cb7edeac1f097143e65b1b2cde102482");
});
});
describe('#guess', () => { describe("#guess", () => {
it('correcty guesses a song', () => { it("correcty guesses a song", () => {
songStore.guess('i swear', albumStore.byId(1193)).id.should.equal('39189f4545f9d5671fb3dc964f0080a0') songStore
}) .guess("i swear", albumStore.byId(1193))
}) .id.should.equal("39189f4545f9d5671fb3dc964f0080a0");
}) });
});
});

View file

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

View file

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

View file

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

View file

@ -1,37 +1,37 @@
import alertify from 'alertify.js' import alertify from "alertify.js";
const alerts = { const alerts = {
alert (msg) { alert(msg) {
alertify.alert(msg) alertify.alert(msg);
}, },
confirm (msg, okFunc, cancelFunc = null) { confirm(msg, okFunc, cancelFunc = null) {
alertify.confirm(msg, okFunc, cancelFunc) alertify.confirm(msg, okFunc, cancelFunc);
}, },
log (msg, type, cb = null) { log(msg, type, cb = null) {
alertify.logPosition('top right') alertify.logPosition("top right");
alertify.closeLogOnClick(true) alertify.closeLogOnClick(true);
switch (type) { switch (type) {
case 'success': case "success":
alertify.success(msg, cb) alertify.success(msg, cb);
break break;
case 'error': case "error":
alertify.error(msg, cb) alertify.error(msg, cb);
break break;
default: default:
alertify.log(msg, cb) alertify.log(msg, cb);
break break;
} }
}, },
success (msg, cb = null) { success(msg, cb = null) {
return this.log(msg, 'success', cb) return this.log(msg, "success", cb);
}, },
error (msg, cb = null) { error(msg, cb = null) {
return this.log(msg, 'error', cb) return this.log(msg, "error", cb);
} }
} };
export { alerts } export { alerts };

View file

@ -1,8 +1,8 @@
/** /**
* Other common methods. * Other common methods.
*/ */
import select from 'select' import select from "select";
import { event } from '@/utils' import { event } from "@/utils";
/** /**
* Load (display) a main panel (view). * Load (display) a main panel (view).
@ -10,17 +10,17 @@ import { event } from '@/utils'
* @param {String} view The view, which can be found under components/main-wrapper/main-content. * @param {String} view The view, which can be found under components/main-wrapper/main-content.
* @param {...*} Extra data to attach to the view. * @param {...*} Extra data to attach to the view.
*/ */
export function loadMainView (view, ...args) { export function loadMainView(view, ...args) {
event.emit('main-content-view:load', view, ...args) event.emit("main-content-view:load", view, ...args);
} }
/** /**
* Force reloading window regardless of "Confirm before reload" setting. * Force reloading window regardless of "Confirm before reload" setting.
* This is handy for certain cases, for example Last.fm connect/disconnect. * This is handy for certain cases, for example Last.fm connect/disconnect.
*/ */
export function forceReloadWindow () { export function forceReloadWindow() {
window.onbeforeunload = function () {} window.onbeforeunload = function() {};
window.location.reload() window.location.reload();
} }
/** /**
@ -30,15 +30,19 @@ export function forceReloadWindow () {
* @param {String} type * @param {String} type
* @param {Boolean} dismissable * @param {Boolean} dismissable
*/ */
export function showOverlay (message = 'Just a little patience…', type = 'loading', dismissable = false) { export function showOverlay(
event.emit('overlay:show', { message, type, dismissable }) message = "Just a little patience…",
type = "loading",
dismissable = false
) {
event.emit("overlay:show", { message, type, dismissable });
} }
/** /**
* Hide the overlay. * Hide the overlay.
*/ */
export function hideOverlay () { export function hideOverlay() {
event.emit('overlay:hide') event.emit("overlay:hide");
} }
/** /**
@ -46,10 +50,11 @@ export function hideOverlay () {
* *
* @param {string} txt * @param {string} txt
*/ */
export function copyText (txt) { export function copyText(txt) {
const copyArea = document.querySelector('#copyArea') const copyArea = document.querySelector("#copyArea");
copyArea.style.top = `${window.pageYOffset || document.documentElement.scrollTop}px` copyArea.style.top = `${window.pageYOffset ||
copyArea.value = txt document.documentElement.scrollTop}px`;
select(copyArea) copyArea.value = txt;
document.execCommand('copy') select(copyArea);
document.execCommand("copy");
} }

View file

@ -1,84 +1,84 @@
import { each, isObject, isNumber, get } from 'lodash' import { each, isObject, isNumber, get } from "lodash";
export function orderBy (arr, sortKey, reverse) { export function orderBy(arr, sortKey, reverse) {
if (!sortKey) { if (!sortKey) {
return arr return arr;
} }
const order = (reverse && reverse < 0) ? -1 : 1 const order = reverse && reverse < 0 ? -1 : 1;
function compareRecordsByKey (a, b, key) { function compareRecordsByKey(a, b, key) {
let aKey = isObject(a) ? get(a, key) : a let aKey = isObject(a) ? get(a, key) : a;
let bKey = isObject(b) ? get(b, key) : b let bKey = isObject(b) ? get(b, key) : b;
if (isNumber(aKey) && isNumber(bKey)) { if (isNumber(aKey) && isNumber(bKey)) {
return aKey === bKey ? 0 : aKey > bKey return aKey === bKey ? 0 : aKey > bKey;
} }
aKey = aKey === undefined ? aKey : `${aKey}`.toLowerCase() aKey = aKey === undefined ? aKey : `${aKey}`.toLowerCase();
bKey = bKey === undefined ? bKey : `${bKey}`.toLowerCase() bKey = bKey === undefined ? bKey : `${bKey}`.toLowerCase();
return aKey === bKey ? 0 : aKey > bKey return aKey === bKey ? 0 : aKey > bKey;
} }
// sort on a copy to avoid mutating original array // sort on a copy to avoid mutating original array
return arr.slice().sort((a, b) => { return arr.slice().sort((a, b) => {
if (sortKey.constructor === Array) { if (sortKey.constructor === Array) {
let diff = 0 let diff = 0;
for (let i = 0; i < sortKey.length; i++) { for (let i = 0; i < sortKey.length; i++) {
diff = compareRecordsByKey(a, b, sortKey[i]) diff = compareRecordsByKey(a, b, sortKey[i]);
if (diff !== 0) { if (diff !== 0) {
break break;
} }
} }
return diff === 0 ? 0 : diff === true ? order : -order return diff === 0 ? 0 : diff === true ? order : -order;
} }
a = isObject(a) ? get(a, sortKey) : a a = isObject(a) ? get(a, sortKey) : a;
b = isObject(b) ? get(b, sortKey) : b b = isObject(b) ? get(b, sortKey) : b;
if (isNumber(a) && isNumber(b)) { if (isNumber(a) && isNumber(b)) {
return a === b ? 0 : a > b ? order : -order return a === b ? 0 : a > b ? order : -order;
} }
a = a === undefined ? a : a.toLowerCase() a = a === undefined ? a : a.toLowerCase();
b = b === undefined ? b : b.toLowerCase() b = b === undefined ? b : b.toLowerCase();
return a === b ? 0 : a > b ? order : -order return a === b ? 0 : a > b ? order : -order;
}) });
} }
export function limitBy (arr, n, offset = 0) { export function limitBy(arr, n, offset = 0) {
return arr.slice(offset, offset + n) return arr.slice(offset, offset + n);
} }
export function filterBy (arr, search, ...keys) { export function filterBy(arr, search, ...keys) {
if (!search) { if (!search) {
return arr return arr;
} }
// cast to lowercase string // cast to lowercase string
search = (`${search}`).toLowerCase() search = `${search}`.toLowerCase();
const res = [] const res = [];
each(arr, item => { each(arr, item => {
each(keys, key => { each(keys, key => {
if (`${get(item, key)}`.toLowerCase().indexOf(search) !== -1) { if (`${get(item, key)}`.toLowerCase().indexOf(search) !== -1) {
res.push(item) res.push(item);
return false return false;
} }
}) });
}) });
return res return res;
} }
export function pluralize () { export function pluralize() {
if (!arguments[0] || arguments[0] > 1) { if (!arguments[0] || arguments[0] > 1) {
return `${arguments[0]} ${arguments[1]}s` return `${arguments[0]} ${arguments[1]}s`;
} }
return `${arguments[0]} ${arguments[1]}` return `${arguments[0]} ${arguments[1]}`;
} }

View file

@ -2,28 +2,28 @@
* Convert a duration in seconds into H:i:s format. * Convert a duration in seconds into H:i:s format.
* If H is 0, it will be ommited. * If H is 0, it will be ommited.
*/ */
export function secondsToHis (d) { export function secondsToHis(d) {
d = ~~d d = ~~d;
let s = d % 60 let s = d % 60;
if (s < 10) { if (s < 10) {
s = '0' + s s = "0" + s;
} }
let i = Math.floor((d / 60) % 60) let i = Math.floor((d / 60) % 60);
if (i < 10) { if (i < 10) {
i = '0' + i i = "0" + i;
} }
let h = Math.floor(d / 3600) let h = Math.floor(d / 3600);
if (h < 10) { if (h < 10) {
h = '0' + h h = "0" + h;
} }
return (h === '00' ? '' : h + ':') + i + ':' + s return (h === "00" ? "" : h + ":") + i + ":" + s;
} }
/** /**
@ -33,8 +33,11 @@ export function secondsToHis (d) {
* *
* @return {Array.<String>} * @return {Array.<String>}
*/ */
export function parseValidationError (error) { export function parseValidationError(error) {
return Object.keys(error).reduce((messages, field) => messages.concat(error[field]), []) return Object.keys(error).reduce(
(messages, field) => messages.concat(error[field]),
[]
);
} }
/** /**
@ -44,6 +47,6 @@ export function parseValidationError (error) {
* *
* @return {string} * @return {string}
*/ */
export function br2nl (str) { export function br2nl(str) {
return str.replace(/<br\s*[\/]?>/gi, '\n') return str.replace(/<br\s*[\/]?>/gi, "\n");
} }

View file

@ -1,6 +1,6 @@
export * from './alerts' export * from "./alerts";
export * from './filters' export * from "./filters";
export * from './formatters' export * from "./formatters";
export * from './supports' export * from "./supports";
export * from './common' export * from "./common";
export * from './$' export * from "./$";

View file

@ -1,51 +1,52 @@
import isMobile from 'ismobilejs' import isMobile from "ismobilejs";
import Vue from 'vue' import Vue from "vue";
import { each } from 'lodash' import { each } from "lodash";
/** /**
* Check if AudioContext is supported by the current browser. * Check if AudioContext is supported by the current browser.
* *
* @return {Boolean} * @return {Boolean}
*/ */
export function isAudioContextSupported () { export function isAudioContextSupported() {
// Apple device just doesn't love AudioContext that much. // Apple device just doesn't love AudioContext that much.
if (isMobile.apple.device) { if (isMobile.apple.device) {
return false return false;
} }
const AudioContext = (window.AudioContext || const AudioContext =
window.AudioContext ||
window.webkitAudioContext || window.webkitAudioContext ||
window.mozAudioContext || window.mozAudioContext ||
window.oAudioContext || window.oAudioContext ||
window.msAudioContext) window.msAudioContext;
if (!AudioContext) { if (!AudioContext) {
return false return false;
} }
// Safari (MacOS & iOS alike) has webkitAudioContext, but is buggy. // Safari (MacOS & iOS alike) has webkitAudioContext, but is buggy.
// @link http://caniuse.com/#search=audiocontext // @link http://caniuse.com/#search=audiocontext
if (!(new AudioContext()).createMediaElementSource) { if (!new AudioContext().createMediaElementSource) {
return false return false;
} }
return true return true;
} }
/** /**
* Checks if HTML5 clipboard can be used. * Checks if HTML5 clipboard can be used.
* @return {Boolean} * @return {Boolean}
*/ */
export function isClipboardSupported () { export function isClipboardSupported() {
return 'execCommand' in document return "execCommand" in document;
} }
/** /**
* Checks if Media Session API is supported. * Checks if Media Session API is supported.
* @return {Boolean} * @return {Boolean}
*/ */
export function isMediaSessionSupported () { export function isMediaSessionSupported() {
return 'mediaSession' in navigator return "mediaSession" in navigator;
} }
/** /**
@ -56,28 +57,30 @@ export function isMediaSessionSupported () {
const event = { const event = {
bus: null, bus: null,
init () { init() {
if (!this.bus) { if (!this.bus) {
this.bus = new Vue() this.bus = new Vue();
} }
return this return this;
}, },
emit (name, ...args) { emit(name, ...args) {
this.bus.$emit(name, ...args) this.bus.$emit(name, ...args);
return this return this;
}, },
on () { on() {
if (arguments.length === 2) { if (arguments.length === 2) {
this.bus.$on(arguments[0], arguments[1]) this.bus.$on(arguments[0], arguments[1]);
} else { } else {
each(Object.keys(arguments[0]), key => this.bus.$on(key, arguments[0][key])) each(Object.keys(arguments[0]), key =>
this.bus.$on(key, arguments[0][key])
);
} }
return this return this;
} }
} };
export { event } export { event };

View file

@ -1,33 +1,36 @@
const mix = require('laravel-mix') const mix = require("laravel-mix");
const fs = require('fs') const fs = require("fs");
mix.setResourceRoot('./public/') mix.setResourceRoot("./public/");
mix.config.detectHotReloading() mix.config.detectHotReloading();
if (mix.config.hmr) { if (mix.config.hmr) {
// There's a bug with Mix/copy plugin which prevents HMR from working: // There's a bug with Mix/copy plugin which prevents HMR from working:
// https://github.com/JeffreyWay/laravel-mix/issues/150 // https://github.com/JeffreyWay/laravel-mix/issues/150
console.log('In HMR mode. If assets are missing, Ctr+C and run `yarn dev` first.') console.log(
"In HMR mode. If assets are missing, Ctr+C and run `yarn dev` first."
);
// Somehow public/hot isn't being removed by Mix. We'll handle it ourselves. // Somehow public/hot isn't being removed by Mix. We'll handle it ourselves.
process.on('SIGINT', () => { process.on("SIGINT", () => {
try { try {
fs.unlinkSync(mix.config.publicPath + '/hot') fs.unlinkSync(mix.config.publicPath + "/hot");
} catch (e) { } catch (e) {}
} process.exit();
process.exit() });
})
} else { } else {
mix.copy('resources/assets/img', 'public/img', false) mix
.copy('node_modules/font-awesome/fonts', 'public/fonts', false) .copy("resources/assets/img", "public/img", false)
.copy("node_modules/font-awesome/fonts", "public/fonts", false);
} }
mix.js('resources/assets/js/app.js', 'public/js') mix
.sass('resources/assets/sass/app.scss', 'public/css') .js("resources/assets/js/app.js", "public/js")
.js('resources/assets/js/remote/app.js', 'public/js/remote') .sass("resources/assets/sass/app.scss", "public/css")
.sass('resources/assets/sass/remote.scss', 'public/css') .js("resources/assets/js/remote/app.js", "public/js/remote")
.sass("resources/assets/sass/remote.scss", "public/css");
if (mix.config.inProduction) { if (mix.config.inProduction) {
mix.version() mix.version();
mix.disableNotifications() mix.disableNotifications();
} }