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

View file

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

View file

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

View file

@ -3,6 +3,6 @@
*/
export const focusDirective = {
inserted(el) {
el.focus()
}
el.focus();
}
};

View file

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

View file

@ -1,3 +1,143 @@
/*! modernizr 3.2.0 (Custom Build) | MIT *
* 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 {
computed: {
length() {
return this.album.songs.reduce((acc, song) => {
return acc + song.length
}, 0)
return acc + song.length;
}, 0);
},
fmtLength() {
return secondsToHis(this.length)
}
return secondsToHis(this.length);
}
}
};

View file

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

View file

@ -2,13 +2,13 @@
* Add necessary functionalities into a view that contains a song-list component.
*/
import { assignIn } from 'lodash'
import isMobile from 'ismobilejs'
import { assignIn } from "lodash";
import isMobile from "ismobilejs";
import { playback } from '@/services'
import songList from '@/components/shared/song-list.vue'
import songListControls from '@/components/shared/song-list-controls.vue'
import controlsToggler from '@/components/shared/song-list-controls-toggler.vue'
import { playback } from "@/services";
import songList from "@/components/shared/song-list.vue";
import songListControls from "@/components/shared/song-list-controls.vue";
import controlsToggler from "@/components/shared/song-list-controls-toggler.vue";
export default {
components: { songList, songListControls, controlsToggler },
@ -18,34 +18,34 @@ export default {
state: null,
meta: {
songCount: 0,
totalLength: '00:00'
totalLength: "00:00"
},
selectedSongs: [],
showingControls: false,
songListControlConfig: {},
isPhone: isMobile.phone
}
};
},
methods: {
setSelectedSongs(songs) {
this.selectedSongs = songs
this.selectedSongs = songs;
},
updateMeta(meta) {
assignIn(this.meta, meta)
assignIn(this.meta, meta);
},
shuffleAll() {
playback.queueAndPlay(this.state.songs, true)
playback.queueAndPlay(this.state.songs, true);
},
shuffleSelected() {
playback.queueAndPlay(this.selectedSongs, true)
playback.queueAndPlay(this.selectedSongs, true);
},
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.
@ -12,7 +12,7 @@ export default {
return {
numOfItems: 30, // Number of currently loaded and displayed items
perPage: 30 // Number of items to be loaded per "page"
}
};
},
methods: {
@ -20,7 +20,7 @@ export default {
// 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 (scrollTop + clientHeight >= scrollHeight - 32) {
this.displayMore()
this.displayMore();
}
},
@ -28,7 +28,7 @@ export default {
* Load and display more items into the scrollable area.
*/
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.
@ -14,7 +14,7 @@ export default {
shown: false,
top: 0,
left: 0
}
};
},
methods: {
@ -24,42 +24,42 @@ export default {
* Close all submenus.
*/
close() {
each(Array.from(this.$el.querySelectorAll('.submenu')), el => {
el.style.display = 'none'
})
this.shown = false
each(Array.from(this.$el.querySelectorAll(".submenu")), el => {
el.style.display = "none";
});
this.shown = false;
},
/**
* Queue select songs after the current song.
*/
queueSongsAfterCurrent() {
queueStore.queueAfterCurrent(this.songs)
this.close()
queueStore.queueAfterCurrent(this.songs);
this.close();
},
/**
* Queue selected songs to bottom of queue.
*/
queueSongsToBottom() {
queueStore.queue(this.songs)
this.close()
queueStore.queue(this.songs);
this.close();
},
/**
* Queue selected songs to top of queue.
*/
queueSongsToTop() {
queueStore.queue(this.songs, false, true)
this.close()
queueStore.queue(this.songs, false, true);
this.close();
},
/**
* Add the selected songs into Favorites.
*/
addSongsToFavorite() {
favoriteStore.like(this.songs)
this.close()
favoriteStore.like(this.songs);
this.close();
},
/**
@ -68,8 +68,8 @@ export default {
* @param {Object} playlist The playlist.
*/
addSongsToExistingPlaylist(playlist) {
playlistStore.addSongs(playlist, this.songs)
this.close()
}
playlistStore.addSongs(playlist, this.songs);
this.close();
}
}
};

View file

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

View file

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

View file

@ -1,109 +1,114 @@
import isMobile from 'ismobilejs'
import { each } from 'lodash'
import isMobile from "ismobilejs";
import { each } from "lodash";
import { loadMainView } from './utils'
import { artistStore, albumStore, songStore, queueStore, playlistStore, userStore } from './stores'
import { playback } from './services'
import { loadMainView } from "./utils";
import {
artistStore,
albumStore,
songStore,
queueStore,
playlistStore,
userStore
} from "./stores";
import { playback } from "./services";
export default {
routes: {
'/home' () {
loadMainView('home')
"/home"() {
loadMainView("home");
},
'/queue' () {
loadMainView('queue')
"/queue"() {
loadMainView("queue");
},
'/songs' () {
loadMainView('songs')
"/songs"() {
loadMainView("songs");
},
'/albums' () {
loadMainView('albums')
"/albums"() {
loadMainView("albums");
},
'/album/(\\d+)' (id) {
const album = albumStore.byId(~~id)
"/album/(\\d+)"(id) {
const album = albumStore.byId(~~id);
if (album) {
loadMainView('album', album)
loadMainView("album", album);
}
},
'/artists' () {
loadMainView('artists')
"/artists"() {
loadMainView("artists");
},
'/artist/(\\d+)' (id) {
const artist = artistStore.byId(~~id)
"/artist/(\\d+)"(id) {
const artist = artistStore.byId(~~id);
if (artist) {
loadMainView('artist', artist)
loadMainView("artist", artist);
}
},
'/favorites' () {
loadMainView('favorites')
"/favorites"() {
loadMainView("favorites");
},
'/playlist/(\\d+)' (id) {
const playlist = playlistStore.byId(~~id)
"/playlist/(\\d+)"(id) {
const playlist = playlistStore.byId(~~id);
if (playlist) {
loadMainView('playlist', playlist)
loadMainView("playlist", playlist);
}
},
'/settings' () {
userStore.current.is_admin && loadMainView('settings')
"/settings"() {
userStore.current.is_admin && loadMainView("settings");
},
'/users' () {
userStore.current.is_admin && loadMainView('users')
"/users"() {
userStore.current.is_admin && loadMainView("users");
},
'/profile' () {
loadMainView('profile')
"/profile"() {
loadMainView("profile");
},
'/login' () {
"/login"() {},
},
'/song/([a-z0-9]{32})' (id) {
const song = songStore.byId(id)
if (!song) return
"/song/([a-z0-9]{32})"(id) {
const song = songStore.byId(id);
if (!song) return;
if (isMobile.apple.device) {
// Mobile Safari doesn't allow autoplay, so we just queue.
queueStore.queue(song)
loadMainView('queue')
queueStore.queue(song);
loadMainView("queue");
} else {
playback.queueAndPlay(song)
playback.queueAndPlay(song);
}
},
'/youtube' () {
loadMainView('youtubePlayer')
"/youtube"() {
loadMainView("youtubePlayer");
}
},
init() {
this.loadState()
window.addEventListener('popstate', () => this.loadState(), true)
this.loadState();
window.addEventListener("popstate", () => this.loadState(), true);
},
loadState() {
if (!window.location.hash) {
return this.go('home')
return this.go("home");
}
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) {
const [, ...params] = matches
this.routes[route](...params)
return false
const [, ...params] = matches;
this.routes[route](...params);
return false;
}
})
});
},
/**
@ -112,14 +117,14 @@ export default {
* @param {String} path
*/
go(path) {
if (path[0] !== '/') {
path = `/${path}`
if (path[0] !== "/") {
path = `/${path}`;
}
if (path.indexOf('/#!') !== 0) {
path = `/#!${path}`
if (path.indexOf("/#!") !== 0) {
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 { ls } from '.'
import { playlistStore, favoriteStore } from "@/stores";
import { ls } from ".";
export const download = {
/**
@ -10,9 +10,9 @@ export const download = {
* @param {Array.<Object>|Object} songs
*/
fromSongs(songs) {
songs = [].concat(songs)
const query = reduce(songs, (q, song) => `songs[]=${song.id}&${q}`, '')
return this.trigger(`songs?${query}`)
songs = [].concat(songs);
const query = reduce(songs, (q, song) => `songs[]=${song.id}&${q}`, "");
return this.trigger(`songs?${query}`);
},
/**
@ -21,7 +21,7 @@ export const download = {
* @param {Object} album
*/
fromAlbum(album) {
return this.trigger(`album/${album.id}`)
return this.trigger(`album/${album.id}`);
},
/**
@ -33,7 +33,7 @@ export const download = {
// It's safe to assume an artist always has songs.
// After all, what's an artist without her songs?
// (See what I did there? Yes, I'm advocating for women's rights).
return this.trigger(`artist/${artist.id}`)
return this.trigger(`artist/${artist.id}`);
},
/**
@ -43,10 +43,10 @@ export const download = {
*/
fromPlaylist(playlist) {
if (!playlistStore.getSongs(playlist).length) {
return
return;
}
return this.trigger(`playlist/${playlist.id}`)
return this.trigger(`playlist/${playlist.id}`);
},
/**
@ -54,11 +54,11 @@ export const download = {
*/
fromFavorites() {
if (!favoriteStore.all.length) {
console.warn("You don't like any song? Come on, don't be that grumpy.")
return
console.warn("You don't like any song? Come on, don't be that grumpy.");
return;
}
return this.trigger('favorites')
return this.trigger("favorites");
},
/**
@ -68,10 +68,13 @@ export const download = {
* artist, playlist, or album.
*/
trigger(uri) {
const sep = uri.indexOf('?') === -1 ? '?' : '&'
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.setAttribute('src', `/api/download/${uri}${sep}jwt-token=${ls.get('jwt-token')}`)
document.body.appendChild(iframe)
}
const sep = uri.indexOf("?") === -1 ? "?" : "&";
const iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.setAttribute(
"src",
`/api/download/${uri}${sep}jwt-token=${ls.get("jwt-token")}`
);
document.body.appendChild(iframe);
}
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/*eslint-disable camelcase*/
import { http, albumInfo, artistInfo } from '..'
import { http, albumInfo, artistInfo } from "..";
export const songInfo = {
/**
@ -12,18 +12,22 @@ export const songInfo = {
return new Promise((resolve, reject) => {
// Check if the song's info has been retrieved before.
if (song.infoRetrieved) {
resolve(song)
return
resolve(song);
return;
}
http.get(`${song.id}/info`, ({ data: { artist_info, album_info, youtube, lyrics }}) => {
song.lyrics = lyrics
artist_info && artistInfo.merge(song.artist, artist_info)
album_info && albumInfo.merge(song.album, album_info)
song.youtube = youtube
song.infoRetrieved = true
resolve(song)
}, error => reject(error))
})
}
http.get(
`${song.id}/info`,
({ data: { artist_info, album_info, youtube, lyrics } }) => {
song.lyrics = lyrics;
artist_info && artistInfo.merge(song.artist, artist_info);
album_info && albumInfo.merge(song.album, album_info);
song.youtube = youtube;
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 = {
get(key, defaultVal = null) {
return localStore(key) || defaultVal
return localStore(key) || defaultVal;
},
set(key, val) {
return localStore(key, val)
return localStore(key, val);
},
remove(key) {
return localStore.remove(key)
}
return localStore.remove(key);
}
};

View file

@ -1,18 +1,24 @@
import { shuffle, orderBy } from 'lodash'
import plyr from 'plyr'
import Vue from 'vue'
import isMobile from 'ismobilejs'
import { shuffle, orderBy } from "lodash";
import plyr from "plyr";
import Vue from "vue";
import isMobile from "ismobilejs";
import { event, isMediaSessionSupported } from '@/utils'
import { queueStore, sharedStore, userStore, songStore, preferenceStore as preferences } from '@/stores'
import { socket } from '@/services'
import config from '@/config'
import router from '@/router'
import { event, isMediaSessionSupported } from "@/utils";
import {
queueStore,
sharedStore,
userStore,
songStore,
preferenceStore as preferences
} from "@/stores";
import { socket } from "@/services";
import config from "@/config";
import router from "@/router";
export const playback = {
player: null,
volumeInput: null,
repeatModes: ['NO_REPEAT', 'REPEAT_ALL', 'REPEAT_ONE'],
repeatModes: ["NO_REPEAT", "REPEAT_ALL", "REPEAT_ONE"],
initialized: false,
/**
@ -21,98 +27,114 @@ export const playback = {
init() {
// We don't need to init this service twice, or the media events will be duplicated.
if (this.initialized) {
return
return;
}
this.player = plyr.setup({
controls: []
})[0]
})[0];
this.audio = document.querySelector('audio')
this.volumeInput = document.getElementById('volumeRange')
this.audio = document.querySelector("audio");
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.
*/
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.
*/
player.addEventListener('ended', e => {
if (sharedStore.state.useLastfm && userStore.current.preferences.lastfm_session_key) {
songStore.scrobble(queueStore.current)
player.addEventListener("ended", e => {
if (
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.
*/
player.addEventListener('canplaythrough', e => {
const nextSong = queueStore.next
if (!nextSong || nextSong.preloaded || (isMobile.any && preferences.transcodeOnMobile)) {
player.addEventListener("canplaythrough", e => {
const nextSong = queueStore.next;
if (
!nextSong ||
nextSong.preloaded ||
(isMobile.any && preferences.transcodeOnMobile)
) {
// Don't preload if
// - there's no next song
// - next song has already been preloaded
// - we're on mobile and "transcode" option is checked
return
return;
}
const audio = document.createElement('audio')
audio.setAttribute('src', songStore.getSourceUrl(nextSong))
audio.setAttribute('preload', 'auto')
audio.load()
nextSong.preloaded = true
})
const audio = document.createElement("audio");
audio.setAttribute("src", songStore.getSourceUrl(nextSong));
audio.setAttribute("preload", "auto");
audio.load();
nextSong.preloaded = true;
});
player.addEventListener('timeupdate', e => {
const song = queueStore.current
player.addEventListener("timeupdate", e => {
const song = queueStore.current;
if (this.player.media.currentTime > 10 && !song.registeredPlayCount) {
// After 10 seconds, register a play count and add it into "recently played" list
songStore.addRecentlyPlayed(song)
songStore.registerPlay(song)
songStore.addRecentlyPlayed(song);
songStore.registerPlay(song);
song.registeredPlayCount = true
song.registeredPlayCount = true;
}
})
});
// On init, set the volume to the value found in the local storage.
this.setVolume(preferences.volume)
this.setVolume(preferences.volume);
// Init the equalizer if supported.
event.emit('equalizer:init', this.player.media)
event.emit("equalizer:init", this.player.media);
if (isMediaSessionSupported()) {
navigator.mediaSession.setActionHandler('play', () => this.resume())
navigator.mediaSession.setActionHandler('pause', () => this.pause())
navigator.mediaSession.setActionHandler('previoustrack', () => this.playPrev())
navigator.mediaSession.setActionHandler('nexttrack', () => this.playNext())
navigator.mediaSession.setActionHandler("play", () => this.resume());
navigator.mediaSession.setActionHandler("pause", () => this.pause());
navigator.mediaSession.setActionHandler("previoustrack", () =>
this.playPrev()
);
navigator.mediaSession.setActionHandler("nexttrack", () =>
this.playNext()
);
}
socket.listen('playback:toggle', () => this.toggle())
.listen('playback:next', () => this.playNext())
.listen('playback:prev', () => this.playPrev())
.listen('status:get', () => {
const data = queueStore.current ? songStore.generateDataToBroadcast(queueStore.current) : {}
data.volume = this.volumeInput.value
socket.broadcast('status', data)
socket
.listen("playback:toggle", () => this.toggle())
.listen("playback:next", () => this.playNext())
.listen("playback:prev", () => this.playPrev())
.listen("status:get", () => {
const data = queueStore.current
? songStore.generateDataToBroadcast(queueStore.current)
: {};
data.volume = this.volumeInput.value;
socket.broadcast("status", data);
})
.listen('song:getcurrent', () => {
.listen("song:getcurrent", () => {
socket.broadcast(
'song',
"song",
queueStore.current
? songStore.generateDataToBroadcast(queueStore.current)
: { song: null }
)
);
})
.listen('volume:set', ({ volume }) => this.setVolume(volume))
.listen("volume:set", ({ volume }) => this.setVolume(volume));
this.initialized = true
this.initialized = true;
},
/**
@ -127,27 +149,29 @@ export const playback = {
*/
play(song) {
if (!song) {
return
return;
}
if (queueStore.current) {
queueStore.current.playbackState = 'stopped'
queueStore.current.playbackState = "stopped";
}
song.playbackState = 'playing'
song.playbackState = "playing";
// Set the song as the current song
queueStore.current = song
queueStore.current = song;
// Manually set the `src` attribute of the audio to prevent plyr from resetting
// the audio media object and cause our equalizer to malfunction.
this.player.media.src = songStore.getSourceUrl(song)
this.player.media.src = songStore.getSourceUrl(song);
document.title = `${song.title}${config.appTitle}`
document.querySelector('.plyr audio').setAttribute('title', `${song.artist.name} - ${song.title}`)
document.title = `${song.title}${config.appTitle}`;
document
.querySelector(".plyr audio")
.setAttribute("title", `${song.artist.name} - ${song.title}`);
// We'll just "restart" playing the song, which will handle notification, scrobbling etc.
this.restart()
this.restart();
},
/**
@ -158,34 +182,34 @@ export const playback = {
showNotification(song) {
// Show the notification if we're allowed to
if (!window.Notification || !preferences.notify) {
return
return;
}
try {
const notif = new window.Notification(`${song.title}`, {
icon: song.album.cover,
body: `${song.album.name} ${song.artist.name}`
})
});
notif.onclick = () => window.focus()
notif.onclick = () => window.focus();
// Close the notif after 5 secs.
window.setTimeout(() => notif.close(), 5000)
window.setTimeout(() => notif.close(), 5000);
} catch (e) {
// Notification fails.
// @link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification
}
if ('mediaSession' in navigator) {
if ("mediaSession" in navigator) {
/* global MediaMetadata */
navigator.mediaSession.metadata = new MediaMetadata({
title: song.title,
artist: song.artist.name,
album: song.album.name,
artwork: [
{ src: song.album.cover, sizes: '256x256', type: 'image/png' }
{ src: song.album.cover, sizes: "256x256", type: "image/png" }
]
})
});
}
},
@ -193,21 +217,21 @@ export const playback = {
* Restart playing a song.
*/
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
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.play()
this.player.restart();
this.player.play();
},
/**
@ -218,11 +242,11 @@ export const playback = {
*/
get next() {
if (queueStore.next) {
return queueStore.next
return queueStore.next;
}
if (preferences.repeatMode === 'REPEAT_ALL') {
return queueStore.first
if (preferences.repeatMode === "REPEAT_ALL") {
return queueStore.first;
}
},
@ -234,11 +258,11 @@ export const playback = {
*/
get previous() {
if (queueStore.previous) {
return queueStore.previous
return queueStore.previous;
}
if (preferences.repeatMode === 'REPEAT_ALL') {
return queueStore.last
if (preferences.repeatMode === "REPEAT_ALL") {
return queueStore.last;
}
},
@ -247,13 +271,13 @@ export const playback = {
* The selected mode will be stored into local storage as well.
*/
changeRepeatMode() {
let index = this.repeatModes.indexOf(preferences.repeatMode) + 1
let index = this.repeatModes.indexOf(preferences.repeatMode) + 1;
if (index >= this.repeatModes.length) {
index = 0
index = 0;
}
preferences.repeatMode = this.repeatModes[index]
preferences.repeatMode = this.repeatModes[index];
},
/**
@ -264,15 +288,15 @@ export const playback = {
// If the song's duration is greater than 5 seconds and we've passed 5 seconds into it,
// restart playing instead.
if (this.player.media.currentTime > 5 && queueStore.current.length > 5) {
this.player.restart()
this.player.restart();
return
return;
}
const prev = this.previous
!prev && preferences.repeatMode === 'NO_REPEAT'
const prev = this.previous;
!prev && preferences.repeatMode === "NO_REPEAT"
? this.stop()
: this.play(prev)
: this.play(prev);
},
/**
@ -280,10 +304,10 @@ export const playback = {
* If the next song is not found and the current mode is NO_REPEAT, we stop completely.
*/
playNext() {
const next = this.next
!next && preferences.repeatMode === 'NO_REPEAT'
const next = this.next;
!next && preferences.repeatMode === "NO_REPEAT"
? this.stop() // Nothing lasts forever, even cold November rain.
: this.play(next)
: this.play(next);
},
/**
@ -293,20 +317,20 @@ export const playback = {
* @param {Boolean=true} persist Whether the volume should be saved into local storage
*/
setVolume(volume, persist = true) {
this.player.setVolume(volume)
this.player.setVolume(volume);
if (persist) {
preferences.volume = volume
preferences.volume = volume;
}
this.volumeInput.value = volume
this.volumeInput.value = volume;
},
/**
* Mute playback.
*/
mute() {
this.setVolume(0, false)
this.setVolume(0, false);
},
/**
@ -314,45 +338,51 @@ export const playback = {
*/
unmute() {
// If the saved volume is 0, we unmute to the default level (7).
if (preferences.volume === '0' || preferences.volume === 0) {
preferences.volume = 7
if (preferences.volume === "0" || preferences.volume === 0) {
preferences.volume = 7;
}
this.setVolume(preferences.volume)
this.setVolume(preferences.volume);
},
/**
* Completely stop playback.
*/
stop() {
document.title = config.appTitle
this.player.pause()
this.player.seek(0)
document.title = config.appTitle;
this.player.pause();
this.player.seek(0);
if (queueStore.current) {
queueStore.current.playbackState = 'stopped'
queueStore.current.playbackState = "stopped";
}
socket.broadcast('playback:stopped')
socket.broadcast("playback:stopped");
},
/**
* Pause playback.
*/
pause() {
this.player.pause()
queueStore.current.playbackState = 'paused'
socket.broadcast('song', songStore.generateDataToBroadcast(queueStore.current))
this.player.pause();
queueStore.current.playbackState = "paused";
socket.broadcast(
"song",
songStore.generateDataToBroadcast(queueStore.current)
);
},
/**
* Resume playback.
*/
resume() {
this.player.play()
queueStore.current.playbackState = 'playing'
event.emit('song:played', queueStore.current)
socket.broadcast('song', songStore.generateDataToBroadcast(queueStore.current))
this.player.play();
queueStore.current.playbackState = "playing";
event.emit("song:played", queueStore.current);
socket.broadcast(
"song",
songStore.generateDataToBroadcast(queueStore.current)
);
},
/**
@ -360,16 +390,16 @@ export const playback = {
*/
toggle() {
if (!queueStore.current) {
this.playFirstInQueue()
return
this.playFirstInQueue();
return;
}
if (queueStore.current.playbackState !== 'playing') {
this.resume()
return
if (queueStore.current.playbackState !== "playing") {
this.resume();
return;
}
this.pause()
this.pause();
},
/**
@ -380,25 +410,25 @@ export const playback = {
*/
queueAndPlay(songs = null, shuffled = false) {
if (!songs) {
songs = songStore.all
songs = songStore.all;
}
if (!songs.length) {
return
return;
}
if (shuffled) {
songs = shuffle(songs)
songs = shuffle(songs);
}
queueStore.queue(songs, true)
queueStore.queue(songs, true);
// Wrap this inside a nextTick() to wait for the DOM to complete updating
// and then play the first song in the queue.
Vue.nextTick(() => {
router.go('queue')
this.play(queueStore.first)
})
router.go("queue");
this.play(queueStore.first);
});
},
/**
@ -406,7 +436,7 @@ export const playback = {
* If the current queue is empty, try creating it by shuffling all songs.
*/
playFirstInQueue() {
queueStore.all.length ? this.play(queueStore.first) : this.queueAndPlay()
queueStore.all.length ? this.play(queueStore.first) : this.queueAndPlay();
},
/**
@ -418,7 +448,7 @@ export const playback = {
playAllByArtist({ songs }, shuffled = true) {
shuffled
? this.queueAndPlay(songs, true)
: this.queueAndPlay(orderBy(songs, 'album_id', 'track'))
: this.queueAndPlay(orderBy(songs, "album_id", "track"));
},
/**
@ -430,6 +460,6 @@ export const playback = {
playAllInAlbum({ songs }, shuffled = true) {
shuffled
? this.queueAndPlay(songs, true)
: this.queueAndPlay(orderBy(songs, 'track'))
}
: this.queueAndPlay(orderBy(songs, "track"));
}
};

View file

@ -1,7 +1,7 @@
import Pusher from 'pusher-js'
import Pusher from "pusher-js";
import { userStore } from '@/stores'
import { ls } from '.'
import { userStore } from "@/stores";
import { ls } from ".";
export const socket = {
pusher: null,
@ -10,28 +10,28 @@ export const socket = {
async init() {
return new Promise((resolve, reject) => {
if (!window.PUSHER_APP_KEY) {
return resolve()
return resolve();
}
this.pusher = new Pusher(window.PUSHER_APP_KEY, {
authEndpoint: '/api/broadcasting/auth',
authEndpoint: "/api/broadcasting/auth",
auth: {
headers: {
Authorization: `Bearer ${ls.get('jwt-token')}`
Authorization: `Bearer ${ls.get("jwt-token")}`
}
},
cluster: window.PUSHER_APP_CLUSTER,
encrypted: true
})
});
this.channel = this.pusher.subscribe('private-koel')
this.channel.bind('pusher:subscription_succeeded', () => {
return resolve()
})
this.channel.bind('pusher:subscription_error', () => {
return resolve()
})
})
this.channel = this.pusher.subscribe("private-koel");
this.channel.bind("pusher:subscription_succeeded", () => {
return resolve();
});
this.channel.bind("pusher:subscription_error", () => {
return resolve();
});
});
},
/**
@ -40,9 +40,10 @@ export const socket = {
* @param {Object} data The event's 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;
},
/**
@ -51,8 +52,11 @@ export const socket = {
* @param {Function} 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 { event } from '@/utils'
import router from '@/router'
import { http } from ".";
import { event } from "@/utils";
import router from "@/router";
export const youtube = {
/**
@ -10,19 +10,21 @@ export const youtube = {
*/
searchVideosRelatedToSong(song) {
if (!song.youtube) {
song.youtube = {}
song.youtube = {};
}
const pageToken = song.youtube.nextPageToken || ''
const pageToken = song.youtube.nextPageToken || "";
return new Promise((resolve, reject) => {
http.get(`youtube/search/song/${song.id}?pageToken=${pageToken}`,
http.get(
`youtube/search/song/${song.id}?pageToken=${pageToken}`,
({ data: { nextPageToken, items } }) => {
song.youtube.nextPageToken = nextPageToken
song.youtube.items.push(...items)
resolve()
}, error => reject(error)
)
})
song.youtube.nextPageToken = nextPageToken;
song.youtube.items.push(...items);
resolve();
},
error => reject(error)
);
});
},
/**
@ -31,10 +33,10 @@ export const youtube = {
* @param {Object} vide The video object
*/
play(video) {
event.emit('youtube:play', {
event.emit("youtube:play", {
id: video.id.videoId,
title: video.snippet.title
})
router.go('youtube')
}
});
router.go("youtube");
}
};

View file

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

View file

@ -1,10 +1,10 @@
/*eslint camelcase: ["error", {properties: "never"}]*/
import Vue from 'vue'
import { reduce, each, union, difference, take, filter, orderBy } from 'lodash'
import Vue from "vue";
import { reduce, each, union, difference, take, filter, orderBy } from "lodash";
import stub from '@/stubs/album'
import { artistStore } from '.'
import stub from "@/stubs/album";
import { artistStore } from ".";
export const albumStore = {
stub,
@ -21,22 +21,22 @@ export const albumStore = {
*/
init(albums) {
// Traverse through the artists array and add their albums into our master album list.
this.all = albums
each(this.all, album => this.setupAlbum(album))
this.all = albums;
each(this.all, album => this.setupAlbum(album));
},
setupAlbum(album) {
const artist = artistStore.byId(album.artist_id)
artist.albums = union(artist.albums, [album])
const artist = artistStore.byId(album.artist_id);
artist.albums = union(artist.albums, [album]);
Vue.set(album, 'artist', artist)
Vue.set(album, 'info', null)
Vue.set(album, 'songs', [])
Vue.set(album, 'playCount', 0)
Vue.set(album, "artist", artist);
Vue.set(album, "info", null);
Vue.set(album, "songs", []);
Vue.set(album, "playCount", 0);
this.cache[album.id] = album
this.cache[album.id] = album;
return album
return album;
},
/**
@ -45,7 +45,7 @@ export const albumStore = {
* @return {Array.<Object>}
*/
get all() {
return this.state.albums
return this.state.albums;
},
/**
@ -54,11 +54,11 @@ export const albumStore = {
* @param {Array.<Object>} value
*/
set all(value) {
this.state.albums = value
this.state.albums = value;
},
byId(id) {
return this.cache[id]
return this.cache[id];
},
/**
@ -67,30 +67,34 @@ export const albumStore = {
* @param {Array.<Object>|Object} albums
*/
add(albums) {
albums = [].concat(albums)
albums = [].concat(albums);
each(albums, album => {
this.setupAlbum(album, album.artist)
album.playCount = reduce(album.songs, (count, song) => count + song.playCount, 0)
})
this.setupAlbum(album, album.artist);
album.playCount = reduce(
album.songs,
(count, song) => count + song.playCount,
0
);
});
this.all = union(this.all, albums)
this.all = union(this.all, albums);
},
purify() {
this.compact()
this.compact();
},
/**
* Remove empty albums from the store.
*/
compact() {
const emptyAlbums = filter(this.all, album => album.songs.length === 0)
const emptyAlbums = filter(this.all, album => album.songs.length === 0);
if (!emptyAlbums.length) {
return
return;
}
this.all = difference(this.all, emptyAlbums)
each(emptyAlbums, album => delete this.cache[album.id])
this.all = difference(this.all, emptyAlbums);
each(emptyAlbums, album => delete this.cache[album.id]);
},
/**
@ -102,8 +106,11 @@ export const albumStore = {
*/
getMostPlayed(n = 6) {
// Only non-unknown albums with actually play count are applicable.
const applicable = filter(this.all, album => album.playCount && album.id !== 1)
return take(orderBy(applicable, 'playCount', 'desc'), n)
const applicable = filter(
this.all,
album => album.playCount && album.id !== 1
);
return take(orderBy(applicable, "playCount", "desc"), n);
},
/**
@ -114,7 +121,7 @@ export const albumStore = {
* @return {Array.<Object>}
*/
getRecentlyAdded(n = 6) {
const applicable = filter(this.all, album => album.id !== 1)
return take(orderBy(applicable, 'created_at', 'desc'), n)
}
const applicable = filter(this.all, album => album.id !== 1);
return take(orderBy(applicable, "created_at", "desc"), n);
}
};

View file

@ -1,12 +1,12 @@
/*eslint camelcase: ["error", {properties: "never"}]*/
import Vue from 'vue'
import { reduce, each, union, difference, take, filter, orderBy } from 'lodash'
import Vue from "vue";
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 VARIOUS_ARTISTS_ID = 2
const UNKNOWN_ARTIST_ID = 1;
const VARIOUS_ARTISTS_ID = 2;
export const artistStore = {
stub,
@ -22,10 +22,10 @@ export const artistStore = {
* @param {Array.<Object>} artists The array of artists we got from the server.
*/
init(artists) {
this.all = artists
this.all = artists;
// 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));
},
/**
@ -34,14 +34,14 @@ export const artistStore = {
* @param {Object} artist
*/
setupArtist(artist) {
Vue.set(artist, 'playCount', 0)
Vue.set(artist, 'info', null)
Vue.set(artist, 'albums', [])
Vue.set(artist, 'songs', [])
Vue.set(artist, "playCount", 0);
Vue.set(artist, "info", null);
Vue.set(artist, "albums", []);
Vue.set(artist, "songs", []);
this.cache[artist.id] = artist
this.cache[artist.id] = artist;
return artist
return artist;
},
/**
@ -50,7 +50,7 @@ export const artistStore = {
* @return {Array.<Object>}
*/
get all() {
return this.state.artists
return this.state.artists;
},
/**
@ -59,7 +59,7 @@ export const artistStore = {
* @param {Array.<Object>} value
*/
set all(value) {
this.state.artists = value
this.state.artists = value;
},
/**
@ -68,7 +68,7 @@ export const artistStore = {
* @param {Number} id
*/
byId(id) {
return this.cache[id]
return this.cache[id];
},
/**
@ -77,30 +77,34 @@ export const artistStore = {
* @param {Array.<Object>|Object} artists
*/
add(artists) {
artists = [].concat(artists)
artists = [].concat(artists);
each(artists, artist => {
this.setupArtist(artist)
artist.playCount = reduce(artist.songs, (count, song) => count + song.playCount, 0)
})
this.setupArtist(artist);
artist.playCount = reduce(
artist.songs,
(count, song) => count + song.playCount,
0
);
});
this.all = union(this.all, artists)
this.all = union(this.all, artists);
},
purify() {
this.compact()
this.compact();
},
/**
* Remove empty artists from the store.
*/
compact() {
const emptyArtists = filter(this.all, artist => artist.songs.length === 0)
const emptyArtists = filter(this.all, artist => artist.songs.length === 0);
if (!emptyArtists.length) {
return
return;
}
this.all = difference(this.all, emptyArtists)
each(emptyArtists, artist => delete this.cache[artist.id])
this.all = difference(this.all, emptyArtists);
each(emptyArtists, artist => delete this.cache[artist.id]);
},
/**
@ -111,7 +115,7 @@ export const artistStore = {
* @return {Boolean}
*/
isVariousArtists(artist) {
return artist.id === VARIOUS_ARTISTS_ID
return artist.id === VARIOUS_ARTISTS_ID;
},
/**
@ -122,7 +126,7 @@ export const artistStore = {
* @return {Boolean}
*/
isUnknownArtist(artist) {
return artist.id === UNKNOWN_ARTIST_ID
return artist.id === UNKNOWN_ARTIST_ID;
},
/**
@ -133,7 +137,7 @@ export const artistStore = {
* @return {Array.<Object>}
*/
getSongsByArtist(artist) {
return artist.songs
return artist.songs;
},
/**
@ -147,11 +151,13 @@ export const artistStore = {
// Only non-unknown artists with actually play count are applicable.
// Also, "Various Artists" doesn't count.
const applicable = filter(this.all, artist => {
return artist.playCount &&
return (
artist.playCount &&
!this.isUnknownArtist(artist) &&
!this.isVariousArtists(artist)
})
);
});
return take(orderBy(applicable, 'playCount', 'desc'), n)
}
return take(orderBy(applicable, "playCount", "desc"), n);
}
};

View file

@ -1,97 +1,97 @@
import { find } from 'lodash'
import { find } from "lodash";
import { preferenceStore } from '.'
import { preferenceStore } from ".";
export const equalizerStore = {
presets: [
{
id: 0,
name: 'Default',
name: "Default",
preamp: 0,
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
{
id: 1,
name: 'Classical',
name: "Classical",
preamp: -1,
gains: [-1, -1, -1, -1, -1, -1, -7, -7, -7, -9]
},
{
id: 2,
name: 'Club',
name: "Club",
preamp: -6.7,
gains: [-1, -1, 8, 5, 5, 5, 3, -1, -1, -1]
},
{
id: 3,
name: 'Dance',
name: "Dance",
preamp: -4.3,
gains: [9, 7, 2, -1, -1, -5, -7, -7, -1, -1]
},
{
id: 4,
name: 'Full Bass',
name: "Full Bass",
preamp: -7.2,
gains: [-8, 9, 9, 5, 1, -4, -8, -10, -11, -11]
},
{
id: 5,
name: 'Full Treble',
name: "Full Treble",
preamp: -12,
gains: [-9, -9, -9, -4, 2, 11, 16, 16, 16, 16]
},
{
id: 6,
name: 'Headphone',
name: "Headphone",
preamp: -8,
gains: [4, 11, 5, -3, -2, 1, 4, 9, 12, 14]
},
{
id: 7,
name: 'Large Hall',
name: "Large Hall",
preamp: -7.2,
gains: [10, 10, 5, 5, -1, -4, -4, -4, -1, -1]
},
{
id: 8,
name: 'Live',
name: "Live",
preamp: -5.3,
gains: [-4, -1, 4, 5, 5, 5, 4, 2, 2, 2]
},
{
id: 9,
name: 'Pop',
name: "Pop",
preamp: -6.2,
gains: [-1, 4, 7, 8, 5, -1, -2, -2, -1, -1]
},
{
id: 10,
name: 'Reggae',
name: "Reggae",
preamp: -8.2,
gains: [-1, -1, -1, -5, -1, 6, 6, -1, -1, -1]
},
{
id: 11,
name: 'Rock',
name: "Rock",
preamp: -10,
gains: [8, 4, -5, -8, -3, 4, 8, 11, 11, 11]
},
{
id: 12,
name: 'Soft Rock',
name: "Soft Rock",
preamp: -5.3,
gains: [4, 4, 2, -1, -4, -5, -3, -1, 2, 8]
},
{
id: 13,
name: 'Techno',
name: "Techno",
preamp: -7.7,
gains: [8, 5, -1, -5, -4, -1, 8, 9, 9, 8]
}
],
getPresetById(id) {
return find(this.presets, { id })
return find(this.presets, { id });
},
/**
@ -101,11 +101,11 @@ export const equalizerStore = {
*/
get() {
if (!this.presets[preferenceStore.selectedPreset]) {
return preferenceStore.equalizer
return preferenceStore.equalizer;
}
// If the user chose a preset (instead of customizing one), just return it.
return this.getPresetById(preferenceStore.selectedPreset)
return this.getPresetById(preferenceStore.selectedPreset);
},
/**
@ -115,6 +115,6 @@ export const equalizerStore = {
* @param {Array.<Number>} gains The band's gain value (dB)
*/
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 NProgress from 'nprogress'
import { each, map, difference, union } from "lodash";
import NProgress from "nprogress";
import { http } from '@/services'
import { alerts, pluralize } from '@/utils'
import { http } from "@/services";
import { alerts, pluralize } from "@/utils";
export const favoriteStore = {
state: {
songs: [],
length: 0,
fmtLength: ''
fmtLength: ""
},
/**
@ -17,7 +17,7 @@ export const favoriteStore = {
* @return {Array.<Object>}
*/
get all() {
return this.state.songs
return this.state.songs;
},
/**
@ -26,7 +26,7 @@ export const favoriteStore = {
* @param {Array.<Object>} value
*/
set all(value) {
this.state.songs = value
this.state.songs = value;
},
/**
@ -38,17 +38,22 @@ export const favoriteStore = {
toggleOne(song) {
// Don't wait for the HTTP response to update the status, just toggle right away.
// This may cause a minor problem if the request fails somehow, but do we care?
song.liked = !song.liked
song.liked ? this.add(song) : this.remove(song)
song.liked = !song.liked;
song.liked ? this.add(song) : this.remove(song);
NProgress.start()
NProgress.start();
return new Promise((resolve, reject) => {
http.post('interaction/like', { song: song.id }, ({ data }) => {
http.post(
"interaction/like",
{ song: song.id },
({ data }) => {
// We don't really need to notify just for one song.
resolve(data)
}, error => reject(error))
})
resolve(data);
},
error => reject(error)
);
});
},
/**
@ -57,7 +62,7 @@ export const favoriteStore = {
* @param {Array.<Object>|Object} songs
*/
add(songs) {
this.all = union(this.all, [].concat(songs))
this.all = union(this.all, [].concat(songs));
},
/**
@ -66,14 +71,14 @@ export const favoriteStore = {
* @param {Array.<Object>|Object} songs
*/
remove(songs) {
this.all = difference(this.all, [].concat(songs))
this.all = difference(this.all, [].concat(songs));
},
/**
* Remove all favorites.
*/
clear() {
this.all = []
this.all = [];
},
/**
@ -85,18 +90,25 @@ export const favoriteStore = {
// Don't wait for the HTTP response to update the status, just set them to Liked right away.
// This may cause a minor problem if the request fails somehow, but do we care?
each(songs, song => {
song.liked = true
})
this.add(songs)
song.liked = true;
});
this.add(songs);
NProgress.start()
NProgress.start();
return new Promise((resolve, reject) => {
http.post('interaction/batch/like', { songs: map(songs, 'id') }, ({ data }) => {
alerts.success(`Added ${pluralize(songs.length, 'song')} into Favorites.`)
resolve(data)
}, error => reject(error))
})
http.post(
"interaction/batch/like",
{ songs: map(songs, "id") },
({ data }) => {
alerts.success(
`Added ${pluralize(songs.length, "song")} into Favorites.`
);
resolve(data);
},
error => reject(error)
);
});
},
/**
@ -106,17 +118,24 @@ export const favoriteStore = {
*/
unlike(songs) {
each(songs, song => {
song.liked = false
})
this.remove(songs)
song.liked = false;
});
this.remove(songs);
NProgress.start()
NProgress.start();
return new Promise((resolve, reject) => {
http.post('interaction/batch/unlike', { songs: map(songs, 'id') }, ({ data }) => {
alerts.success(`Removed ${pluralize(songs.length, 'song')} from Favorites.`)
resolve(data)
}, error => reject(error))
})
}
http.post(
"interaction/batch/unlike",
{ songs: map(songs, "id") },
({ 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 './artist'
export * from './equalizer'
export * from './favorite'
export * from './playlist'
export * from './preference'
export * from './queue'
export * from './setting'
export * from './shared'
export * from './song'
export * from './user'
export * from "./album";
export * from "./artist";
export * from "./equalizer";
export * from "./favorite";
export * from "./playlist";
export * from "./preference";
export * from "./queue";
export * from "./setting";
export * from "./shared";
export * from "./song";
export * from "./user";

View file

@ -1,10 +1,10 @@
import { each, find, map, difference, union } from 'lodash'
import NProgress from 'nprogress'
import { each, find, map, difference, union } from "lodash";
import NProgress from "nprogress";
import stub from '@/stubs/playlist'
import { http } from '@/services'
import { alerts, pluralize } from '@/utils'
import { songStore } from '.'
import stub from "@/stubs/playlist";
import { http } from "@/services";
import { alerts, pluralize } from "@/utils";
import { songStore } from ".";
export const playlistStore = {
stub,
@ -14,8 +14,8 @@ export const playlistStore = {
},
init(playlists) {
this.all = playlists
each(this.all, this.objectifySongs)
this.all = playlists;
each(this.all, this.objectifySongs);
},
/**
@ -24,7 +24,7 @@ export const playlistStore = {
* @return {Array.<Object>}
*/
get all() {
return this.state.playlists
return this.state.playlists;
},
/**
@ -33,7 +33,7 @@ export const playlistStore = {
* @param {Array.<Object>} value
*/
set all(value) {
this.state.playlists = value
this.state.playlists = value;
},
/**
@ -44,7 +44,7 @@ export const playlistStore = {
* @return {Object}
*/
byId(id) {
return find(this.all, { id })
return find(this.all, { id });
},
/**
@ -54,7 +54,7 @@ export const playlistStore = {
* @param {Object} playlist
*/
objectifySongs(playlist) {
playlist.songs = songStore.byIds(playlist.songs)
playlist.songs = songStore.byIds(playlist.songs);
},
/**
@ -65,7 +65,7 @@ export const playlistStore = {
* return {Array.<Object>}
*/
getSongs(playlist) {
return playlist.songs
return playlist.songs;
},
/**
@ -74,7 +74,7 @@ export const playlistStore = {
* @param {Array.<Object>|Object} playlists
*/
add(playlists) {
this.all = union(this.all, [].concat(playlists))
this.all = union(this.all, [].concat(playlists));
},
/**
@ -83,7 +83,7 @@ export const playlistStore = {
* @param {Array.<Object>|Object} playlist
*/
remove(playlists) {
this.all = difference(this.all, [].concat(playlists))
this.all = difference(this.all, [].concat(playlists));
},
/**
@ -95,20 +95,25 @@ export const playlistStore = {
store(name, songs = []) {
if (songs.length) {
// Extract the IDs from the song objects.
songs = map(songs, 'id')
songs = map(songs, "id");
}
NProgress.start()
NProgress.start();
return new Promise((resolve, reject) => {
http.post('playlist', { name, songs }, ({ data: playlist }) => {
playlist.songs = songs
this.objectifySongs(playlist)
this.add(playlist)
alerts.success(`Created playlist &quot;${playlist.name}&quot;.`)
resolve(playlist)
}, error => reject(error))
})
http.post(
"playlist",
{ name, songs },
({ data: playlist }) => {
playlist.songs = songs;
this.objectifySongs(playlist);
this.add(playlist);
alerts.success(`Created playlist &quot;${playlist.name}&quot;.`);
resolve(playlist);
},
error => reject(error)
);
});
},
/**
@ -117,15 +122,20 @@ export const playlistStore = {
* @param {Object} playlist
*/
delete(playlist) {
NProgress.start()
NProgress.start();
return new Promise((resolve, reject) => {
http.delete(`playlist/${playlist.id}`, {}, ({ data }) => {
this.remove(playlist)
alerts.success(`Deleted playlist &quot;${playlist.name}&quot;.`)
resolve(data)
}, error => reject(error))
})
http.delete(
`playlist/${playlist.id}`,
{},
({ data }) => {
this.remove(playlist);
alerts.success(`Deleted playlist &quot;${playlist.name}&quot;.`);
resolve(data);
},
error => reject(error)
);
});
},
/**
@ -136,21 +146,31 @@ export const playlistStore = {
*/
addSongs(playlist, songs) {
return new Promise((resolve, reject) => {
const count = playlist.songs.length
playlist.songs = union(playlist.songs, songs)
const count = playlist.songs.length;
playlist.songs = union(playlist.songs, songs);
if (count === playlist.songs.length) {
resolve(playlist)
return
resolve(playlist);
return;
}
NProgress.start()
NProgress.start();
http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') }, () => {
alerts.success(`Added ${pluralize(songs.length, 'song')} into &quot;${playlist.name}&quot;.`)
resolve(playlist)
}, error => reject(error))
})
http.put(
`playlist/${playlist.id}/sync`,
{ songs: map(playlist.songs, "id") },
() => {
alerts.success(
`Added ${pluralize(
songs.length,
"song"
)} into &quot;${playlist.name}&quot;.`
);
resolve(playlist);
},
error => reject(error)
);
});
},
/**
@ -160,16 +180,26 @@ export const playlistStore = {
* @param {Array.<Object>} 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) => {
http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') }, () => {
alerts.success(`Removed ${pluralize(songs.length, 'song')} from &quot;${playlist.name}&quot;.`)
resolve(playlist)
}, error => reject(error))
})
http.put(
`playlist/${playlist.id}/sync`,
{ songs: map(playlist.songs, "id") },
() => {
alerts.success(
`Removed ${pluralize(
songs.length,
"song"
)} from &quot;${playlist.name}&quot;.`
);
resolve(playlist);
},
error => reject(error)
);
});
},
/**
@ -178,7 +208,7 @@ export const playlistStore = {
* @param {Object} playlist
*/
update(playlist) {
NProgress.start()
NProgress.start();
return new Promise((resolve, reject) => {
http.put(
@ -186,7 +216,7 @@ export const playlistStore = {
{ name: playlist.name },
() => resolve(playlist),
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 { ls } from '@/services'
import { userStore } from ".";
import { ls } from "@/services";
export const preferenceStore = {
storeKey: '',
storeKey: "",
state: {
volume: 7,
notify: true,
repeatMode: 'NO_REPEAT',
repeatMode: "NO_REPEAT",
showExtraPanel: true,
confirmClosing: false,
equalizer: {
@ -29,12 +29,12 @@ export const preferenceStore = {
*/
init(user = null) {
if (!user) {
user = userStore.current
user = userStore.current;
}
this.storeKey = `preferences_${user.id}`
extend(this.state, ls.get(this.storeKey, this.state))
this.setupProxy()
this.storeKey = `preferences_${user.id}`;
extend(this.state, ls.get(this.storeKey, this.state));
this.setupProxy();
},
/**
@ -46,20 +46,20 @@ export const preferenceStore = {
get: () => this.get(key),
set: value => this.set(key, value),
configurable: true
})
})
});
});
},
set(key, val) {
this.state[key] = val
this.save()
this.state[key] = val;
this.save();
},
get(key) {
return has(this.state, key) ? this.state[key] : null
return has(this.state, key) ? this.state[key] : null;
},
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 = {
state: {
@ -37,7 +47,7 @@ export const queueStore = {
* @return {Array.<Object>}
*/
get all() {
return this.state.songs
return this.state.songs;
},
/**
@ -46,7 +56,7 @@ export const queueStore = {
* @param {Array.<Object>}
*/
set all(songs) {
this.state.songs = songs
this.state.songs = songs;
},
/**
@ -55,7 +65,7 @@ export const queueStore = {
* @return {?Object}
*/
get first() {
return head(this.all)
return head(this.all);
},
/**
@ -64,7 +74,7 @@ export const queueStore = {
* @return {?Object}
*/
get last() {
return last(this.all)
return last(this.all);
},
/**
@ -75,7 +85,7 @@ export const queueStore = {
* @return {Boolean}
*/
contains(song) {
return includes(this.all, song)
return includes(this.all, song);
},
/**
@ -87,12 +97,12 @@ export const queueStore = {
* @param {Boolean} toTop Whether to prepend or append to the queue
*/
queue(songs, replace = false, toTop = false) {
songs = [].concat(songs)
songs = [].concat(songs);
if (replace) {
this.all = songs
this.all = songs;
} else {
this.all = toTop ? union(songs, this.all) : union(this.all, songs)
this.all = toTop ? union(songs, this.all) : union(this.all, songs);
}
},
@ -102,17 +112,17 @@ export const queueStore = {
* @param {Array.<Object>|Object} songs
*/
queueAfterCurrent(songs) {
songs = [].concat(songs)
songs = [].concat(songs);
if (!this.current || !this.all.length) {
return this.queue(songs)
return this.queue(songs);
}
// First we unqueue the songs to make sure there are no duplicates.
this.unqueue(songs)
this.unqueue(songs);
const head = this.all.splice(0, this.indexOf(this.current) + 1)
this.all = head.concat(songs, this.all)
const head = this.all.splice(0, this.indexOf(this.current) + 1);
this.all = head.concat(songs, this.all);
},
/**
@ -121,7 +131,7 @@ export const queueStore = {
* @param {Object|String|Array.<Object>} songs The song(s) to unqueue
*/
unqueue(songs) {
this.all = difference(this.all, [].concat(songs))
this.all = difference(this.all, [].concat(songs));
},
/**
@ -131,19 +141,19 @@ export const queueStore = {
* @param {Object} target The target song object
*/
move(songs, target) {
const $targetIndex = this.indexOf(target)
const $targetIndex = this.indexOf(target);
each(songs, song => {
this.all.splice(this.indexOf(song), 1)
this.all.splice($targetIndex, 0, song)
})
this.all.splice(this.indexOf(song), 1);
this.all.splice($targetIndex, 0, song);
});
},
/**
* Clear the current queue.
*/
clear() {
this.all = []
this.all = [];
},
/**
@ -154,7 +164,7 @@ export const queueStore = {
* @return {?Integer}
*/
indexOf(song) {
return this.all.indexOf(song)
return this.all.indexOf(song);
},
/**
@ -164,12 +174,12 @@ export const queueStore = {
*/
get next() {
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];
},
/**
@ -179,12 +189,12 @@ export const queueStore = {
*/
get previous() {
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];
},
/**
@ -193,7 +203,7 @@ export const queueStore = {
* @return {Object}
*/
get current() {
return this.state.current
return this.state.current;
},
/**
@ -204,8 +214,8 @@ export const queueStore = {
* @return {Object} The queued song.
*/
set current(song) {
this.state.current = song
return this.state.current
this.state.current = song;
return this.state.current;
},
/**
@ -214,7 +224,7 @@ export const queueStore = {
* @return {Array.<Object>} The shuffled array of song objects
*/
shuffle() {
this.all = _shuffle(this.all)
return this.all
}
this.all = _shuffle(this.all);
return this.all;
}
};

View file

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

View file

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

View file

@ -1,12 +1,28 @@
import Vue from 'vue'
import slugify from 'slugify'
import { assign, without, map, take, remove, orderBy, each, unionBy, compact } from 'lodash'
import isMobile from 'ismobilejs'
import Vue from "vue";
import slugify from "slugify";
import {
assign,
without,
map,
take,
remove,
orderBy,
each,
unionBy,
compact
} from "lodash";
import isMobile from "ismobilejs";
import { secondsToHis, alerts, pluralize } from '@/utils'
import { http, ls } from '@/services'
import { sharedStore, favoriteStore, albumStore, artistStore, preferenceStore } from '.'
import stub from '@/stubs/song'
import { secondsToHis, alerts, pluralize } from "@/utils";
import { http, ls } from "@/services";
import {
sharedStore,
favoriteStore,
albumStore,
artistStore,
preferenceStore
} from ".";
import stub from "@/stubs/song";
export const songStore = {
stub,
@ -35,38 +51,38 @@ export const songStore = {
* @param {Array.<Object>} songs The array of song objects
*/
init(songs) {
this.all = songs
each(this.all, song => this.setupSong(song))
this.state.recentlyPlayed = this.gatherRecentlyPlayedFromLocalStorage()
this.all = songs;
each(this.all, song => this.setupSong(song));
this.state.recentlyPlayed = this.gatherRecentlyPlayedFromLocalStorage();
},
setupSong(song) {
song.fmtLength = secondsToHis(song.length)
song.fmtLength = secondsToHis(song.length);
const album = albumStore.byId(song.album_id)
const artist = artistStore.byId(song.artist_id)
const album = albumStore.byId(song.album_id);
const artist = artistStore.byId(song.artist_id);
// Manually set these additional properties to be reactive
Vue.set(song, 'playCount', song.playCount || 0)
Vue.set(song, 'album', album)
Vue.set(song, 'artist', artist)
Vue.set(song, 'liked', song.liked || false)
Vue.set(song, 'lyrics', song.lyrics || null)
Vue.set(song, 'playbackState', song.playbackState || 'stopped')
Vue.set(song, "playCount", song.playCount || 0);
Vue.set(song, "album", album);
Vue.set(song, "artist", artist);
Vue.set(song, "liked", song.liked || false);
Vue.set(song, "lyrics", song.lyrics || null);
Vue.set(song, "playbackState", song.playbackState || "stopped");
artist.songs = unionBy(artist.songs || [], [song], 'id')
album.songs = unionBy(album.songs || [], [song], 'id')
artist.songs = unionBy(artist.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
// into its artist as well
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
this.cache[song.id] = song
this.cache[song.id] = song;
return song
return song;
},
/**
@ -75,22 +91,22 @@ export const songStore = {
* @param {Array.<Object>} interactions The array of interactions of the current user
*/
initInteractions(interactions) {
favoriteStore.clear()
favoriteStore.clear();
each(interactions, interaction => {
const song = this.byId(interaction.song_id)
const song = this.byId(interaction.song_id);
if (!song) {
return
return;
}
song.liked = interaction.liked
song.playCount = interaction.play_count
song.album.playCount += song.playCount
song.artist.playCount += song.playCount
song.liked = interaction.liked;
song.playCount = interaction.play_count;
song.album.playCount += song.playCount;
song.artist.playCount += song.playCount;
song.liked && favoriteStore.add(song)
})
song.liked && favoriteStore.add(song);
});
},
/**
@ -102,9 +118,9 @@ export const songStore = {
* @return {Float|String}
*/
getLength(songs, toHis) {
const duration = songs.reduce((length, song) => length + song.length, 0)
const duration = songs.reduce((length, song) => length + song.length, 0);
return toHis ? secondsToHis(duration) : duration
return toHis ? secondsToHis(duration) : duration;
},
/**
@ -113,7 +129,7 @@ export const songStore = {
* @return {Array.<Object>}
*/
get all() {
return this.state.songs
return this.state.songs;
},
/**
@ -122,7 +138,7 @@ export const songStore = {
* @param {Array.<Object>} value
*/
set all(value) {
this.state.songs = value
this.state.songs = value;
},
/**
@ -133,7 +149,7 @@ export const songStore = {
* @return {Object}
*/
byId(id) {
return this.cache[id]
return this.cache[id];
},
/**
@ -144,7 +160,7 @@ export const songStore = {
* @return {Array.<Object>}
*/
byIds(ids) {
return ids.map(id => this.byId(id))
return ids.map(id => this.byId(id));
},
/**
@ -157,15 +173,15 @@ export const songStore = {
* @return {Object|false}
*/
guess(title, album) {
title = slugify(title.toLowerCase())
let found = false
title = slugify(title.toLowerCase());
let found = false;
each(album.songs, song => {
if (slugify(song.title.toLowerCase()) === title) {
found = song
found = song;
}
})
});
return found
return found;
},
/**
@ -175,17 +191,22 @@ export const songStore = {
*/
registerPlay(song) {
return new Promise((resolve, reject) => {
const oldCount = song.playCount
const oldCount = song.playCount;
http.post('interaction/play', { song: song.id }, ({ data }) => {
http.post(
"interaction/play",
{ song: song.id },
({ data }) => {
// Use the data from the server to make sure we don't miss a play from another device.
song.playCount = data.play_count
song.album.playCount += song.playCount - oldCount
song.artist.playCount += song.playCount - oldCount
song.playCount = data.play_count;
song.album.playCount += song.playCount - oldCount;
song.artist.playCount += song.playCount - oldCount;
resolve(data)
}, error => reject(error))
})
resolve(data);
},
error => reject(error)
);
});
},
/**
@ -194,14 +215,14 @@ export const songStore = {
* @param {Object} 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.
this.state.recentlyPlayed.unshift(song)
this.state.recentlyPlayed.unshift(song);
// Only take first 7 songs
this.state.recentlyPlayed.splice(7)
this.state.recentlyPlayed.splice(7);
// Save to local storage as well
preferenceStore.set('recent-songs', map(this.state.recentlyPlayed, 'id'))
preferenceStore.set("recent-songs", map(this.state.recentlyPlayed, "id"));
},
/**
@ -211,10 +232,15 @@ export const songStore = {
*/
scrobble(song) {
return new Promise((resolve, reject) => {
http.post(`${song.id}/scrobble/${song.playStartTime}`, {}, ({ data }) => {
resolve(data)
}, error => reject(error))
})
http.post(
`${song.id}/scrobble/${song.playStartTime}`,
{},
({ data }) => {
resolve(data);
},
error => reject(error)
);
});
},
/**
@ -225,39 +251,56 @@ export const songStore = {
*/
update(songs, data) {
return new Promise((resolve, reject) => {
http.put('songs', {
http.put(
"songs",
{
data,
songs: map(songs, 'id')
}, ({ data: { songs, artists, albums }}) => {
songs: map(songs, "id")
},
({ 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(
artists,
artist => !artistStore.byId(artist.id) && artistStore.add(artist)
);
each(
albums,
album => !albumStore.byId(album.id) && albumStore.add(album)
);
each(songs, song => {
const originalSong = this.byId(song.id)
const originalSong = this.byId(song.id);
if (originalSong.album_id !== song.album_id) {
// 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) {
// 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
this.setupSong(originalSong)
})
this.setupSong(originalSong);
});
artistStore.compact()
albumStore.compact()
artistStore.compact();
albumStore.compact();
alerts.success(`Updated ${pluralize(songs.length, 'song')}.`)
resolve(songs)
}, error => reject(error))
})
alerts.success(`Updated ${pluralize(songs.length, "song")}.`);
resolve(songs);
},
error => reject(error)
);
});
},
/**
@ -269,9 +312,12 @@ export const songStore = {
*/
getSourceUrl(song) {
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"
)}`;
},
/**
@ -283,7 +329,7 @@ export const songStore = {
* @return {string}
*/
getShareableUrl(song) {
return `${window.location.origin}/#!/song/${song.id}`
return `${window.location.origin}/#!/song/${song.id}`;
},
/**
@ -291,7 +337,7 @@ export const songStore = {
* @return {Array.<Object>}
*/
get recentlyPlayed() {
return this.state.recentlyPlayed
return this.state.recentlyPlayed;
},
/**
@ -299,7 +345,7 @@ export const songStore = {
* @return {Array.<Object>}
*/
gatherRecentlyPlayedFromLocalStorage() {
return compact(this.byIds(preferenceStore.get('recent-songs') || []))
return compact(this.byIds(preferenceStore.get("recent-songs") || []));
},
/**
@ -310,12 +356,12 @@ export const songStore = {
* @return {Array.<Object>}
*/
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(songs, song => !song.playCount)
remove(songs, song => !song.playCount);
return songs
return songs;
},
/**
@ -324,7 +370,7 @@ export const songStore = {
* @return {Array.<Object>}
*/
getRecentlyAdded(n = 10) {
return take(orderBy(this.all, 'created_at', 'desc'), n)
return take(orderBy(this.all, "created_at", "desc"), n);
},
/**
@ -347,6 +393,6 @@ export const songStore = {
name: song.artist.name
}
}
};
}
}
}
};

View file

@ -1,11 +1,11 @@
import { each, find, without } from 'lodash'
import md5 from 'blueimp-md5'
import Vue from 'vue'
import NProgress from 'nprogress'
import { each, find, without } from "lodash";
import md5 from "blueimp-md5";
import Vue from "vue";
import NProgress from "nprogress";
import { http } from '@/services'
import { alerts } from '@/utils'
import stub from '@/stubs/user'
import { http } from "@/services";
import { alerts } from "@/utils";
import stub from "@/stubs/user";
export const userStore = {
stub,
@ -22,14 +22,14 @@ export const userStore = {
* @param {Object} currentUser The current user.
*/
init(users, currentUser) {
this.all = users
this.current = currentUser
this.all = users;
this.current = currentUser;
// Set the avatar for each of the users…
each(this.all, this.setAvatar)
each(this.all, this.setAvatar);
// …and the current user as well.
this.setAvatar()
this.setAvatar();
},
/**
@ -38,7 +38,7 @@ export const userStore = {
* @return {Array.<Object>}
*/
get all() {
return this.state.users
return this.state.users;
},
/**
@ -47,7 +47,7 @@ export const userStore = {
* @param {Array.<Object>} value
*/
set all(value) {
this.state.users = value
this.state.users = value;
},
/**
@ -58,7 +58,7 @@ export const userStore = {
* @return {Object}
*/
byId(id) {
return find(this.all, { id })
return find(this.all, { id });
},
/**
@ -67,7 +67,7 @@ export const userStore = {
* @return {Object}
*/
get current() {
return this.state.current
return this.state.current;
},
/**
@ -78,8 +78,8 @@ export const userStore = {
* @return {Object}
*/
set current(user) {
this.state.current = user
return this.state.current
this.state.current = user;
return this.state.current;
},
/**
@ -89,10 +89,14 @@ export const userStore = {
*/
setAvatar(user = null) {
if (!user) {
user = this.current
user = this.current;
}
Vue.set(user, 'avatar', `https://www.gravatar.com/avatar/${md5(user.email)}?s=256`)
Vue.set(
user,
"avatar",
`https://www.gravatar.com/avatar/${md5(user.email)}?s=256`
);
},
/**
@ -102,13 +106,18 @@ export const userStore = {
* @param {String} password
*/
login(email, password) {
NProgress.start()
NProgress.start();
return new Promise((resolve, reject) => {
http.post('me', { email, password }, ({ data }) => {
resolve(data)
}, error => reject(error))
})
http.post(
"me",
{ email, password },
({ data }) => {
resolve(data);
},
error => reject(error)
);
});
},
/**
@ -116,10 +125,15 @@ export const userStore = {
*/
logout() {
return new Promise((resolve, reject) => {
http.delete('me', {}, ({ data }) => {
resolve(data)
}, error => reject(error))
})
http.delete(
"me",
{},
({ data }) => {
resolve(data);
},
error => reject(error)
);
});
},
/**
@ -127,10 +141,14 @@ export const userStore = {
*/
getProfile() {
return new Promise((resolve, reject) => {
http.get('me', ({ data }) => {
resolve(data)
}, error => reject(error))
})
http.get(
"me",
({ data }) => {
resolve(data);
},
error => reject(error)
);
});
},
/**
@ -139,20 +157,24 @@ export const userStore = {
* @param {string} password Can be an empty string if the user is not changing his password.
*/
updateProfile(password) {
NProgress.start()
NProgress.start();
return new Promise((resolve, reject) => {
http.put('me', {
http.put(
"me",
{
password,
name: this.current.name,
email: this.current.email
}, () => {
this.setAvatar()
alerts.success('Profile updated.')
resolve(this.current)
},
error => reject(error))
})
() => {
this.setAvatar();
alerts.success("Profile updated.");
resolve(this.current);
},
error => reject(error)
);
});
},
/**
@ -163,16 +185,21 @@ export const userStore = {
* @param {string} password
*/
store(name, email, password) {
NProgress.start()
NProgress.start();
return new Promise((resolve, reject) => {
http.post('user', { name, email, password }, ({ data: user }) => {
this.setAvatar(user)
this.all.unshift(user)
alerts.success(`New user &quot;${name}&quot; created.`)
resolve(user)
}, error => reject(error))
})
http.post(
"user",
{ name, email, password },
({ data: user }) => {
this.setAvatar(user);
this.all.unshift(user);
alerts.success(`New user &quot;${name}&quot; created.`);
resolve(user);
},
error => reject(error)
);
});
},
/**
@ -184,16 +211,21 @@ export const userStore = {
* @param {String} password
*/
update(user, name, email, password) {
NProgress.start()
NProgress.start();
return new Promise((resolve, reject) => {
http.put(`user/${user.id}`, { name, email, password }, () => {
http.put(
`user/${user.id}`,
{ name, email, password },
() => {
this.setAvatar(user);
[user.name, user.email, user.password] = [name, email, '']
alerts.success('User profile updated.')
resolve(user)
}, error => reject(error))
})
[user.name, user.email, user.password] = [name, email, ""];
alerts.success("User profile updated.");
resolve(user);
},
error => reject(error)
);
});
},
/**
@ -202,12 +234,15 @@ export const userStore = {
* @param {Object} user
*/
destroy(user) {
NProgress.start()
NProgress.start();
return new Promise((resolve, reject) => {
http.delete(`user/${user.id}`, {}, ({ data }) => {
this.all = without(this.all, user)
alerts.success(`User &quot;${user.name}&quot; deleted.`)
http.delete(
`user/${user.id}`,
{},
({ data }) => {
this.all = without(this.all, user);
alerts.success(`User &quot;${user.name}&quot; deleted.`);
// Mama, just killed a man
// Put a gun against his head
@ -231,8 +266,10 @@ export const userStore = {
/**
* Brian May enters the stage.
*/
resolve(data)
}, error => reject(error))
})
}
resolve(data);
},
error => reject(error)
);
});
}
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,13 @@
import { jsdom } from 'jsdom'
import { jsdom } from "jsdom";
const doc = jsdom('<!doctype html><html><body></body></html>')
const win = doc.defaultView
const doc = jsdom("<!doctype html><html><body></body></html>");
const win = doc.defaultView;
global.document = doc
global.window = win
global.document = doc;
global.window = win;
Object.keys(window).forEach((key) => {
Object.keys(window).forEach(key => {
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 { ls } from '../../services'
import localStorage from "local-storage";
import { ls } from "../../services";
describe('services/ls', () => {
beforeEach(() => localStorage.remove('foo'))
describe("services/ls", () => {
beforeEach(() => localStorage.remove("foo"));
describe('#get', () => {
it('correctly gets an existing item from local storage', () => {
localStorage('foo', 'bar')
ls.get('foo').should.equal('bar')
})
describe("#get", () => {
it("correctly gets an existing item from local storage", () => {
localStorage("foo", "bar");
ls.get("foo").should.equal("bar");
});
it('correctly returns the default value for a non exising item', () => {
ls.get('baz', 'qux').should.equal('qux')
})
})
it("correctly returns the default value for a non exising item", () => {
ls.get("baz", "qux").should.equal("qux");
});
});
describe('#set', () => {
it('correctly sets an item into local storage', () => {
ls.set('foo', 'bar')
localStorage('foo').should.equal('bar')
})
})
describe("#set", () => {
it("correctly sets an item into local storage", () => {
ls.set("foo", "bar");
localStorage("foo").should.equal("bar");
});
});
describe('#remove', () => {
it('correctly removes an item from local storage', () => {
localStorage('foo', 'bar')
ls.remove('foo')
var result = localStorage('foo') === null
result.should.be.true
})
})
})
describe("#remove", () => {
it("correctly removes an item from local storage", () => {
localStorage("foo", "bar");
ls.remove("foo");
var result = localStorage("foo") === null;
result.should.be.true;
});
});
});

View file

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

View file

@ -1,32 +1,32 @@
require('chai').should()
import { cloneDeep, last } from 'lodash'
require("chai").should();
import { cloneDeep, last } from "lodash";
import { artistStore } from '../../stores'
import data from '../blobs/data'
const artists = data.artists
import { artistStore } from "../../stores";
import data from "../blobs/data";
const artists = data.artists;
describe('stores/artist', () => {
beforeEach(() => artistStore.init(cloneDeep(artists)))
afterEach(() => artistStore.state.artists = [])
describe("stores/artist", () => {
beforeEach(() => artistStore.init(cloneDeep(artists)));
afterEach(() => (artistStore.state.artists = []));
describe('#init', () => {
it('correctly gathers artists', () => {
artistStore.state.artists.length.should.equal(5)
})
})
describe("#init", () => {
it("correctly gathers artists", () => {
artistStore.state.artists.length.should.equal(5);
});
});
describe('#byId', () => {
it('correctly gets an artist by ID', () => {
artistStore.byId(3).name.should.equal('All-4-One')
})
})
describe("#byId", () => {
it("correctly gets an artist by ID", () => {
artistStore.byId(3).name.should.equal("All-4-One");
});
});
describe('#compact', () => {
it('correctly compact artists', () => {
artistStore.compact()
describe("#compact", () => {
it("correctly compact artists", () => {
artistStore.compact();
// because we've not processed songs/albums, all artists here have no songs
// 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 { preferenceStore } from '../../stores'
import localStorage from "local-storage";
import { preferenceStore } from "../../stores";
const user = { id: 0 }
const user = { id: 0 };
const preferences = {
volume: 8,
notify: false
}
};
describe('stores/preference', () => {
describe("stores/preference", () => {
beforeEach(() => {
localStorage.set(`preferences_${user.id}`, preferences)
preferenceStore.init(user)
})
localStorage.set(`preferences_${user.id}`, preferences);
preferenceStore.init(user);
});
describe("#set", () => {
it('correctly sets preferences', () => {
preferenceStore.set('volume', 5)
localStorage.get(`preferences_${user.id}`).volume.should.equal(5)
it("correctly sets preferences", () => {
preferenceStore.set("volume", 5);
localStorage.get(`preferences_${user.id}`).volume.should.equal(5);
// Test the proxy
preferenceStore.volume = 6
localStorage.get(`preferences_${user.id}`).volume.should.equal(6)
})
})
preferenceStore.volume = 6;
localStorage.get(`preferences_${user.id}`).volume.should.equal(6);
});
});
describe("#get", () => {
it('returns correct preference values', () => {
preferenceStore.get('volume').should.equal(8)
it("returns correct preference values", () => {
preferenceStore.get("volume").should.equal(8);
// 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 data from '../blobs/data'
import { queueStore } from "../../stores";
import data from "../blobs/data";
const { songs: allSongs } = data
const { songs: allSongs } = data;
// 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(() => {
queueStore.state.songs = songs
queueStore.state.current = songs[1]
})
queueStore.state.songs = songs;
queueStore.state.current = songs[1];
});
describe('#all', () => {
it('correctly returns all queued songs', () => {
queueStore.all.should.equal(songs)
})
})
describe("#all", () => {
it("correctly returns all queued songs", () => {
queueStore.all.should.equal(songs);
});
});
describe('#first', () => {
it('correctly returns the first queued song', () => {
queueStore.first.title.should.equal('No bravery')
})
})
describe("#first", () => {
it("correctly returns the first queued song", () => {
queueStore.first.title.should.equal("No bravery");
});
});
describe('#last', () => {
it('correctly returns the last queued song', () => {
queueStore.last.title.should.equal('Tears and rain')
})
})
describe("#last", () => {
it("correctly returns the last queued song", () => {
queueStore.last.title.should.equal("Tears and rain");
});
});
describe('#queue', () => {
beforeEach(() => queueStore.state.songs = songs)
describe("#queue", () => {
beforeEach(() => (queueStore.state.songs = songs));
const song = allSongs[0]
const song = allSongs[0];
it('correctly appends a song to end of the queue', () => {
queueStore.queue(song)
queueStore.last.title.should.equal('I Swear')
})
it("correctly appends a song to end of the queue", () => {
queueStore.queue(song);
queueStore.last.title.should.equal("I Swear");
});
it('correctly prepends a song to top of the queue', () => {
queueStore.queue(song, false, true)
queueStore.first.title.should.equal('I Swear')
})
it("correctly prepends a song to top of the queue", () => {
queueStore.queue(song, false, true);
queueStore.first.title.should.equal("I Swear");
});
it('correctly replaces the whole queue', () => {
queueStore.queue(song, true)
queueStore.all.length.should.equal(1)
queueStore.first.title.should.equal('I Swear')
})
})
it("correctly replaces the whole queue", () => {
queueStore.queue(song, true);
queueStore.all.length.should.equal(1);
queueStore.first.title.should.equal("I Swear");
});
});
describe('#unqueue', () => {
beforeEach(() => queueStore.state.songs = songs)
describe("#unqueue", () => {
beforeEach(() => (queueStore.state.songs = songs));
it('correctly removes a song from queue', () => {
queueStore.unqueue(queueStore.state.songs[0])
queueStore.first.title.should.equal('So long, Jimmy'); // Oh the irony.
})
it("correctly removes a song from queue", () => {
queueStore.unqueue(queueStore.state.songs[0]);
queueStore.first.title.should.equal("So long, Jimmy"); // Oh the irony.
});
it('correctly removes mutiple songs from queue', () => {
queueStore.unqueue([queueStore.state.songs[0], queueStore.state.songs[1]])
queueStore.first.title.should.equal('Wisemen')
})
})
it("correctly removes mutiple songs from queue", () => {
queueStore.unqueue([
queueStore.state.songs[0],
queueStore.state.songs[1]
]);
queueStore.first.title.should.equal("Wisemen");
});
});
describe('#clear', () => {
it('correctly clears all songs from queue', () => {
queueStore.clear()
queueStore.state.songs.length.should.equal(0)
})
})
describe("#clear", () => {
it("correctly clears all songs from queue", () => {
queueStore.clear();
queueStore.state.songs.length.should.equal(0);
});
});
describe('#current', () => {
it('returns the correct current song', () => {
queueStore.current.title.should.equal('So long, Jimmy')
})
describe("#current", () => {
it("returns the correct current song", () => {
queueStore.current.title.should.equal("So long, Jimmy");
});
it('successfully sets the current song', () => {
queueStore.current = queueStore.state.songs[0]
queueStore.current.title.should.equal('No bravery')
})
})
it("successfully sets the current song", () => {
queueStore.current = queueStore.state.songs[0];
queueStore.current.title.should.equal("No bravery");
});
});
describe('#getNextSong', () => {
it('correctly gets the next song in queue', () => {
queueStore.next.title.should.equal('Wisemen')
})
describe("#getNextSong", () => {
it("correctly gets the next song in queue", () => {
queueStore.next.title.should.equal("Wisemen");
});
it('correctly returns null if at end of queue', () => {
queueStore.current = queueStore.state.songs[queueStore.state.songs.length - 1]
var result = queueStore.next === null
result.should.be.true
})
})
it("correctly returns null if at end of queue", () => {
queueStore.current =
queueStore.state.songs[queueStore.state.songs.length - 1];
var result = queueStore.next === null;
result.should.be.true;
});
});
describe('#getPrevSong', () => {
it('correctly gets the previous song in queue', () => {
queueStore.previous.title.should.equal('No bravery')
})
describe("#getPrevSong", () => {
it("correctly gets the previous song in queue", () => {
queueStore.previous.title.should.equal("No bravery");
});
it('correctly returns null if at end of queue', () => {
queueStore.current = queueStore.state.songs[0]
var result = queueStore.previous === null
result.should.be.true
})
})
})
it("correctly returns null if at end of queue", () => {
queueStore.current = queueStore.state.songs[0];
var result = queueStore.previous === null;
result.should.be.true;
});
});
});

View file

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

View file

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

View file

@ -9,46 +9,53 @@ export const $ = {
el.msMatchesSelector ||
el.mozMatchesSelector ||
el.webkitMatchesSelector ||
el.oMatchesSelector).call(el, selector)
el.oMatchesSelector
).call(el, selector);
},
addClass(el, className) {
if (!el) {
return
return;
}
if (el.classList) {
el.classList.add(className)
el.classList.add(className);
} else {
el.className += ` ${className}`
el.className += ` ${className}`;
}
},
removeClass(el, className) {
if (!el) {
return
return;
}
if (el.classList) {
el.classList.remove(className)
el.classList.remove(className);
} 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) {
if (duration <= 0 || !el) {
return
return;
}
const difference = to - el.scrollTop
const perTick = difference / duration * 10
const difference = to - el.scrollTop;
const perTick = difference / duration * 10;
window.setTimeout(() => {
el.scrollTop = el.scrollTop + perTick
el.scrollTop = el.scrollTop + perTick;
if (el.scrollTop === to) {
cb && cb()
return
}
this.scrollTo(el, to, duration - 10)
}, 10)
cb && cb();
return;
}
this.scrollTo(el, to, duration - 10);
}, 10);
}
};

View file

@ -1,37 +1,37 @@
import alertify from 'alertify.js'
import alertify from "alertify.js";
const alerts = {
alert(msg) {
alertify.alert(msg)
alertify.alert(msg);
},
confirm(msg, okFunc, cancelFunc = null) {
alertify.confirm(msg, okFunc, cancelFunc)
alertify.confirm(msg, okFunc, cancelFunc);
},
log(msg, type, cb = null) {
alertify.logPosition('top right')
alertify.closeLogOnClick(true)
alertify.logPosition("top right");
alertify.closeLogOnClick(true);
switch (type) {
case 'success':
alertify.success(msg, cb)
break
case 'error':
alertify.error(msg, cb)
break
case "success":
alertify.success(msg, cb);
break;
case "error":
alertify.error(msg, cb);
break;
default:
alertify.log(msg, cb)
break
alertify.log(msg, cb);
break;
}
},
success(msg, cb = null) {
return this.log(msg, 'success', cb)
return this.log(msg, "success", cb);
},
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.
*/
import select from 'select'
import { event } from '@/utils'
import select from "select";
import { event } from "@/utils";
/**
* Load (display) a main panel (view).
@ -11,7 +11,7 @@ import { event } from '@/utils'
* @param {...*} Extra data to attach to the view.
*/
export function loadMainView(view, ...args) {
event.emit('main-content-view:load', view, ...args)
event.emit("main-content-view:load", view, ...args);
}
/**
@ -19,8 +19,8 @@ export function loadMainView (view, ...args) {
* This is handy for certain cases, for example Last.fm connect/disconnect.
*/
export function forceReloadWindow() {
window.onbeforeunload = function () {}
window.location.reload()
window.onbeforeunload = function() {};
window.location.reload();
}
/**
@ -30,15 +30,19 @@ export function forceReloadWindow () {
* @param {String} type
* @param {Boolean} dismissable
*/
export function showOverlay (message = 'Just a little patience…', type = 'loading', dismissable = false) {
event.emit('overlay:show', { message, type, dismissable })
export function showOverlay(
message = "Just a little patience…",
type = "loading",
dismissable = false
) {
event.emit("overlay:show", { message, type, dismissable });
}
/**
* Hide the overlay.
*/
export function hideOverlay() {
event.emit('overlay:hide')
event.emit("overlay:hide");
}
/**
@ -47,9 +51,10 @@ export function hideOverlay () {
* @param {string} txt
*/
export function copyText(txt) {
const copyArea = document.querySelector('#copyArea')
copyArea.style.top = `${window.pageYOffset || document.documentElement.scrollTop}px`
copyArea.value = txt
select(copyArea)
document.execCommand('copy')
const copyArea = document.querySelector("#copyArea");
copyArea.style.top = `${window.pageYOffset ||
document.documentElement.scrollTop}px`;
copyArea.value = txt;
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) {
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) {
let aKey = isObject(a) ? get(a, key) : a
let bKey = isObject(b) ? get(b, key) : b
let aKey = isObject(a) ? get(a, key) : a;
let bKey = isObject(b) ? get(b, key) : b;
if (isNumber(aKey) && isNumber(bKey)) {
return aKey === bKey ? 0 : aKey > bKey
return aKey === bKey ? 0 : aKey > bKey;
}
aKey = aKey === undefined ? aKey : `${aKey}`.toLowerCase()
bKey = bKey === undefined ? bKey : `${bKey}`.toLowerCase()
aKey = aKey === undefined ? aKey : `${aKey}`.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
return arr.slice().sort((a, b) => {
if (sortKey.constructor === Array) {
let diff = 0
let diff = 0;
for (let i = 0; i < sortKey.length; i++) {
diff = compareRecordsByKey(a, b, sortKey[i])
diff = compareRecordsByKey(a, b, sortKey[i]);
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
b = isObject(b) ? get(b, sortKey) : b
a = isObject(a) ? get(a, sortKey) : a;
b = isObject(b) ? get(b, sortKey) : 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()
b = b === undefined ? b : b.toLowerCase()
a = a === undefined ? a : a.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) {
return arr.slice(offset, offset + n)
return arr.slice(offset, offset + n);
}
export function filterBy(arr, search, ...keys) {
if (!search) {
return arr
return arr;
}
// cast to lowercase string
search = (`${search}`).toLowerCase()
search = `${search}`.toLowerCase();
const res = []
const res = [];
each(arr, item => {
each(keys, key => {
if (`${get(item, key)}`.toLowerCase().indexOf(search) !== -1) {
res.push(item)
return false
res.push(item);
return false;
}
})
})
});
});
return res
return res;
}
export function pluralize() {
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

@ -3,27 +3,27 @@
* If H is 0, it will be ommited.
*/
export function secondsToHis(d) {
d = ~~d
d = ~~d;
let s = d % 60
let s = d % 60;
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) {
i = '0' + i
i = "0" + i;
}
let h = Math.floor(d / 3600)
let h = Math.floor(d / 3600);
if (h < 10) {
h = '0' + h
h = "0" + h;
}
return (h === '00' ? '' : h + ':') + i + ':' + s
return (h === "00" ? "" : h + ":") + i + ":" + s;
}
/**
@ -34,7 +34,10 @@ export function secondsToHis (d) {
* @return {Array.<String>}
*/
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]),
[]
);
}
/**
@ -45,5 +48,5 @@ export function parseValidationError (error) {
* @return {string}
*/
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 './filters'
export * from './formatters'
export * from './supports'
export * from './common'
export * from './$'
export * from "./alerts";
export * from "./filters";
export * from "./formatters";
export * from "./supports";
export * from "./common";
export * from "./$";

View file

@ -1,6 +1,6 @@
import isMobile from 'ismobilejs'
import Vue from 'vue'
import { each } from 'lodash'
import isMobile from "ismobilejs";
import Vue from "vue";
import { each } from "lodash";
/**
* Check if AudioContext is supported by the current browser.
@ -10,26 +10,27 @@ import { each } from 'lodash'
export function isAudioContextSupported() {
// Apple device just doesn't love AudioContext that much.
if (isMobile.apple.device) {
return false
return false;
}
const AudioContext = (window.AudioContext ||
const AudioContext =
window.AudioContext ||
window.webkitAudioContext ||
window.mozAudioContext ||
window.oAudioContext ||
window.msAudioContext)
window.msAudioContext;
if (!AudioContext) {
return false
return false;
}
// Safari (MacOS & iOS alike) has webkitAudioContext, but is buggy.
// @link http://caniuse.com/#search=audiocontext
if (!(new AudioContext()).createMediaElementSource) {
return false
if (!new AudioContext().createMediaElementSource) {
return false;
}
return true
return true;
}
/**
@ -37,7 +38,7 @@ export function isAudioContextSupported () {
* @return {Boolean}
*/
export function isClipboardSupported() {
return 'execCommand' in document
return "execCommand" in document;
}
/**
@ -45,7 +46,7 @@ export function isClipboardSupported () {
* @return {Boolean}
*/
export function isMediaSessionSupported() {
return 'mediaSession' in navigator
return "mediaSession" in navigator;
}
/**
@ -58,26 +59,28 @@ const event = {
init() {
if (!this.bus) {
this.bus = new Vue()
this.bus = new Vue();
}
return this
return this;
},
emit(name, ...args) {
this.bus.$emit(name, ...args)
return this
this.bus.$emit(name, ...args);
return this;
},
on() {
if (arguments.length === 2) {
this.bus.$on(arguments[0], arguments[1])
this.bus.$on(arguments[0], arguments[1]);
} 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 fs = require('fs')
const mix = require("laravel-mix");
const fs = require("fs");
mix.setResourceRoot('./public/')
mix.setResourceRoot("./public/");
mix.config.detectHotReloading()
mix.config.detectHotReloading();
if (mix.config.hmr) {
// There's a bug with Mix/copy plugin which prevents HMR from working:
// 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.
process.on('SIGINT', () => {
process.on("SIGINT", () => {
try {
fs.unlinkSync(mix.config.publicPath + '/hot')
} catch (e) {
}
process.exit()
})
fs.unlinkSync(mix.config.publicPath + "/hot");
} catch (e) {}
process.exit();
});
} else {
mix.copy('resources/assets/img', 'public/img', false)
.copy('node_modules/font-awesome/fonts', 'public/fonts', false)
mix
.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')
.sass('resources/assets/sass/app.scss', 'public/css')
.js('resources/assets/js/remote/app.js', 'public/js/remote')
.sass('resources/assets/sass/remote.scss', 'public/css')
mix
.js("resources/assets/js/app.js", "public/js")
.sass("resources/assets/sass/app.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) {
mix.version()
mix.disableNotifications()
mix.version();
mix.disableNotifications();
}