Skip to content
Snippets Groups Projects
Commit 7d60155b authored by Eliot Berriot's avatar Eliot Berriot
Browse files

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
parents 2182227f 6c066881
No related branches found
No related tags found
No related merge requests found
Showing
with 211 additions and 86 deletions
......@@ -7,11 +7,53 @@ variables:
stages:
- review
- lint
- test
- build
- deploy
review:
stage: review
image: node:9
when: manual
allow_failure: true
before_script:
- cd front
script:
- yarn install
# this is to ensure we don't have any errors in the output,
# cf https://code.eliotberriot.com/funkwhale/funkwhale/issues/169
- INSTANCE_URL=$REVIEW_INSTANCE_URL yarn run build | tee /dev/stderr | (! grep -i 'ERROR in')
- mkdir -p /static/$CI_BUILD_REF_SLUG
- cp -r dist/* /static/$CI_BUILD_REF_SLUG
cache:
key: "$CI_PROJECT_ID__front_dependencies"
paths:
- front/node_modules
- front/yarn.lock
environment:
name: review/$CI_BUILD_REF_NAME
url: http://$CI_BUILD_REF_SLUG.$REVIEW_DOMAIN
on_stop: stop_review
only:
- branches@funkwhale/funkwhale
tags:
- funkwhale-review
stop_review:
stage: review
script:
- rm -rf /static/$CI_BUILD_REF_SLUG/
variables:
GIT_STRATEGY: none
when: manual
environment:
name: review/$CI_BUILD_REF_NAME
action: stop
tags:
- funkwhale-review
black:
image: python:3.6
stage: lint
......
......@@ -12,6 +12,42 @@ This document will guide you through common operations such as:
- Writing unit tests to validate your work
- Submit your work
A quick path to contribute on the front-end
-------------------------------------------
The next sections of this document include a full installation guide to help
you setup a local, development version of Funkwhale. If you only want to fix small things
on the front-end, and don't want to manage a full development environment, there is anoter way.
As the front-end can work with any Funkwhale server, you can work with the front-end only,
and make it talk with an existing instance (like the demo one, or you own instance, if you have one).
If even that is too much for you, you can also make your changes without any development environment,
and open a merge request. We will be able to to review your work easily by spawning automatically a
live version of your changes, thanks to Gitlab Review apps.
Setup front-end only development environment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1. Clone the repository::
git clone ssh://git@code.eliotberriot.com:2222/funkwhale/funkwhale.git
cd funkwhale
cd front
2. Install [nodejs](https://nodejs.org/en/download/package-manager/) and [yarn](https://yarnpkg.com/lang/en/docs/install/#debian-stable)
3. Install the dependencies::
yarn install
4. Launch the development server::
# this will serve the front-end on http://localhost:8000
WEBPACK_DEVSERVER_PORT=8000 yarn dev
5. Make the front-end talk with an existing server (like https://demo.funkwhale.audio),
by clicking on the corresponding link in the footer
6. Start hacking!
Setup your development environment
----------------------------------
......
Funkwhale's front-end can now point to any instance (#327)
Removed front-end and back-end coupling
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Eventhough Funkwhale's front-end has always been a Single Page Application,
talking to an API, it was only able to talk to an API on the same domain.
There was no real technical justification behind this (only lazyness), and it was
also blocking interesting use cases:
- Use multiple customized versions of the front-end with the same instance
- Use a customized version of the front-end with multiple instances
- Use a locally hosted front-end with a remote API, which is especially useful in development
From now on, Funkwhale's front-end can connect to any Funkwhale server. You can
change the server you are connecting to in the footer.
Fixing this also unlocked a really interesting feature in our development/review workflow:
by leveraging Gitlab CI and review apps, we are now able to deploy automatically live versions of
a merge request, making it possible for anyone to review front-end changes easily, without
the need to install a local environment.
let url = process.env.INSTANCE_URL || '/'
module.exports = {
NODE_ENV: '"production"',
BACKEND_URL: '"/"'
INSTANCE_URL: `"${url}"`
}
<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{
......
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
}
import backend from './backend'
export default {
getCover (track) {
return backend.absoluteUrl(track.album.cover)
}
}
......@@ -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">
......
......@@ -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
}
......
......@@ -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}')
}
})
}
......
......@@ -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
......
......@@ -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">
......
......@@ -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">
......
......@@ -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">
......
......@@ -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>
......
......@@ -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: {
......
......@@ -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: {
......
......@@ -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: {
......
......@@ -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 () {
......
......@@ -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)
}
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment