(spotify) add spotify player

This commit is contained in:
2020-07-22 22:19:05 +02:00
parent 589fe8ee5f
commit d5402fe154
14 changed files with 883 additions and 41 deletions

View File

@@ -1,7 +1,12 @@
<template>
<div class="chilled-music">
<transition name="fade">
<img v-show="!ready" src="@/assets/loader.svg" :alt="$t('loading')" />
<img
v-show="!ready"
class="loader"
src="@/assets/loader.svg"
:alt="$t('music.loading')"
/>
</transition>
<section v-show="false">
<div
@@ -99,29 +104,3 @@ export default defineComponent({
}
})
</script>
<style lang="scss" scoped>
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
img {
animation: rotating 4s linear infinite;
}
</style>
<i18n>
{
"en": {
"loading": "loading..."
},
"fr": {
"loading": "chargement..."
}
}
</i18n>

View File

@@ -25,7 +25,7 @@
<script lang="ts">
import { defineComponent } from '@vue/composition-api'
import { useInterval } from '../hooks/useInterval'
import { useInterval } from '@/hooks/useInterval'
export default defineComponent({
name: 'IntervalInput',

View File

@@ -0,0 +1,36 @@
<template>
<div class="spotify-music">
<transition name="fade">
<img
v-show="!ready"
class="loader"
src="@/assets/loader.svg"
:alt="$t('music.loading')"
/>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent, watchEffect } from '@vue/composition-api'
import { useSpotify } from '@/hooks/useSpotify'
export default defineComponent({
name: 'SpotifyMusic',
props: {
play: {
type: Boolean,
required: true
}
},
setup(props) {
const { ready, play, pause } = useSpotify()
watchEffect(() => {
props.play ? play() : pause()
})
return { ready, play, pause }
}
})
</script>

View File

@@ -20,16 +20,25 @@
:class="{ selected: hasMusic, disabled: !hasMusic }"
>
<span v-t="'chill'"></span>
<SpotifyMusic
v-if="hasSpotify"
:play="play"
@play="onPlay"
@pause="onPause"
/>
<ChilledMusic
class="chilled-music"
v-if="hasMusic"
v-else-if="hasMusic"
:play="play"
@play="play"
@pause="pause"
@play="onPlay"
@pause="onPause"
/>
</span>
</label>
</div>
<p v-if="!hasSpotify">
connect with <a href="#" @click.prevent="setHasSpotify(true)">Spotify</a>
</p>
</div>
</template>
@@ -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
}
}
})
</script>
@@ -110,6 +134,10 @@ label[data-pm-holder] {
.disabled {
color: desaturate($color, 70%);
}
p {
font-family: $serif-font-family;
}
</style>
<i18n>

View File

@@ -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<GetterTree<State, State>>(['hasMusic'])
const { setHasMusic } = useActions<RootActions>(['setHasMusic'])
const { hasMusic, hasSpotify } = useGetters<GetterTree<State, State>>([
'hasMusic',
'hasSpotify'
])
const { setHasMusic, setHasSpotify } = useActions<RootActions>([
'setHasMusic',
'setHasSpotify'
])
return {
isMusicAvailable: !isIOS,
hasMusic,
setHasMusic
setHasMusic,
hasSpotify,
setHasSpotify
}
}

89
src/hooks/useSpotify.ts Normal file
View File

@@ -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<string | null>(null)
const spotify = reactive({
player: null
})
const { accessToken, hasValidAccessToken } = useGetters<
GetterTree<State, State>
>(['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<RootActions>([
'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))
}

571
src/lib/spotify-player.js Normal file
View File

@@ -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('')
}
})
}
])

View File

@@ -8,5 +8,8 @@
"title": "Change!",
"body": "{dev}, your turn"
}
},
"music": {
"loading": "loading..."
}
}

View File

@@ -8,5 +8,8 @@
"title": "Changement !",
"body": "À votre tour {dev}."
}
},
"music": {
"loading": "loading..."
}
}

View File

@@ -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')
}
]

View File

@@ -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<State, State> {
@@ -16,6 +19,8 @@ export interface RootActions extends ActionTree<State, State> {
setHasMusic: (ctx: ActionContext<State, State>, payload: boolean) => void
setDev1: (ctx: ActionContext<State, State>, payload: string) => void
setDev2: (ctx: ActionContext<State, State>, payload: string) => void
setAccessToken: (ctx: ActionContext<State, State>, payload: string) => void
setTokenExpire: (ctx: ActionContext<State, State>, payload: Date) => void
}
const vuexLocal = new VuexPersistence<State>({
@@ -25,21 +30,35 @@ const vuexLocal = new VuexPersistence<State>({
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>({
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<State>({
[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<State>({
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]

View File

@@ -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;
}

54
src/utils/spotify.ts Normal file
View File

@@ -0,0 +1,54 @@
const LOFI = 'spotify:playlist:0vvXsWCC9xrXsKd4FyS8kM'
const LOFI_MAX = 250
const random = (max: number) => {
return Math.floor(Math.random() * Math.floor(max))
}
export const redirectToSpotifyConnect = () => {
const authorizeURL = new URL('https://accounts.spotify.com/authorize')
authorizeURL.searchParams.append(
'client_id',
process.env.VUE_APP_SPOTIFY_CLIENT_ID || ''
)
authorizeURL.searchParams.append('response_type', 'token')
authorizeURL.searchParams.append('show_dialog', 'false')
authorizeURL.searchParams.append('scope', 'user-modify-playback-state')
authorizeURL.searchParams.append('redirect_uri', `${location.origin}/spotify`)
location.href = authorizeURL.toString()
}
export const playOnSpotify = async (token: string, deviceId: string) => {
await fetch(
`https://api.spotify.com/v1/me/player/play?device_id=${deviceId}`,
{
method: 'PUT',
headers: {
accept: 'application/json',
'content-type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
context_uri: LOFI,
offset: {
position: random(LOFI_MAX)
}
})
}
)
}
export const pauseOnSpotify = async (token: string, deviceId: string) => {
await fetch(
`https://api.spotify.com/v1/me/player/pause?device_id=${deviceId}`,
{
method: 'PUT',
headers: {
accept: 'application/json',
'content-type': 'application/json',
authorization: `Bearer ${token}`
}
}
)
}

View File

@@ -0,0 +1,16 @@
<template>
<div class="spotify-callback"></div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api'
import { useSpotifyConnect } from '../hooks/useSpotify'
export default defineComponent({
name: 'SpotifyCallback',
setup(_, { root }) {
useSpotifyConnect(root.$router.currentRoute.hash)
root.$router.replace({ name: 'Home' })
}
})
</script>