Merge branch '327-versatile-front' into 'develop'

Resolve "Make it possible to point the front-end to custom API urls"

Closes #327

See merge request funkwhale/funkwhale!269
This commit is contained in:
Eliot Berriot 2018-06-23 16:29:15 +00:00
commit 7d60155b0f
25 changed files with 251 additions and 101 deletions

View file

@ -1,44 +1,71 @@
<template>
<div id="app">
<sidebar></sidebar>
<service-messages v-if="messages.length > 0" />
<router-view :key="$route.fullPath"></router-view>
<div class="ui fitted divider"></div>
<div id="footer" class="ui vertical footer segment">
<div class="ui container">
<div class="ui stackable equal height stackable grid">
<div class="three wide column">
<i18next tag="h4" class="ui header" path="Links"></i18next>
<div class="ui link list">
<router-link class="item" to="/about">
<i18next path="About this instance" />
</router-link>
<a href="https://funkwhale.audio" class="item" target="_blank">{{ $t('Official website') }}</a>
<a href="https://docs.funkwhale.audio" class="item" target="_blank">{{ $t('Documentation') }}</a>
<a href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank">
<template v-if="version">{{ $t('Source code ({% version %})', {version: version}) }}</template>
<template v-else>{{ $t('Source code') }}</template>
</a>
<a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank">{{ $t('Issue tracker') }}</a>
<div class="ui main text container instance-chooser" v-if="!$store.state.instance.instanceUrl">
<div class="ui padded segment">
<h1 class="ui header">{{ $t('Choose your instance') }}</h1>
<form class="ui form" @submit.prevent="$store.dispatch('instance/setUrl', instanceUrl)">
<p>{{ $t('You need to select an instance in order to continue') }}</p>
<div class="ui action input">
<input type="text" v-model="instanceUrl">
<button type="submit" class="ui button">{{ $t('Submit') }}</button>
</div>
<p>{{ $t('Suggested choices') }}</p>
<div class="ui bulleted list">
<div class="ui item" v-for="url in suggestedInstances">
<a @click="instanceUrl = url">{{ url }}</a>
</div>
</div>
<div class="ten wide column">
<i18next tag="h4" class="ui header" path="About funkwhale" />
<p>
<i18next path="Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!"/>
</p>
<p>
<i18next path="The funkwhale logo was kindly designed and provided by Francis Gading."/>
</p>
</form>
</div>
</div>
<template v-else>
<sidebar></sidebar>
<service-messages v-if="messages.length > 0" />
<router-view :key="$route.fullPath"></router-view>
<div class="ui fitted divider"></div>
<div id="footer" class="ui vertical footer segment">
<div class="ui container">
<div class="ui stackable equal height stackable grid">
<div class="three wide column">
<i18next tag="h4" class="ui header" path="Links"></i18next>
<div class="ui link list">
<router-link class="item" to="/about">
<i18next path="About this instance" />
</router-link>
<a href="https://funkwhale.audio" class="item" target="_blank">{{ $t('Official website') }}</a>
<a href="https://docs.funkwhale.audio" class="item" target="_blank">{{ $t('Documentation') }}</a>
<a href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank">
<template v-if="version">{{ $t('Source code ({% version %})', {version: version}) }}</template>
<template v-else>{{ $t('Source code') }}</template>
</a>
<a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank">{{ $t('Issue tracker') }}</a>
<a @click="switchInstance" class="item" >
{{ $t('Use another instance') }}
<template v-if="$store.state.instance.instanceUrl !== '/'">
<br>
({{ $store.state.instance.instanceUrl }})
</template>
</a>
</div>
</div>
<div class="ten wide column">
<i18next tag="h4" class="ui header" path="About funkwhale" />
<p>
<i18next path="Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!"/>
</p>
<p>
<i18next path="The funkwhale logo was kindly designed and provided by Francis Gading."/>
</p>
</div>
</div>
</div>
</div>
</div>
<raven
v-if="$store.state.instance.settings.raven.front_enabled.value"
:dsn="$store.state.instance.settings.raven.front_dsn.value">
</raven>
<playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
<raven
v-if="$store.state.instance.settings.raven.front_enabled.value"
:dsn="$store.state.instance.settings.raven.front_dsn.value">
</raven>
<playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
</template>
</div>
</template>
@ -63,17 +90,22 @@ export default {
},
data () {
return {
nodeinfo: null
nodeinfo: null,
instanceUrl: null
}
},
created () {
this.$store.dispatch('instance/fetchSettings')
let self = this
setInterval(() => {
// used to redraw ago dates every minute
self.$store.commit('ui/computeLastDate')
}, 1000 * 60)
this.fetchNodeInfo()
if (this.$store.state.instance.instanceUrl) {
this.$store.commit('instance/instanceUrl', this.$store.state.instance.instanceUrl)
this.$store.dispatch('auth/check')
this.$store.dispatch('instance/fetchSettings')
this.fetchNodeInfo()
}
},
methods: {
fetchNodeInfo () {
@ -81,18 +113,38 @@ export default {
axios.get('instance/nodeinfo/2.0/').then(response => {
self.nodeinfo = response.data
})
},
switchInstance () {
let confirm = window.confirm(this.$t('This will erase your local data and disconnect you, do you want to continue?'))
if (confirm) {
this.$store.commit('instance/instanceUrl', null)
}
}
},
computed: {
...mapState({
messages: state => state.ui.messages
}),
suggestedInstances () {
let rootUrl = (
window.location.protocol + '//' + window.location.hostname +
(window.location.port ? ':' + window.location.port : '')
)
let instances = [rootUrl, 'https://demo.funkwhale.audio']
return instances
},
version () {
if (!this.nodeinfo) {
return null
}
return _.get(this.nodeinfo, 'software.version')
}
},
watch: {
'$store.state.instance.instanceUrl' () {
this.$store.dispatch('instance/fetchSettings')
this.fetchNodeInfo()
}
}
}
</script>
@ -116,6 +168,11 @@ html, body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.instance-chooser {
margin-top: 2em;
}
.main.pusher, .footer {
@include media(">desktop") {
margin-left: 350px !important;
@ -173,7 +230,7 @@ html, body {
.ui.icon.header .circular.icon {
display: flex;
justify-content: center;
}
.segment-content .button{

View file

@ -1,5 +1,3 @@
import config from '@/config'
var Album = {
clean (album) {
// we manually rebind the album and artist to each child track
@ -21,21 +19,6 @@ var Artist = {
}
}
export default {
absoluteUrl (url) {
if (url.startsWith('http')) {
return url
}
if (url.startsWith('/')) {
let rootUrl = (
window.location.protocol + '//' + window.location.hostname +
(window.location.port ? ':' + window.location.port : '')
)
return rootUrl + url
} else {
return config.BACKEND_URL + url
}
},
Artist: Artist,
Album: Album
}

View file

@ -1,7 +0,0 @@
import backend from './backend'
export default {
getCover (track) {
return backend.absoluteUrl(track.album.cover)
}
}

View file

@ -120,7 +120,7 @@
<tr @click="$store.dispatch('queue/currentIndex', index)" v-for="(track, index) in queue.tracks" :key="index" :class="[{'active': index === queue.currentIndex}]">
<td class="right aligned">{{ index + 1}}</td>
<td class="center aligned">
<img class="ui mini image" v-if="track.album.cover" :src="backend.absoluteUrl(track.album.cover)">
<img class="ui mini image" v-if="track.album.cover" :src="$store.getters['instance/absoluteUrl'](track.album.cover)">
<img class="ui mini image" v-else src="../assets/audio/default-cover.png">
</td>
<td colspan="4">

View file

@ -14,7 +14,7 @@
<div v-if="currentTrack" class="track-area ui unstackable items">
<div class="ui inverted item">
<div class="ui tiny image">
<img ref="cover" @load="updateBackground" v-if="currentTrack.album.cover" :src="Track.getCover(currentTrack)">
<img ref="cover" @load="updateBackground" v-if="currentTrack.album.cover" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover)">
<img v-else src="../../assets/audio/default-cover.png">
</div>
<div class="middle aligned content">
@ -143,7 +143,6 @@ import {mapState, mapGetters, mapActions} from 'vuex'
import GlobalEvents from '@/components/utils/global-events'
import ColorThief from '@/vendor/color-thief'
import Track from '@/audio/track'
import AudioTrack from '@/components/audio/Track'
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
@ -162,7 +161,6 @@ export default {
isShuffling: false,
renderAudio: true,
sliderVolume: this.volume,
Track: Track,
defaultAmbiantColors: defaultAmbiantColors,
ambiantColors: defaultAmbiantColors
}

View file

@ -11,11 +11,8 @@
<script>
import jQuery from 'jquery'
import config from '@/config'
import router from '@/router'
const SEARCH_URL = config.API_URL + 'search?query={query}'
export default {
mounted () {
let self = this
@ -94,7 +91,7 @@ export default {
})
return {results: results}
},
url: SEARCH_URL
url: this.$store.getters['instance/absoluteUrl']('search?query={query}')
}
})
}

View file

@ -49,7 +49,7 @@ export default {
return []
}
let sources = [
{type: file.mimetype, url: file.path}
{type: file.mimetype, url: this.$store.getters['instance/absoluteUrl'](file.path)}
]
if (this.$store.state.auth.authenticated) {
// we need to send the token directly in url

View file

@ -2,7 +2,7 @@
<div class="ui card">
<div class="content">
<div class="right floated tiny ui image">
<img v-if="album.cover" v-lazy="backend.absoluteUrl(album.cover)">
<img v-if="album.cover" v-lazy="$store.getters['instance/absoluteUrl'](album.cover)">
<img v-else src="../../../assets/audio/default-cover.png">
</div>
<div class="header">

View file

@ -11,7 +11,7 @@
<tbody>
<tr v-for="album in albums">
<td>
<img class="ui mini image" v-if="album.cover" :src="backend.absoluteUrl(album.cover)">
<img class="ui mini image" v-if="album.cover" :src="$store.getters['instance/absoluteUrl'](album.cover)">
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
</td>
<td colspan="4">

View file

@ -4,7 +4,7 @@
<play-button class="basic icon" :discrete="true" :track="track"></play-button>
</td>
<td>
<img class="ui mini image" v-if="track.album.cover" v-lazy="backend.absoluteUrl(track.album.cover)">
<img class="ui mini image" v-if="track.album.cover" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover)">
<img class="ui mini image" v-else src="../../..//assets/audio/default-cover.png">
</td>
<td colspan="6">

View file

@ -35,7 +35,7 @@
<pre>
export PRIVATE_TOKEN="{{ $store.state.auth.token }}"
<template v-for="track in tracks"><template v-if="track.files.length > 0">
curl -G -o "{{ track.files[0].filename }}" <template v-if="$store.state.auth.authenticated">--header "Authorization: JWT $PRIVATE_TOKEN"</template> "{{ backend.absoluteUrl(track.files[0].path) }}"</template></template>
curl -G -o "{{ track.files[0].filename }}" <template v-if="$store.state.auth.authenticated">--header "Authorization: JWT $PRIVATE_TOKEN"</template> "{{ $store.getters['instance/absoluteUrl'](track.files[0].path) }}"</template></template>
</pre>
</div>
</div>

View file

@ -87,7 +87,7 @@ export default {
if (!this.album.cover) {
return ''
}
return 'background-image: url(' + backend.absoluteUrl(this.album.cover) + ')'
return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.album.cover) + ')'
}
},
watch: {

View file

@ -127,7 +127,7 @@ export default {
if (!this.cover) {
return ''
}
return 'background-image: url(' + backend.absoluteUrl(this.cover) + ')'
return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.cover) + ')'
}
},
watch: {

View file

@ -108,7 +108,6 @@ import time from '@/utils/time'
import axios from 'axios'
import url from '@/utils/url'
import logger from '@/logging'
import backend from '@/audio/backend'
import PlayButton from '@/components/audio/PlayButton'
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
@ -169,7 +168,7 @@ export default {
},
downloadUrl () {
if (this.track.files.length > 0) {
let u = backend.absoluteUrl(this.track.files[0].path)
let u = this.$store.getters['instance/absoluteUrl'](this.track.files[0].path)
if (this.$store.state.auth.authenticated) {
u = url.updateQueryString(u, 'jwt', this.$store.state.auth.token)
}
@ -191,7 +190,7 @@ export default {
if (!this.cover) {
return ''
}
return 'background-image: url(' + backend.absoluteUrl(this.cover) + ')'
return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.cover) + ')'
}
},
watch: {

View file

@ -63,7 +63,6 @@
</template>
<script>
import axios from 'axios'
import config from '@/config'
import $ from 'jquery'
import _ from 'lodash'
@ -86,7 +85,7 @@ export default {
return {
checkResult: null,
showCandidadesModal: false,
exclude: config.not
exclude: this.config.not
}
},
mounted: function () {

View file

@ -43,8 +43,6 @@
<script>
import axios from 'axios'
import backend from '@/audio/backend'
export default {
data () {
return {
@ -72,7 +70,7 @@ export default {
})
},
getUrl (code) {
return backend.absoluteUrl(this.$router.resolve({name: 'signup', query: {invitation: code.toUpperCase()}}).href)
return this.$store.getters['instance/absoluteUrl'](this.$router.resolve({name: 'signup', query: {invitation: code.toUpperCase()}}).href)
}
}
}

View file

@ -22,7 +22,6 @@
<script>
import jQuery from 'jquery'
import config from '@/config'
export default {
props: {
@ -117,7 +116,7 @@ export default {
})[0]
},
searchUrl: function () {
return config.API_URL + 'providers/musicbrainz/search/' + this.currentTypeObject.value + 's/?query={query}'
return this.$store.getters['instance/absoluteUrl']('providers/musicbrainz/search/' + this.currentTypeObject.value + 's/?query={query}')
},
types: function () {
return [

View file

@ -1,8 +0,0 @@
class Config {
constructor () {
this.BACKEND_URL = process.env.BACKEND_URL
this.API_URL = this.BACKEND_URL + 'api/v1/'
}
}
export default new Config()

View file

@ -15,7 +15,6 @@ import i18next from 'i18next'
import i18nextFetch from 'i18next-fetch-backend'
import VueI18Next from '@panter/vue-i18next'
import store from './store'
import config from './config'
import { sync } from 'vuex-router-sync'
import filters from '@/filters' // eslint-disable-line
import globals from '@/components/globals' // eslint-disable-line
@ -56,8 +55,6 @@ Vue.directive('title', {
document.title = parts.join(' - ')
}
})
axios.defaults.baseURL = config.API_URL
axios.interceptors.request.use(function (config) {
// Do something before request is sent
if (store.state.auth.token) {
@ -104,7 +101,6 @@ axios.interceptors.response.use(function (response) {
// Do something with response error
return Promise.reject(error)
})
store.dispatch('auth/check')
// i18n
i18next

View file

@ -34,7 +34,7 @@ export default new Vuex.Store({
}),
createPersistedState({
key: 'instance',
paths: ['instance.events']
paths: ['instance.events', 'instance.instanceUrl']
}),
createPersistedState({
key: 'radios',

View file

@ -6,6 +6,7 @@ export default {
namespaced: true,
state: {
maxEvents: 200,
instanceUrl: process.env.INSTANCE_URL,
events: [],
settings: {
instance: {
@ -51,9 +52,46 @@ export default {
},
events: (state, value) => {
state.events = value
},
instanceUrl: (state, value) => {
state.instanceUrl = value
if (!value) {
axios.defaults.baseURL = null
return
}
let apiUrl
let suffix = 'api/v1/'
if (state.instanceUrl.endsWith('/')) {
apiUrl = state.instanceUrl + suffix
} else {
apiUrl = state.instanceUrl + '/' + suffix
}
axios.defaults.baseURL = apiUrl
}
},
getters: {
absoluteUrl: (state) => (relativeUrl) => {
if (relativeUrl.startsWith('http')) {
return relativeUrl
}
return state.instanceUrl + relativeUrl
}
},
actions: {
setUrl ({commit, dispatch}, url) {
commit('instanceUrl', url)
let modules = [
'auth',
'favorites',
'player',
'playlists',
'queue',
'radios'
]
modules.forEach(m => {
commit(`${m}/reset`, null, {root: true})
})
},
// Send a request to the login URL and save the returned JWT
fetchSettings ({commit}, payload) {
return axios.get('instance/settings/').then(response => {