diff --git a/src/components/ChilledMusic.vue b/src/components/ChilledMusic.vue index ddc811f..a38cc7c 100644 --- a/src/components/ChilledMusic.vue +++ b/src/components/ChilledMusic.vue @@ -1,7 +1,12 @@ @@ -40,7 +49,8 @@ import { useMusic } from '@/hooks/useMusic' export default defineComponent({ name: 'ZoneMusic', components: { - ChilledMusic: () => import('@/components/ChilledMusic.vue') + ChilledMusic: () => import('@/components/ChilledMusic.vue'), + SpotifyMusic: () => import('@/components/SpotifyMusic.vue') }, props: { play: { @@ -49,17 +59,31 @@ export default defineComponent({ } }, setup(_, { emit }) { - const { isMusicAvailable, hasMusic, setHasMusic } = useMusic() + const { + isMusicAvailable, + hasSpotify, + setHasSpotify, + hasMusic, + setHasMusic + } = useMusic() - const play = () => { + const onPlay = () => { emit('play') } - const pause = () => { + const onPause = () => { emit('pause') } - return { isMusicAvailable, hasMusic, setHasMusic, play, pause } + return { + isMusicAvailable, + hasSpotify, + setHasSpotify, + hasMusic, + setHasMusic, + onPlay, + onPause + } } }) @@ -110,6 +134,10 @@ label[data-pm-holder] { .disabled { color: desaturate($color, 70%); } + +p { + font-family: $serif-font-family; +} diff --git a/src/hooks/useMusic.ts b/src/hooks/useMusic.ts index e886ada..3bfae44 100644 --- a/src/hooks/useMusic.ts +++ b/src/hooks/useMusic.ts @@ -5,12 +5,20 @@ import { State, RootActions } from '@/store' export const useMusic = () => { const isIOS = navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) - const { hasMusic } = useGetters>(['hasMusic']) - const { setHasMusic } = useActions(['setHasMusic']) + const { hasMusic, hasSpotify } = useGetters>([ + 'hasMusic', + 'hasSpotify' + ]) + const { setHasMusic, setHasSpotify } = useActions([ + 'setHasMusic', + 'setHasSpotify' + ]) return { isMusicAvailable: !isIOS, hasMusic, - setHasMusic + setHasMusic, + hasSpotify, + setHasSpotify } } diff --git a/src/hooks/useSpotify.ts b/src/hooks/useSpotify.ts new file mode 100644 index 0000000..fa1d060 --- /dev/null +++ b/src/hooks/useSpotify.ts @@ -0,0 +1,89 @@ +/* eslint-disable */ +// @ts-ignore +// @ts-nocheck +import '@/lib/spotify-player' +import { useGetters, useActions } from 'vuex-composition-helpers' +import { GetterTree } from 'vuex' +import { State, RootActions } from '@/store' +import { + redirectToSpotifyConnect, + playOnSpotify, + pauseOnSpotify +} from '@/utils/spotify' +import { ref, reactive } from '@vue/composition-api' + +export const useSpotify = () => { + const ready = ref(false) + const deviceId = ref(null) + const spotify = reactive({ + player: null + }) + const { accessToken, hasValidAccessToken } = useGetters< + GetterTree + >(['accessToken', 'hasValidAccessToken']) + + window.onSpotifyWebPlaybackSDKReady = async () => { + if (!hasValidAccessToken.value) { + redirectToSpotifyConnect() + return + } + + spotify.player = new Spotify.Player({ + name: 'binome', + getOAuthToken: (callback) => callback(accessToken.value) + }) + spotify.player.addListener('ready', ({ device_id }) => { + deviceId.value = device_id + ready.value = true + }) + await spotify.player.connect() + } + + const play = async () => { + try { + if (spotify.player && ready.value) { + const playerState = await spotify.player.getCurrentState() + console.log(playerState) + + if (!playerState) { + playOnSpotify(accessToken.value, deviceId.value) + return + } + await spotify.player.resume() + } + } catch (error) { + console.warn(error) + } + } + + const pause = () => { + if (ready.value) { + pauseOnSpotify(accessToken.value, deviceId.value) + } + } + + return { + ready, + play, + pause + } +} + +export const useSpotifyConnect = (hash: string) => { + window.onSpotifyWebPlaybackSDKReady = () => {} + const { setAccessToken, setTokenExpire } = useActions([ + 'setAccessToken', + 'setTokenExpire' + ]) + hash = hash.replace('#', '') + const connection = [...hash.split('&').values()] + .map((entry) => entry.split('=')) + .reduce((acc, entry) => { + acc[entry[0]] = entry[1] + return acc + }, {}) + + setAccessToken(connection['access_token']) + const now = new Date() + setTokenExpire(now.setTime(now.getTime() + connection['expires_in'] * 1000)) +} diff --git a/src/lib/spotify-player.js b/src/lib/spotify-player.js new file mode 100644 index 0000000..f1d2b9f --- /dev/null +++ b/src/lib/spotify-player.js @@ -0,0 +1,571 @@ +/* eslint-disable */ +/* Spotify JS-SDK - v1.7.1-d2b664d */ +!(function(e) { + function t(r) { + if (n[r]) return n[r].exports + var s = (n[r] = { i: r, l: !1, exports: {} }) + return e[r].call(s.exports, s, s.exports, t), (s.l = !0), s.exports + } + var n = {} + ;(t.m = e), + (t.c = n), + (t.d = function(e, n, r) { + t.o(e, n) || + Object.defineProperty(e, n, { + configurable: !1, + enumerable: !0, + get: r + }) + }), + (t.n = function(e) { + var n = + e && e.__esModule + ? function() { + return e.default + } + : function() { + return e + } + return t.d(n, 'a', n), n + }), + (t.o = function(e, t) { + return Object.prototype.hasOwnProperty.call(e, t) + }), + (t.p = ''), + t((t.s = 2)) +})([ + function(e, t, n) { + 'use strict' + var r = { + SPOTIFY_MESSAGE: 'SP_MESSAGE', + ACCOUNT_ERROR: 'ACCOUNT_ERROR', + AUTH_ERROR: 'AUTH_ERROR', + CONNECT: 'CONNECT', + CONNECTED: 'CONNECTED', + CURRENT_STATE: 'CURRENT_STATE', + DISCONNECT: 'DISCONNECT', + EVENT: 'EVENT', + GET_CURRENT_STATE: 'GET_CURRENT_STATE', + GET_TOKEN: 'GET_TOKEN', + GET_VOLUME: 'GET_VOLUME', + INIT: 'INIT', + LOADED: 'LOADED', + NEXT_TRACK: 'NEXT_TRACK', + PAUSE: 'PAUSE', + PLAYBACK_ERROR: 'PLAYBACK_ERROR', + PLAYER_INIT_ERROR: 'PLAYER_INIT_ERROR', + PLAYER_READY: 'PLAYER_READY', + PLAYER_NOT_READY: 'PLAYER_NOT_READY', + PLAYER_STATE_CHANGED: 'PLAYER_STATE_CHANGED', + PREV_TRACK: 'PREV_TRACK', + RESUME: 'RESUME', + SEEK: 'SEEK', + SET_NAME: 'SET_NAME', + SET_VOLUME: 'SET_VOLUME', + TOGGLE_PLAY: 'TOGGLE_PLAY', + TOKEN: 'TOKEN', + VOLUME: 'VOLUME', + accountError: function(e) { + return this._createEventMessage(r.ACCOUNT_ERROR, { message: e }) + }, + authError: function(e) { + return this._createEventMessage(r.AUTH_ERROR, e) + }, + playbackError: function(e) { + return this._createEventMessage(r.PLAYBACK_ERROR, e) + }, + playerReady: function(e) { + return this._createEventMessage(r.PLAYER_READY, e) + }, + playerNotReady: function(e) { + return this._createEventMessage(r.PLAYER_NOT_READY, e) + }, + connect: function() { + return this._createMessage(r.CONNECT) + }, + connected: function(e, t) { + return this._createMessage(r.CONNECTED, { connected: e, ref: t }) + }, + disconnect: function() { + return this._createMessage(r.DISCONNECT) + }, + init: function(e) { + return this._createMessage(r.INIT, e) + }, + playerInitError: function(e) { + return this._createEventMessage(r.PLAYER_INIT_ERROR, e) + }, + getToken: function() { + return this._createMessage(r.GET_TOKEN) + }, + token: function(e, t) { + return this._createMessage(r.TOKEN, { token: e, ref: t }) + }, + pause: function() { + return this._createMessage(r.PAUSE) + }, + resume: function() { + return this._createMessage(r.RESUME) + }, + togglePlay: function() { + return this._createMessage(r.TOGGLE_PLAY) + }, + seek: function(e) { + return this._createMessage(r.SEEK, e) + }, + nextTrack: function(e) { + return this._createMessage(r.NEXT_TRACK, e) + }, + previousTrack: function(e) { + return this._createMessage(r.PREV_TRACK, e) + }, + getCurrentState: function() { + return this._createMessage(r.GET_CURRENT_STATE) + }, + currentState: function(e, t) { + return this._createMessage(r.CURRENT_STATE, { state: e, ref: t }) + }, + playerStateChanged: function(e) { + return this._createEventMessage(r.PLAYER_STATE_CHANGED, e) + }, + getVolume: function() { + return this._createMessage(r.GET_VOLUME) + }, + volume: function(e, t) { + return this._createMessage(r.VOLUME, { volume: e, ref: t }) + }, + setVolume: function(e) { + return this._createMessage(r.SET_VOLUME, e) + }, + setName: function(e) { + return this._createMessage(r.SET_NAME, e) + }, + embeddedLoaded: function() { + return this._createMessage(r.LOADED) + }, + _createEventMessage: function(e, t) { + return this._createMessage(r.EVENT, { name: e, eventData: t }) + }, + _createMessage: function(e, t) { + return { + type: r.SPOTIFY_MESSAGE, + body: { topic: e, data: t ? JSON.parse(JSON.stringify(t)) : null } + } + } + } + e.exports = r + }, + function(e, t) { + var n + n = (function() { + return this + })() + try { + n = n || Function('return this')() || (0, eval)('this') + } catch (e) { + 'object' == typeof window && (n = window) + } + e.exports = n + }, + function(e, t, n) { + 'use strict' + function r() { + var e = n(3) + if (!document.body) throw new Error("Document doesn't have a body") + if ( + ((window.Spotify = { Player: e.setupPlayerEnv(window) }), + window.onSpotifyWebPlaybackSDKReady) + ) + window.onSpotifyWebPlaybackSDKReady() + else { + if (!window.onSpotifyPlayerAPIReady) + throw new Error('onSpotifyWebPlaybackSDKReady is not defined') + window.onSpotifyPlayerAPIReady() + } + } + 'complete' === document.readyState + ? r() + : window.addEventListener('load', r) + }, + function(e, t, n) { + 'use strict' + var r = n(4), + s = n(5), + o = n(0), + i = n(6), + a = function(e, t) { + var n, + a = 'https://sdk.scdn.co/embedded/index.html', + u = s.defer(), + c = new r(), + _ = + t || + function(t) { + var n = e.document.createElement('iframe') + return ( + (n.src = t), + n.setAttribute('alt', 'Audio Playback Container'), + n.setAttribute('tabIndex', '-1'), + n.style.setProperty('position', 'absolute', 'important'), + n.style.setProperty('left', '-1px', 'important'), + n.style.setProperty('width', '0', 'important'), + n.style.setProperty('height', '0', 'important'), + n.style.setProperty('border', 'none', 'important'), + n.style.setProperty('outline', 'none', 'important'), + (n.allow = 'encrypted-media; autoplay'), + e.document.body.appendChild(n), + n.contentWindow + ) + }, + h = function(t) { + t === o.LOADED && (c.stopListening(e, h), u.resolve()) + } + c.listen(e, h), (n = _(a)) + var f = function(t) { + ;(this._options = this._sanitizeOptions(t)), + (this._handleMessages = this._handleMessages.bind(this)), + (this._messageHandlers = {}), + (this._eventListeners = {}), + this._setupMessageHandlers(), + (this._connectionRequests = {}), + (this._getCurrentStateRequests = {}), + (this._getVolumeRequests = {}), + (this.isLoaded = u.promise.then( + function() { + c.listen(e, this._handleMessages), + this._sendMessage(o.init(this._options)) + }.bind(this) + )) + } + return ( + (f.prototype._sanitizeOptions = function(t) { + var n = (e && e.location && e.location.hostname) || '' + return { + name: t.name || n, + id: i.get(), + getOAuthToken: t.getOAuthToken || t.getOauthToken, + volume: t.volume || 1 + } + }), + (f.prototype._setupMessageHandlers = function() { + ;(this._messageHandlers[o.GET_TOKEN] = this._onGetToken.bind(this)), + (this._messageHandlers[o.EVENT] = this._onEvent.bind(this)), + (this._messageHandlers[o.CONNECTED] = this._onConnected.bind( + this + )), + (this._messageHandlers[ + o.CURRENT_STATE + ] = this._onCurrentState.bind(this)), + (this._messageHandlers[o.VOLUME] = this._onVolume.bind(this)) + }), + (f.prototype.connect = function() { + return this.isLoaded.then( + function() { + var e = this._sendMessage(o.connect()) + return ( + (this._connectionRequests[e] = s.defer()), + this._connectionRequests[e].promise + ) + }.bind(this) + ) + }), + (f.prototype.on = function(e, t) { + if (!e) throw new TypeError('Argument `eventName` is required.') + if ('function' != typeof t) + throw new TypeError('Argument `listener` must be a function.') + return ( + (this._eventListeners[e] = this._eventListeners[e] || []), + -1 === this._eventListeners[e].indexOf(t) && + (this._eventListeners[e].push(t), !0) + ) + }), + (f.prototype.addListener = function(e, t) { + return this.on(e, t) + }), + (f.prototype.removeListener = function(e, t) { + if (!e) throw new TypeError('Argument `eventName` is required.') + if (1 === arguments.length) + return (this._eventListeners[e] = []), !0 + if ('function' != typeof t) + throw new TypeError('Argument `listener` must be a function.') + var n = this._eventListeners[e] + return ( + !(!n || !n.length) && + ((this._eventListeners[e] = n.filter(function(e) { + return e !== t + })), + !0) + ) + }), + (f.prototype.getCurrentState = function() { + return this.isLoaded.then( + function() { + var e = this._sendMessage(o.getCurrentState()) + return ( + (this._getCurrentStateRequests[e] = s.defer()), + this._getCurrentStateRequests[e].promise + ) + }.bind(this) + ) + }), + (f.prototype.getVolume = function() { + return this.isLoaded.then( + function() { + var e = this._sendMessage(o.getVolume()) + return ( + (this._getVolumeRequests[e] = s.defer()), + this._getVolumeRequests[e].promise + ) + }.bind(this) + ) + }), + (f.prototype.setName = function(e) { + return this._sendMessageWhenLoaded(o.setName(e)) + }), + (f.prototype.setVolume = function(e) { + return this._sendMessageWhenLoaded(o.setVolume(e)) + }), + (f.prototype.pause = function() { + return this._sendMessageWhenLoaded(o.pause()) + }), + (f.prototype.resume = function() { + return this._sendMessageWhenLoaded(o.resume()) + }), + (f.prototype.togglePlay = function() { + return this._sendMessageWhenLoaded(o.togglePlay()) + }), + (f.prototype.seek = function(e) { + return this._sendMessageWhenLoaded(o.seek(e)) + }), + (f.prototype.previousTrack = function(e) { + return this._sendMessageWhenLoaded(o.previousTrack(e)) + }), + (f.prototype.nextTrack = function(e) { + return this._sendMessageWhenLoaded(o.nextTrack(e)) + }), + (f.prototype.disconnect = function() { + return this._sendMessageWhenLoaded(o.disconnect()) + }), + (f.prototype._getListeners = (function() { + var e = {} + return ( + (e[o.ACCOUNT_ERROR] = 'account_error'), + (e[o.AUTH_ERROR] = 'authentication_error'), + (e[o.PLAYBACK_ERROR] = 'playback_error'), + (e[o.PLAYER_INIT_ERROR] = 'initialization_error'), + (e[o.PLAYER_READY] = 'ready'), + (e[o.PLAYER_NOT_READY] = 'not_ready'), + (e[o.PLAYER_STATE_CHANGED] = 'player_state_changed'), + function(t) { + return this._eventListeners[e[t]] || [] + } + ) + })()), + (f.prototype._onEvent = function(e) { + this._getListeners(e.name).forEach(function(t) { + t(e.eventData) + }) + }), + (f.prototype._onGetToken = function(e, t) { + var n = this._options.getOAuthToken + if ('function' != typeof n) { + var r = 'getOAuthToken is not a function' + if (this._getListeners(o.PLAYER_INIT_ERROR).length) + return void this._onEvent({ + name: o.PLAYER_INIT_ERROR, + eventData: { message: r } + }) + throw new Error(r) + } + new s(n).then( + function(e) { + this._sendMessage(o.token(e, t)) + }.bind(this) + ) + }), + (f.prototype._onConnected = function(e) { + e.ref in this._connectionRequests && + (this._connectionRequests[e.ref].resolve(e.connected), + delete this._connectionRequests[e.ref]) + }), + (f.prototype._onCurrentState = function(e) { + e.ref in this._getCurrentStateRequests && + (this._getCurrentStateRequests[e.ref].resolve(e.state), + delete this._getCurrentStateRequests[e.ref]) + }), + (f.prototype._onVolume = function(e) { + e.ref in this._getVolumeRequests && + (this._getVolumeRequests[e.ref].resolve(e.volume), + delete this._getVolumeRequests[e.ref]) + }), + (f.prototype._handleMessages = function(e, t, n) { + e in this._messageHandlers && this._messageHandlers[e](t, n) + }), + (f.prototype._sendMessage = function(e) { + return c.send(n, e, a) + }), + (f.prototype._sendMessageWhenLoaded = function(e) { + return this.isLoaded.then(this._sendMessage.bind(this, e)) + }), + f + ) + } + e.exports.setupPlayerEnv = a + }, + function(e, t, n) { + 'use strict' + var r = n(0), + s = function() { + ;(this._onMessageCallback = function() {}), + (this._receiveMessage = this._receiveMessage.bind(this)), + (this._messageId = 0) + } + ;(s.prototype.listen = function(e, t) { + ;(this._onMessageCallback = t), + e.addEventListener('message', this._receiveMessage) + }), + (s.prototype.stopListening = function(e) { + e.removeEventListener('message', this._receiveMessage) + }), + (s.prototype.send = function(e, t, n) { + return e.postMessage(this._addMessageId(t), n || '*'), t.seq + }), + (s.prototype._addMessageId = function(e) { + return (e.seq = this._messageId++), e + }), + (s.prototype._receiveMessage = function(e) { + var t = e.data + t.type === r.SPOTIFY_MESSAGE && + t.body && + t.body.topic && + this._onMessageCallback(t.body.topic, t.body.data, t.seq) + }), + (e.exports = s) + }, + function(e, t, n) { + 'use strict' + ;(function(t) { + var n = t.Promise + if (!n) throw new Error('Fatal: no Promise implementation available.') + var r + ;(r = Object.defineProperty + ? function(e, t, n) { + return e[t] + ? e + : (Object.defineProperty(e, t, { + value: n, + configurable: !0, + writable: !0 + }), + e) + } + : function(e, t, n) { + return e[t] ? e : ((e[t] = n), e) + }), + r(n, 'defer', function() { + var e = {} + return ( + (e.promise = new n(function(t, n) { + ;(e.resolve = t), (e.reject = n) + })), + e + ) + }), + r(n.prototype, 'spread', function(e, t) { + return this.then(function(e) { + return n.all(e) + }).then(function(t) { + return 1 === t.length ? e(t[0]) : e.apply(this, t) + }, t) + }), + (e.exports = n) + }.call(t, n(1))) + }, + function(e, t, n) { + 'use strict' + ;(function(t) { + function r() { + var e = new t.Uint8Array(16) + return t.crypto.getRandomValues(e), i.toHex(e.join(''), 40).slice(0, 40) + } + function s() { + for (var e = new Array(27), t = e.length; t--; ) + e[t] = Math.floor(8 * Math.random()) + return i.toHex(e.join(''), 40) + } + function o(e, n) { + if (e && 'string' == typeof e) return e + if (n) return u() + var r = t.localStorage.getItem(c) + return r || ((r = u()), t.localStorage.setItem(c, r)), r + } + var i = n(7), + a = + 'function' == typeof t.Uint8Array && + void 0 !== t.crypto && + 'function' == typeof t.crypto.getRandomValues, + u = a ? r : s, + c = '_spharmony_device_id' + e.exports = { + get: o, + generate: u, + generateWithCrypto: r, + generateWithRandom: s + } + }.call(t, n(1))) + }, + function(e, t, n) { + 'use strict' + function r(e, t, n) { + for (var r = 0, s = 0; s < e.length; ++s) { + var o = e[s] * t + r + ;(e[s] = o % n), (r = ~~(o / n)) + } + for (; r; ) e.push(r % n), (r = ~~(r / n)) + } + function s(e, t, n, r) { + var s, + o, + i = 0 + for (o = 0; o < t.length; ++o) + (s = ~~e[o] + t[o] * n + i), (e[o] = s % r), (i = ~~(s / r)) + for (; i; ) (s = ~~e[o] + i), (e[o] = s % r), (i = ~~(s / r)), ++o + } + function o(e, t, n) { + for (var o = [0], i = [1], a = 0; a < e.length; ++a) + s(o, i, e[a], n), r(i, t, n) + return o + } + function i(e, t) { + for (var n = [], r = 0; r < e.length; ++r) n.push(t[e[r]]) + return n.reverse() + } + function a(e, t) { + for (; e.length < t; ) e.push(0) + return e + } + var u = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + c = {}, + _ = {} + !(function() { + var e, t + for (e = 0, t = u.length; e < t; ++e) _[u[e]] = e + for (e = 0; e < 16; ++e) c['0123456789abcdef'[e]] = e + for (e = 0; e < 16; ++e) c['0123456789ABCDEF'[e]] = e + })(), + (e.exports = { + fromBytes: function(e, t) { + return i(a(o(e.slice(0).reverse(), 256, 62), t), u).join('') + }, + toBytes: function(e, t) { + return a(o(i(e, _), 62, 256), t).reverse() + }, + toHex: function(e, t) { + return i(a(o(i(e, _), 62, 16), t), u).join('') + }, + fromHex: function(e, t) { + return i(a(o(i(e, c), 16, 62), t), u).join('') + } + }) + } +]) diff --git a/src/locales/en.json b/src/locales/en.json index c34e0ec..b6098a3 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -8,5 +8,8 @@ "title": "Change!", "body": "{dev}, your turn" } + }, + "music": { + "loading": "loading..." } } diff --git a/src/locales/fr.json b/src/locales/fr.json index 9dbe92f..4c5fb59 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -8,5 +8,8 @@ "title": "Changement !", "body": "À votre tour {dev}." } + }, + "music": { + "loading": "loading..." } } diff --git a/src/router/index.ts b/src/router/index.ts index c8fd4f3..9eea0e2 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -20,6 +20,11 @@ const routes: RouteConfig[] = [ path: '/Privacy', name: 'Privacy', component: () => import('@/views/Privacy.vue') + }, + { + path: '/spotify', + name: 'Spotify', + component: () => import('@/views/SpotifyCallback.vue') } ] diff --git a/src/store/index.ts b/src/store/index.ts index 12925fd..fcf1634 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -7,8 +7,11 @@ Vue.use(Vuex) export interface State { interval: number hasMusic: boolean + hasSpotify: boolean dev1: string | null dev2: string | null + accessToken: string | null + tokenExpire: Date | null } export interface RootActions extends ActionTree { @@ -16,6 +19,8 @@ export interface RootActions extends ActionTree { setHasMusic: (ctx: ActionContext, payload: boolean) => void setDev1: (ctx: ActionContext, payload: string) => void setDev2: (ctx: ActionContext, payload: string) => void + setAccessToken: (ctx: ActionContext, payload: string) => void + setTokenExpire: (ctx: ActionContext, payload: Date) => void } const vuexLocal = new VuexPersistence({ @@ -25,21 +30,35 @@ const vuexLocal = new VuexPersistence({ const SET_INTERVAL = 'SET_INTERVAL' const WITH_MUSIC = 'WITH_MUSIC' +const WITH_SPOTIFY = 'WITH_SPOTIFY' const SET_DEV_1 = 'SET_DEV_1' const SET_DEV_2 = 'SET_DEV_2' +const SET_ACCESS_TOKEN = 'SET_ACCESS_TOKEN' +const SET_TOKEN_EXPIRE = 'SET_TOKEN_EXPIRE' const store = new Vuex.Store({ state: { interval: 5, hasMusic: false, + hasSpotify: false, dev1: null, - dev2: null + dev2: null, + accessToken: null, + tokenExpire: null }, getters: { interval: ({ interval }) => interval, hasMusic: ({ hasMusic }) => hasMusic, + hasSpotify: ({ hasSpotify }) => hasSpotify, dev1: ({ dev1 }) => dev1, - dev2: ({ dev2 }) => dev2 + dev2: ({ dev2 }) => dev2, + accessToken: ({ accessToken }) => accessToken, + hasValidAccessToken: ({ accessToken, tokenExpire }) => { + if (!accessToken || !tokenExpire) { + return false + } + return new Date() < tokenExpire + } }, mutations: { [SET_INTERVAL](state, interval: number) { @@ -48,11 +67,20 @@ const store = new Vuex.Store({ [WITH_MUSIC](state, hasMusic: boolean) { state.hasMusic = hasMusic }, + [WITH_SPOTIFY](state, hasSpotify: boolean) { + state.hasSpotify = hasSpotify + }, [SET_DEV_1](state, dev1: string) { state.dev1 = dev1 }, [SET_DEV_2](state, dev2: string) { state.dev2 = dev2 + }, + [SET_ACCESS_TOKEN](state, accessToken: string) { + state.accessToken = accessToken + }, + [SET_TOKEN_EXPIRE](state, tokenExpire: Date) { + state.tokenExpire = tokenExpire } }, actions: { @@ -64,11 +92,20 @@ const store = new Vuex.Store({ setHasMusic({ commit }, hasMusic: boolean) { commit(WITH_MUSIC, hasMusic) }, + setHasSpotify({ commit }, hasSpotify: boolean) { + commit(WITH_SPOTIFY, hasSpotify) + }, setDev1({ commit }, dev1: string) { commit(SET_DEV_1, dev1) }, setDev2({ commit }, dev2: string) { commit(SET_DEV_2, dev2) + }, + setAccessToken({ commit }, accessToken: string) { + commit(SET_ACCESS_TOKEN, accessToken) + }, + setTokenExpire({ commit }, tokenExpire: Date) { + commit(SET_TOKEN_EXPIRE, tokenExpire) } }, plugins: [vuexLocal.plugin] diff --git a/src/styles/app.scss b/src/styles/app.scss index 93b28bc..067d282 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -60,3 +60,16 @@ a { .fade-leave-to { opacity: 0; } + +@keyframes rotating { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +img.loader { + animation: rotating 4s linear infinite; +} diff --git a/src/views/SpotifyCallback.vue b/src/views/SpotifyCallback.vue new file mode 100644 index 0000000..66bd6be --- /dev/null +++ b/src/views/SpotifyCallback.vue @@ -0,0 +1,16 @@ + + +