funkwhale/front/src/App.vue

461 wiersze
15 KiB
Vue
Czysty Zwykły widok Historia

<template>
2021-11-26 11:01:58 +00:00
<div
id="app"
:key="String($store.state.instance.instanceUrl)"
2022-02-21 14:07:07 +00:00
:class="[$store.state.ui.queueFocused ? 'queue-focused' : '',
2022-02-21 18:53:34 +00:00
{'has-bottom-player': $store.state.queue.tracks.length > 0}]"
2021-11-26 11:01:58 +00:00
>
<!-- here, we display custom stylesheets, if any -->
2018-12-04 14:13:37 +00:00
<link
v-for="url in customStylesheets"
2021-11-26 11:01:58 +00:00
:key="url"
2018-12-04 14:13:37 +00:00
rel="stylesheet"
property="stylesheet"
:href="url"
>
2021-11-26 11:01:58 +00:00
<sidebar
:width="width"
@show:set-instance-modal="showSetInstanceModal = !showSetInstanceModal"
@show:shortcuts-modal="showShortcutsModal = !showShortcutsModal"
/>
<set-instance-modal
:show="showSetInstanceModal"
@update:show="showSetInstanceModal = $event"
/>
<service-messages />
<transition name="queue">
<queue
v-if="$store.state.ui.queueFocused"
@touch-progress="$refs.player.setCurrentTime($event)"
/>
</transition>
<router-view
role="main"
:class="{hidden: $store.state.ui.queueFocused}"
/>
<player ref="player" />
<playlist-modal v-if="$store.state.auth.authenticated" />
<channel-upload-modal v-if="$store.state.auth.authenticated" />
<filter-modal v-if="$store.state.auth.authenticated" />
<report-modal />
<shortcuts-modal
:show="showShortcutsModal"
@update:show="showShortcutsModal = $event"
/>
<GlobalEvents @keydown.h.exact="showShortcutsModal = !showShortcutsModal" />
</div>
</template>
<script>
import axios from 'axios'
import _ from 'lodash'
2021-11-26 11:01:58 +00:00
import { mapState, mapGetters } from 'vuex'
import { useWebSocket, whenever } from '@vueuse/core'
2022-02-21 14:07:07 +00:00
import GlobalEvents from '@/components/utils/global-events.vue'
import locales from './locales'
2021-11-26 11:01:58 +00:00
import { getClientOnlyRadio } from '@/radios'
2018-03-20 18:57:34 +00:00
2022-02-21 17:00:40 +00:00
import Player from '@/components/audio/Player.vue'
import Queue from '@/components/Queue.vue'
import PlaylistModal from '@/components/playlists/PlaylistModal.vue'
2022-02-21 18:53:34 +00:00
import ChannelUploadModal from '@/components/channels/UploadModal.vue'
2022-02-21 17:00:40 +00:00
import Sidebar from '@/components/Sidebar.vue'
import ServiceMessages from '@/components/ServiceMessages.vue'
import SetInstanceModal from '@/components/SetInstanceModal.vue'
2022-02-21 18:53:34 +00:00
import ShortcutsModal from '@/components/ShortcutsModal.vue'
2022-02-21 17:00:40 +00:00
import FilterModal from '@/components/moderation/FilterModal.vue'
import ReportModal from '@/components/moderation/ReportModal.vue'
import { watch, watchEffect } from '@vue/composition-api'
2022-02-21 17:00:40 +00:00
export default {
2021-11-26 11:01:58 +00:00
name: 'App',
2018-02-17 20:23:45 +00:00
components: {
2022-02-21 17:00:40 +00:00
Player,
Queue,
PlaylistModal,
ChannelUploadModal,
Sidebar,
ServiceMessages,
SetInstanceModal,
ShortcutsModal,
FilterModal,
ReportModal,
2021-11-26 11:01:58 +00:00
GlobalEvents
2018-02-17 20:23:45 +00:00
},
setup (props, { root }) {
const store = root.$store
const url = store.getters['instance/absoluteUrl']('api/v1/activity')
.replace(/^http/, 'ws')
const { data, status, open, close } = useWebSocket(url, {
autoReconnect: true,
immediate: false
})
watch(() => store.state.auth.authenticated, (authenticated) => {
if (authenticated) return open()
close()
})
whenever(data, () => {
store.dispatch('ui/websocketEvent', JSON.parse(data.value))
})
watchEffect(() => {
console.log('Websocket status:', status.value)
})
},
data () {
return {
instanceUrl: null,
showShortcutsModal: false,
showSetInstanceModal: false,
initialTitle: document.title,
width: window.innerWidth
}
},
2021-11-26 11:01:58 +00:00
computed: {
...mapState({
messages: state => state.ui.messages,
nodeinfo: state => state.instance.nodeinfo,
playing: state => state.player.playing,
bufferProgress: state => state.player.bufferProgress,
isLoadingAudio: state => state.player.isLoadingAudio,
serviceWorker: state => state.ui.serviceWorker
}),
...mapGetters({
hasNext: 'queue/hasNext',
currentTrack: 'queue/currentTrack',
progress: 'player/progress'
}),
labels () {
const play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Play track')
const pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Pause track')
const next = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Next track')
const expandQueue = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Expand queue')
return {
play,
pause,
next,
expandQueue
}
},
suggestedInstances () {
const instances = this.$store.state.instance.knownInstances.slice(0)
if (this.$store.state.instance.frontSettings.defaultServerUrl) {
let serverUrl = this.$store.state.instance.frontSettings.defaultServerUrl
if (!serverUrl.endsWith('/')) {
serverUrl = serverUrl + '/'
}
instances.push(serverUrl)
}
instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/')
return _.uniq(instances.filter((e) => { return e }))
},
version () {
if (!this.nodeinfo) {
return null
}
return _.get(this.nodeinfo, 'software.version')
},
customStylesheets () {
if (this.$store.state.instance.frontSettings) {
return this.$store.state.instance.frontSettings.additionalStylesheets || []
}
return null
},
matchDarkColorScheme () {
if (window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)')
}
return null
2021-11-26 11:01:58 +00:00
}
},
watch: {
'$store.state.instance.instanceUrl' (v) {
this.$store.dispatch('instance/fetchSettings')
this.fetchNodeInfo()
},
'$store.state.ui.theme': {
immediate: true,
handler (newValue) {
const matchesDark = this.matchDarkColorScheme
if (matchesDark) {
if (newValue === 'system') {
newValue = matchesDark.matches ? 'dark' : 'light'
matchesDark.addEventListener('change', this.handleThemeChange)
} else {
matchesDark.removeEventListener('change', this.handleThemeChange)
}
} else {
if (newValue === 'system') {
newValue = 'light'
}
}
this.setTheme(newValue)
2021-11-26 11:01:58 +00:00
}
},
'$store.state.ui.currentLanguage': {
immediate: true,
handler (newValue) {
const self = this
const htmlLocale = newValue.toLowerCase().replace('_', '-')
document.documentElement.setAttribute('lang', htmlLocale)
if (newValue === 'en_US') {
self.$language.current = 'noop'
self.$language.current = newValue
return self.$store.commit('ui/momentLocale', 'en')
}
}
},
currentTrack: {
immediate: true,
handler (newValue) {
this.updateDocumentTitle()
}
},
'$store.state.ui.pageTitle': {
immediate: true,
handler (newValue) {
this.updateDocumentTitle()
}
},
'serviceWorker.updateAvailable': {
handler (v) {
if (!v) {
return
}
const self = this
this.$store.commit('ui/addMessage', {
content: this.$pgettext('App/Message/Paragraph', 'A new version of the app is available.'),
date: new Date(),
key: 'refreshApp',
displayTime: 0,
classActions: 'bottom attached opaque',
actions: [
{
text: this.$pgettext('App/Message/Paragraph', 'Update'),
class: 'primary',
click: function () {
self.updateApp()
}
},
{
text: this.$pgettext('App/Message/Paragraph', 'Later'),
class: 'basic'
}
]
})
},
immediate: true
}
},
2019-09-23 09:14:54 +00:00
async created () {
if (navigator.serviceWorker) {
navigator.serviceWorker.addEventListener(
'controllerchange', () => {
2021-11-26 11:01:58 +00:00
if (this.serviceWorker.refreshing) return
this.$store.commit('ui/serviceWorker', {
refreshing: true
})
2021-11-26 11:01:58 +00:00
window.location.reload()
}
2021-11-26 11:01:58 +00:00
)
}
2021-11-26 11:01:58 +00:00
window.addEventListener('resize', this.handleResize)
this.handleResize()
const self = this
if (!this.$store.state.ui.selectedLanguage) {
this.autodetectLanguage()
}
2018-03-01 22:46:32 +00:00
setInterval(() => {
// used to redraw ago dates every minute
self.$store.commit('ui/computeLastDate')
}, 1000 * 60)
2021-11-26 11:01:58 +00:00
const urlParams = new URLSearchParams(window.location.search)
2019-09-23 09:14:54 +00:00
const serverUrl = urlParams.get('_server')
if (serverUrl) {
this.$store.commit('instance/instanceUrl', serverUrl)
}
const url = urlParams.get('_url')
if (url) {
await this.$router.replace(url)
2021-11-26 11:01:58 +00:00
} else if (!this.$store.state.instance.instanceUrl) {
// we have several way to guess the API server url. By order of precedence:
// 1. use the url provided in settings.json, if any
// 2. use the url specified when building via VUE_APP_INSTANCE_URL
// 3. use the current url
2022-02-21 14:07:07 +00:00
const defaultInstanceUrl =
this.$store.state.instance.frontSettings.defaultServerUrl ||
2022-02-21 22:23:13 +00:00
import.meta.env.VUE_APP_INSTANCE_URL || this.$store.getters['instance/defaultUrl']()
this.$store.commit('instance/instanceUrl', defaultInstanceUrl)
2018-08-21 18:24:35 +00:00
} else {
// needed to trigger initialization of axios / service worker
2018-08-21 18:24:35 +00:00
this.$store.commit('instance/instanceUrl', this.$store.state.instance.instanceUrl)
}
2019-09-23 09:14:54 +00:00
await this.fetchNodeInfo()
this.$store.dispatch('instance/fetchSettings')
2018-09-13 15:18:23 +00:00
this.$store.commit('ui/addWebsocketEventHandler', {
eventName: 'inbox.item_added',
id: 'sidebarCount',
handler: this.incrementNotificationCountInSidebar
})
this.$store.commit('ui/addWebsocketEventHandler', {
eventName: 'mutation.created',
id: 'sidebarReviewEditCount',
handler: this.incrementReviewEditCountInSidebar
})
this.$store.commit('ui/addWebsocketEventHandler', {
eventName: 'mutation.updated',
id: 'sidebarReviewEditCount',
handler: this.incrementReviewEditCountInSidebar
})
this.$store.commit('ui/addWebsocketEventHandler', {
eventName: 'report.created',
id: 'sidebarPendingReviewReportCount',
handler: this.incrementPendingReviewReportsCountInSidebar
})
2020-03-18 10:57:33 +00:00
this.$store.commit('ui/addWebsocketEventHandler', {
eventName: 'user_request.created',
id: 'sidebarPendingReviewRequestCount',
handler: this.incrementPendingReviewRequestsCountInSidebar
})
this.$store.commit('ui/addWebsocketEventHandler', {
eventName: 'Listen',
id: 'handleListen',
handler: this.handleListen
})
2018-09-13 15:18:23 +00:00
},
mounted () {
2021-11-26 11:01:58 +00:00
const self = this
// slight hack to allow use to have internal links in <translate> tags
// while preserving router behaviour
document.documentElement.addEventListener('click', function (event) {
2021-11-26 11:01:58 +00:00
if (!event.target.matches('a.internal')) return
self.$router.push(event.target.getAttribute('href'))
2021-11-26 11:01:58 +00:00
event.preventDefault()
}, false)
this.$nextTick(() => {
document.getElementById('fake-content').classList.add('loaded')
})
},
2018-09-13 15:18:23 +00:00
destroyed () {
this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'inbox.item_added',
2021-11-26 11:01:58 +00:00
id: 'sidebarCount'
2018-09-13 15:18:23 +00:00
})
this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'mutation.created',
2021-11-26 11:01:58 +00:00
id: 'sidebarReviewEditCount'
})
this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'mutation.updated',
2021-11-26 11:01:58 +00:00
id: 'sidebarReviewEditCount'
})
this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'mutation.updated',
2021-11-26 11:01:58 +00:00
id: 'sidebarPendingReviewReportCount'
})
2020-03-18 10:57:33 +00:00
this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'user_request.created',
2021-11-26 11:01:58 +00:00
id: 'sidebarPendingReviewRequestCount'
2020-03-18 10:57:33 +00:00
})
this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'Listen',
2021-11-26 11:01:58 +00:00
id: 'handleListen'
})
},
methods: {
2018-09-13 15:18:23 +00:00
incrementNotificationCountInSidebar (event) {
2021-11-26 11:01:58 +00:00
this.$store.commit('ui/incrementNotifications', { type: 'inbox', count: 1 })
2018-09-13 15:18:23 +00:00
},
incrementReviewEditCountInSidebar (event) {
2021-11-26 11:01:58 +00:00
this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewEdits', value: event.pending_review_count })
},
incrementPendingReviewReportsCountInSidebar (event) {
2021-11-26 11:01:58 +00:00
this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewReports', value: event.unresolved_count })
},
2020-03-18 10:57:33 +00:00
incrementPendingReviewRequestsCountInSidebar (event) {
2021-11-26 11:01:58 +00:00
this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewRequests', value: event.pending_count })
2020-03-18 10:57:33 +00:00
},
handleListen (event) {
if (this.$store.state.radios.current && this.$store.state.radios.running) {
2021-11-26 11:01:58 +00:00
const current = this.$store.state.radios.current
if (current.clientOnly && current.type === 'account') {
getClientOnlyRadio(current).handleListen(current, event, this.$store)
}
}
},
2019-09-23 09:14:54 +00:00
async fetchNodeInfo () {
2021-11-26 11:01:58 +00:00
const response = await axios.get('instance/nodeinfo/2.0/')
2019-09-23 09:14:54 +00:00
this.$store.commit('instance/nodeinfo', response.data)
},
autodetectLanguage () {
2021-11-26 11:01:58 +00:00
const userLanguage = navigator.language || navigator.userLanguage
const available = locales.locales.map(e => { return e.code })
let candidate
2021-11-26 11:01:58 +00:00
const matching = available.filter((a) => {
return userLanguage.replace('-', '_') === a
})
2021-11-26 11:01:58 +00:00
const almostMatching = available.filter((a) => {
return userLanguage.replace('-', '_').split('_')[0] === a.split('_')[0]
})
if (matching.length > 0) {
candidate = matching[0]
} else if (almostMatching.length > 0) {
candidate = almostMatching[0]
} else {
return
}
this.$store.commit('ui/currentLanguage', candidate)
2018-09-13 15:18:23 +00:00
},
2021-11-26 11:01:58 +00:00
getTrackInformationText (track) {
const trackTitle = track.title
2020-02-05 14:06:07 +00:00
const albumArtist = (track.album) ? track.album.artist.name : null
const artistName = (
2020-02-05 14:06:07 +00:00
(track.artist) ? track.artist.name : albumArtist)
2022-02-21 18:53:34 +00:00
const text = `${trackTitle}${artistName}`
return text
},
2021-11-26 11:01:58 +00:00
updateDocumentTitle () {
const parts = []
const currentTrackPart = (
2021-11-26 11:01:58 +00:00
(this.currentTrack)
? this.getTrackInformationText(this.currentTrack)
: null)
if (currentTrackPart) {
parts.push(currentTrackPart)
}
if (this.$store.state.ui.pageTitle) {
parts.push(this.$store.state.ui.pageTitle)
}
parts.push(this.initialTitle || 'Funkwhale')
document.title = parts.join(' – ')
},
2020-02-25 13:43:14 +00:00
updateApp () {
2021-11-26 11:01:58 +00:00
this.$store.commit('ui/serviceWorker', { updateAvailable: false })
if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return }
this.serviceWorker.registration.waiting.postMessage({ command: 'skipWaiting' })
},
2021-11-26 11:01:58 +00:00
handleResize () {
this.width = window.innerWidth
},
handleThemeChange (event) {
this.setTheme(event.matches ? 'dark' : 'light')
},
setTheme (theme) {
const oldTheme = (theme === 'light') ? 'dark' : 'light'
2022-02-21 18:53:34 +00:00
document.body.classList.remove(`theme-${oldTheme}`)
document.body.classList.add(`theme-${theme}`)
}
}
}
</script>
<style lang="scss">
2018-12-19 21:04:35 +00:00
@import "style/_main";
</style>