Profile menu redesign

This commit is contained in:
Ciarán Ainsworth 2021-11-26 11:01:58 +00:00
parent 274bdd1d3e
commit 58df0d4529
13 changed files with 2173 additions and 909 deletions

View File

@ -165,6 +165,16 @@ def discard_unused_icons(rule):
".wrench", ".wrench",
".x", ".x",
".key", ".key",
".cog",
".life.ring",
".language",
".palette",
".sun",
".moon",
".gitlab",
".chevron",
".right",
".left"
] ]
if ":before" not in rule["lines"][0]: if ":before" not in rule["lines"][0]:
return False return False

View File

@ -1,35 +1,47 @@
<template> <template>
<div id="app" :key="String($store.state.instance.instanceUrl)" :class="[$store.state.ui.queueFocused ? 'queue-focused' : '', {'has-bottom-player': $store.state.queue.tracks.length > 0}, `is-${ $store.getters['ui/windowSize']}`]"> <div
id="app"
:key="String($store.state.instance.instanceUrl)"
:class="[$store.state.ui.queueFocused ? 'queue-focused' : '', {'has-bottom-player': $store.state.queue.tracks.length > 0}, `is-${ $store.getters['ui/windowSize']}`]"
>
<!-- here, we display custom stylesheets, if any --> <!-- here, we display custom stylesheets, if any -->
<link <link
v-for="url in customStylesheets" v-for="url in customStylesheets"
:key="url"
rel="stylesheet" rel="stylesheet"
property="stylesheet" property="stylesheet"
:href="url" :href="url"
:key="url"
> >
<template> <sidebar
<sidebar></sidebar> :width="width"
<set-instance-modal @update:show="showSetInstanceModal = $event" :show="showSetInstanceModal"></set-instance-modal> @show:set-instance-modal="showSetInstanceModal = !showSetInstanceModal"
<service-messages></service-messages> @show:shortcuts-modal="showShortcutsModal = !showShortcutsModal"
<transition name="queue"> />
<queue @touch-progress="$refs.player.setCurrentTime($event)" v-if="$store.state.ui.queueFocused"></queue> <set-instance-modal
</transition> :show="showSetInstanceModal"
<router-view role="main" :class="{hidden: $store.state.ui.queueFocused}"></router-view> @update:show="showSetInstanceModal = $event"
<player ref="player"></player> />
<app-footer <service-messages />
:class="{hidden: $store.state.ui.queueFocused}" <transition name="queue">
:version="version" <queue
@show:shortcuts-modal="showShortcutsModal = !showShortcutsModal" v-if="$store.state.ui.queueFocused"
@show:set-instance-modal="showSetInstanceModal = !showSetInstanceModal" @touch-progress="$refs.player.setCurrentTime($event)"
></app-footer> />
<playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal> </transition>
<channel-upload-modal v-if="$store.state.auth.authenticated"></channel-upload-modal> <router-view
<filter-modal v-if="$store.state.auth.authenticated"></filter-modal> role="main"
<report-modal></report-modal> :class="{hidden: $store.state.ui.queueFocused}"
<shortcuts-modal @update:show="showShortcutsModal = $event" :show="showShortcutsModal"></shortcuts-modal> />
<GlobalEvents @keydown.h.exact="showShortcutsModal = !showShortcutsModal"/> <player ref="player" />
</template> <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> </div>
</template> </template>
@ -37,28 +49,26 @@
import Vue from 'vue' import Vue from 'vue'
import axios from 'axios' import axios from 'axios'
import _ from '@/lodash' import _ from '@/lodash'
import {mapState, mapGetters, mapActions} from 'vuex' import { mapState, mapGetters } from 'vuex'
import { WebSocketBridge } from 'django-channels' import { WebSocketBridge } from 'django-channels'
import GlobalEvents from '@/components/utils/global-events' import GlobalEvents from '@/components/utils/global-events'
import moment from 'moment'
import locales from './locales' import locales from './locales'
import {getClientOnlyRadio} from '@/radios' import { getClientOnlyRadio } from '@/radios'
export default { export default {
name: 'app', name: 'App',
components: { components: {
Player: () => import(/* webpackChunkName: "audio" */ "@/components/audio/Player"), Player: () => import(/* webpackChunkName: "audio" */ '@/components/audio/Player'),
Queue: () => import(/* webpackChunkName: "audio" */ "@/components/Queue"), Queue: () => import(/* webpackChunkName: "audio" */ '@/components/Queue'),
PlaylistModal: () => import(/* webpackChunkName: "auth-audio" */ "@/components/playlists/PlaylistModal"), PlaylistModal: () => import(/* webpackChunkName: "auth-audio" */ '@/components/playlists/PlaylistModal'),
ChannelUploadModal: () => import(/* webpackChunkName: "auth-audio" */ "@/components/channels/UploadModal"), ChannelUploadModal: () => import(/* webpackChunkName: "auth-audio" */ '@/components/channels/UploadModal'),
Sidebar: () => import(/* webpackChunkName: "core" */ "@/components/Sidebar"), Sidebar: () => import(/* webpackChunkName: "core" */ '@/components/Sidebar'),
AppFooter: () => import(/* webpackChunkName: "core" */ "@/components/Footer"), ServiceMessages: () => import(/* webpackChunkName: "core" */ '@/components/ServiceMessages'),
ServiceMessages: () => import(/* webpackChunkName: "core" */ "@/components/ServiceMessages"), SetInstanceModal: () => import(/* webpackChunkName: "core" */ '@/components/SetInstanceModal'),
SetInstanceModal: () => import(/* webpackChunkName: "core" */ "@/components/SetInstanceModal"), ShortcutsModal: () => import(/* webpackChunkName: "core" */ '@/components/ShortcutsModal'),
ShortcutsModal: () => import(/* webpackChunkName: "core" */ "@/components/ShortcutsModal"), FilterModal: () => import(/* webpackChunkName: "moderation" */ '@/components/moderation/FilterModal'),
FilterModal: () => import(/* webpackChunkName: "moderation" */ "@/components/moderation/FilterModal"), ReportModal: () => import(/* webpackChunkName: "moderation" */ '@/components/moderation/ReportModal'),
ReportModal: () => import(/* webpackChunkName: "moderation" */ "@/components/moderation/ReportModal"), GlobalEvents
GlobalEvents,
}, },
data () { data () {
return { return {
@ -70,23 +80,168 @@ export default {
width: window.innerWidth width: window.innerWidth
} }
}, },
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
}
},
watch: {
'$store.state.instance.instanceUrl' (v) {
this.$store.dispatch('instance/fetchSettings')
this.fetchNodeInfo()
},
'$store.state.ui.theme': {
immediate: true,
handler (newValue, oldValue) {
const oldTheme = oldValue || 'light'
document.body.classList.remove(`theme-${oldTheme}`)
document.body.classList.add(`theme-${newValue}`)
}
},
'$store.state.auth.authenticated' (newValue) {
if (!newValue) {
this.disconnect()
} else {
this.openWebsocket()
}
},
'$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')
}
import(/* webpackChunkName: "locale-[request]" */ `./translations/${newValue}.json`).then((response) => {
Vue.$translations[newValue] = response.default[newValue]
}).finally(() => {
// set current language twice, otherwise we seem to have a cache somewhere
// and rendering does not happen
self.$language.current = 'noop'
self.$language.current = newValue
})
const momentLocale = newValue.replace('_', '-').toLowerCase()
import(/* webpackChunkName: "moment-locale-[request]" */ `moment/locale/${momentLocale}.js`).then(() => {
self.$store.commit('ui/momentLocale', momentLocale)
}).catch(() => {
console.log('No momentjs locale available for', momentLocale)
const shortLocale = momentLocale.split('-')[0]
import(/* webpackChunkName: "moment-locale-[request]" */ `moment/locale/${shortLocale}.js`).then(() => {
self.$store.commit('ui/momentLocale', shortLocale)
}).catch(() => {
console.log('No momentjs locale available for', shortLocale)
})
})
}
},
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
}
},
async created () { async created () {
if (navigator.serviceWorker) { if (navigator.serviceWorker) {
navigator.serviceWorker.addEventListener( navigator.serviceWorker.addEventListener(
'controllerchange', () => { 'controllerchange', () => {
if (this.serviceWorker.refreshing) return; if (this.serviceWorker.refreshing) return
this.$store.commit('ui/serviceWorker', { this.$store.commit('ui/serviceWorker', {
refreshing: true refreshing: true
}) })
window.location.reload(); window.location.reload()
} }
); )
} }
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize)
this.handleResize(); this.handleResize()
this.openWebsocket() this.openWebsocket()
let self = this const self = this
if (!this.$store.state.ui.selectedLanguage) { if (!this.$store.state.ui.selectedLanguage) {
this.autodetectLanguage() this.autodetectLanguage()
} }
@ -94,7 +249,7 @@ export default {
// used to redraw ago dates every minute // used to redraw ago dates every minute
self.$store.commit('ui/computeLastDate') self.$store.commit('ui/computeLastDate')
}, 1000 * 60) }, 1000 * 60)
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search)
const serverUrl = urlParams.get('_server') const serverUrl = urlParams.get('_server')
if (serverUrl) { if (serverUrl) {
this.$store.commit('instance/instanceUrl', serverUrl) this.$store.commit('instance/instanceUrl', serverUrl)
@ -102,13 +257,12 @@ export default {
const url = urlParams.get('_url') const url = urlParams.get('_url')
if (url) { if (url) {
this.$router.replace(url) this.$router.replace(url)
} } else if (!this.$store.state.instance.instanceUrl) {
else if (!this.$store.state.instance.instanceUrl) {
// we have several way to guess the API server url. By order of precedence: // we have several way to guess the API server url. By order of precedence:
// 1. use the url provided in settings.json, if any // 1. use the url provided in settings.json, if any
// 2. use the url specified when building via VUE_APP_INSTANCE_URL // 2. use the url specified when building via VUE_APP_INSTANCE_URL
// 3. use the current url // 3. use the current url
let defaultInstanceUrl = this.$store.state.instance.frontSettings.defaultServerUrl || process.env.VUE_APP_INSTANCE_URL || this.$store.getters['instance/defaultUrl']() const defaultInstanceUrl = this.$store.state.instance.frontSettings.defaultServerUrl || process.env.VUE_APP_INSTANCE_URL || this.$store.getters['instance/defaultUrl']()
this.$store.commit('instance/instanceUrl', defaultInstanceUrl) this.$store.commit('instance/instanceUrl', defaultInstanceUrl)
} else { } else {
// needed to trigger initialization of axios / service worker // needed to trigger initialization of axios / service worker
@ -153,80 +307,78 @@ export default {
}) })
}, },
mounted () { mounted () {
let self = this const self = this
// slight hack to allow use to have internal links in <translate> tags // slight hack to allow use to have internal links in <translate> tags
// while preserving router behaviour // while preserving router behaviour
document.documentElement.addEventListener('click', function (event) { document.documentElement.addEventListener('click', function (event) {
if (!event.target.matches('a.internal')) return; if (!event.target.matches('a.internal')) return
self.$router.push(event.target.getAttribute('href')) self.$router.push(event.target.getAttribute('href'))
event.preventDefault(); event.preventDefault()
}, false); }, false)
this.$nextTick(() => { this.$nextTick(() => {
document.getElementById('fake-content').classList.add('loaded') document.getElementById('fake-content').classList.add('loaded')
}) })
}, },
destroyed () { destroyed () {
this.$store.commit('ui/removeWebsocketEventHandler', { this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'inbox.item_added', eventName: 'inbox.item_added',
id: 'sidebarCount', id: 'sidebarCount'
}) })
this.$store.commit('ui/removeWebsocketEventHandler', { this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'mutation.created', eventName: 'mutation.created',
id: 'sidebarReviewEditCount', id: 'sidebarReviewEditCount'
}) })
this.$store.commit('ui/removeWebsocketEventHandler', { this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'mutation.updated', eventName: 'mutation.updated',
id: 'sidebarReviewEditCount', id: 'sidebarReviewEditCount'
}) })
this.$store.commit('ui/removeWebsocketEventHandler', { this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'mutation.updated', eventName: 'mutation.updated',
id: 'sidebarPendingReviewReportCount', id: 'sidebarPendingReviewReportCount'
}) })
this.$store.commit('ui/removeWebsocketEventHandler', { this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'user_request.created', eventName: 'user_request.created',
id: 'sidebarPendingReviewRequestCount', id: 'sidebarPendingReviewRequestCount'
}) })
this.$store.commit('ui/removeWebsocketEventHandler', { this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'Listen', eventName: 'Listen',
id: 'handleListen', id: 'handleListen'
}) })
this.disconnect() this.disconnect()
}, },
methods: { methods: {
incrementNotificationCountInSidebar (event) { incrementNotificationCountInSidebar (event) {
this.$store.commit('ui/incrementNotifications', {type: 'inbox', count: 1}) this.$store.commit('ui/incrementNotifications', { type: 'inbox', count: 1 })
}, },
incrementReviewEditCountInSidebar (event) { incrementReviewEditCountInSidebar (event) {
this.$store.commit('ui/incrementNotifications', {type: 'pendingReviewEdits', value: event.pending_review_count}) this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewEdits', value: event.pending_review_count })
}, },
incrementPendingReviewReportsCountInSidebar (event) { incrementPendingReviewReportsCountInSidebar (event) {
this.$store.commit('ui/incrementNotifications', {type: 'pendingReviewReports', value: event.unresolved_count}) this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewReports', value: event.unresolved_count })
}, },
incrementPendingReviewRequestsCountInSidebar (event) { incrementPendingReviewRequestsCountInSidebar (event) {
this.$store.commit('ui/incrementNotifications', {type: 'pendingReviewRequests', value: event.pending_count}) this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewRequests', value: event.pending_count })
}, },
handleListen (event) { handleListen (event) {
if (this.$store.state.radios.current && this.$store.state.radios.running) { if (this.$store.state.radios.current && this.$store.state.radios.running) {
let current = this.$store.state.radios.current const current = this.$store.state.radios.current
if (current.clientOnly && current.type === 'account') { if (current.clientOnly && current.type === 'account') {
getClientOnlyRadio(current).handleListen(current, event, this.$store) getClientOnlyRadio(current).handleListen(current, event, this.$store)
} }
} }
}, },
async fetchNodeInfo () { async fetchNodeInfo () {
let response = await axios.get('instance/nodeinfo/2.0/') const response = await axios.get('instance/nodeinfo/2.0/')
this.$store.commit('instance/nodeinfo', response.data) this.$store.commit('instance/nodeinfo', response.data)
}, },
autodetectLanguage () { autodetectLanguage () {
let userLanguage = navigator.language || navigator.userLanguage const userLanguage = navigator.language || navigator.userLanguage
let available = locales.locales.map(e => { return e.code }) const available = locales.locales.map(e => { return e.code })
let self = this
let candidate let candidate
let matching = available.filter((a) => { const matching = available.filter((a) => {
return userLanguage.replace('-', '_') === a return userLanguage.replace('-', '_') === a
}) })
let almostMatching = available.filter((a) => { const almostMatching = available.filter((a) => {
return userLanguage.replace('-', '_').split('_')[0] === a.split('_')[0] return userLanguage.replace('-', '_').split('_')[0] === a.split('_')[0]
}) })
if (matching.length > 0) { if (matching.length > 0) {
@ -242,15 +394,15 @@ export default {
if (!this.bridge) { if (!this.bridge) {
return return
} }
this.bridge.socket.close(1000, 'goodbye', {keepClosed: true}) this.bridge.socket.close(1000, 'goodbye', { keepClosed: true })
}, },
openWebsocket () { openWebsocket () {
if (!this.$store.state.auth.authenticated) { if (!this.$store.state.auth.authenticated) {
return return
} }
this.disconnect() this.disconnect()
let self = this const self = this
let token = this.$store.state.auth.token const token = this.$store.state.auth.token
// let token = 'test' // let token = 'test'
const bridge = new WebSocketBridge() const bridge = new WebSocketBridge()
this.bridge = bridge this.bridge = bridge
@ -260,7 +412,7 @@ export default {
bridge.connect( bridge.connect(
url, url,
[], [],
{reconnectInterval: 1000 * 60}) { reconnectInterval: 1000 * 60 })
bridge.listen(function (event) { bridge.listen(function (event) {
self.$store.dispatch('ui/websocketEvent', event) self.$store.dispatch('ui/websocketEvent', event)
}) })
@ -268,7 +420,7 @@ export default {
console.log('Connected to WebSocket') console.log('Connected to WebSocket')
}) })
}, },
getTrackInformationText(track) { getTrackInformationText (track) {
const trackTitle = track.title const trackTitle = track.title
const albumArtist = (track.album) ? track.album.artist.name : null const albumArtist = (track.album) ? track.album.artist.name : null
const artistName = ( const artistName = (
@ -276,11 +428,12 @@ export default {
const text = `${trackTitle} ${artistName}` const text = `${trackTitle} ${artistName}`
return text return text
}, },
updateDocumentTitle() { updateDocumentTitle () {
let parts = [] const parts = []
const currentTrackPart = ( const currentTrackPart = (
(this.currentTrack) ? this.getTrackInformationText(this.currentTrack) (this.currentTrack)
: null) ? this.getTrackInformationText(this.currentTrack)
: null)
if (currentTrackPart) { if (currentTrackPart) {
parts.push(currentTrackPart) parts.push(currentTrackPart)
} }
@ -292,158 +445,13 @@ export default {
}, },
updateApp () { updateApp () {
this.$store.commit('ui/serviceWorker', {updateAvailable: false}) this.$store.commit('ui/serviceWorker', { updateAvailable: false })
if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return; } if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return }
this.serviceWorker.registration.waiting.postMessage({command: 'skipWaiting'}) this.serviceWorker.registration.waiting.postMessage({ command: 'skipWaiting' })
}, },
handleResize() { handleResize () {
this.width = window.innerWidth this.width = window.innerWidth
} }
},
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() {
let play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Play track")
let pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Pause track")
let next = this.$pgettext('Sidebar/Player/Icon.Tooltip', "Next track")
let expandQueue = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Expand queue")
return {
play,
pause,
next,
expandQueue,
}
},
suggestedInstances () {
let 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 || []
}
},
},
watch: {
'$store.state.instance.instanceUrl' (v) {
this.$store.dispatch('instance/fetchSettings')
this.fetchNodeInfo()
},
'$store.state.ui.theme': {
immediate: true,
handler (newValue, oldValue) {
let oldTheme = oldValue || 'light'
document.body.classList.remove(`theme-${oldTheme}`)
document.body.classList.add(`theme-${newValue}`)
},
},
'$store.state.auth.authenticated' (newValue) {
if (!newValue) {
this.disconnect()
} else {
this.openWebsocket()
}
},
'$store.state.ui.currentLanguage': {
immediate: true,
handler(newValue) {
let self = this
let 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')
}
import(/* webpackChunkName: "locale-[request]" */ `./translations/${newValue}.json`).then((response) =>{
Vue.$translations[newValue] = response.default[newValue]
}).finally(() => {
// set current language twice, otherwise we seem to have a cache somewhere
// and rendering does not happen
self.$language.current = 'noop'
self.$language.current = newValue
})
let momentLocale = newValue.replace('_', '-').toLowerCase()
import(/* webpackChunkName: "moment-locale-[request]" */ `moment/locale/${momentLocale}.js`).then(() => {
self.$store.commit('ui/momentLocale', momentLocale)
}).catch(() => {
console.log('No momentjs locale available for', momentLocale)
let shortLocale = momentLocale.split('-')[0]
import(/* webpackChunkName: "moment-locale-[request]" */ `moment/locale/${shortLocale}.js`).then(() => {
self.$store.commit('ui/momentLocale', shortLocale)
}).catch(() => {
console.log('No momentjs locale available for', shortLocale)
})
})
}
},
'currentTrack': {
immediate: true,
handler(newValue) {
this.updateDocumentTitle()
},
},
'$store.state.ui.pageTitle': {
immediate: true,
handler(newValue) {
this.updateDocumentTitle()
},
},
'serviceWorker.updateAvailable': {
handler (v) {
if (!v) {
return
}
let 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,
}
} }
} }
</script> </script>

View File

@ -1,252 +1,575 @@
<template> <template>
<aside :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar', 'component-sidebar']"> <aside :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar', 'component-sidebar']">
<header class="ui basic segment header-wrapper"> <header class="ui basic segment header-wrapper">
<router-link :title="'Funkwhale'" :to="{name: logoUrl}"> <router-link
<i class="logo bordered inverted vibrant big icon"> :title="'Funkwhale'"
<logo class="logo"></logo> :to="{name: logoUrl}"
<span class="visually-hidden">Home</span> >
</i> <i class="logo bordered inverted vibrant big icon">
</router-link> <logo class="logo" />
<router-link v-if="!$store.state.auth.authenticated" class="logo-wrapper" :to="{name: logoUrl}" :title="'Funkwhale'"> <span class="visually-hidden">Home</span>
<img src="../assets/logo/text-white.svg" alt="" /> </i>
</router-link> </router-link>
<nav class="top ui compact right aligned inverted text menu"> <nav class="top ui compact right aligned inverted text menu">
<template v-if="$store.state.auth.authenticated">
<div class="right menu"> <div class="right menu">
<div class="item" :title="labels.administration" v-if="$store.state.auth.availablePermissions['settings'] || $store.state.auth.availablePermissions['moderation']"> <div
v-if="$store.state.auth.availablePermissions['settings'] || $store.state.auth.availablePermissions['moderation']"
class="item"
:title="labels.administration"
>
<div class="item ui inline admin-dropdown dropdown"> <div class="item ui inline admin-dropdown dropdown">
<i class="wrench icon"></i> <i class="wrench icon" />
<div <div
v-if="moderationNotifications > 0" v-if="moderationNotifications > 0"
:class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']">{{ moderationNotifications }}</div> :class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']"
>
{{ moderationNotifications }}
</div>
<div class="menu"> <div class="menu">
<h3 class="header"> <h3 class="header">
<translate translate-context="Sidebar/Admin/Title/Noun">Administration</translate> <translate translate-context="Sidebar/Admin/Title/Noun">
Administration
</translate>
</h3> </h3>
<div class="divider"></div> <div class="divider" />
<router-link <router-link
v-if="$store.state.auth.availablePermissions['library']" v-if="$store.state.auth.availablePermissions['library']"
class="item" class="item"
:to="{name: 'manage.library.edits', query: {q: 'is_approved:null'}}"> :to="{name: 'manage.library.edits', query: {q: 'is_approved:null'}}"
>
<div <div
v-if="$store.state.ui.notifications.pendingReviewEdits > 0" v-if="$store.state.ui.notifications.pendingReviewEdits > 0"
:title="labels.pendingReviewEdits" :title="labels.pendingReviewEdits"
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']"> :class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']"
{{ $store.state.ui.notifications.pendingReviewEdits }}</div> >
<translate translate-context="*/*/*/Noun">Library</translate> {{ $store.state.ui.notifications.pendingReviewEdits }}
</div>
<translate translate-context="*/*/*/Noun">
Library
</translate>
</router-link> </router-link>
<router-link <router-link
v-if="$store.state.auth.availablePermissions['moderation']" v-if="$store.state.auth.availablePermissions['moderation']"
class="item" class="item"
:to="{name: 'manage.moderation.reports.list', query: {q: 'resolved:no'}}"> :to="{name: 'manage.moderation.reports.list', query: {q: 'resolved:no'}}"
>
<div <div
v-if="$store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests> 0" v-if="$store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests> 0"
:title="labels.pendingReviewReports" :title="labels.pendingReviewReports"
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']">{{ $store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests }}</div> :class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']"
<translate translate-context="*/Moderation/*">Moderation</translate> >
{{ $store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests }}
</div>
<translate translate-context="*/Moderation/*">
Moderation
</translate>
</router-link> </router-link>
<router-link <router-link
v-if="$store.state.auth.availablePermissions['settings']" v-if="$store.state.auth.availablePermissions['settings']"
class="item" class="item"
:to="{name: 'manage.users.users.list'}"> :to="{name: 'manage.users.users.list'}"
<translate translate-context="*/*/*/Noun">Users</translate> >
<translate translate-context="*/*/*/Noun">
Users
</translate>
</router-link> </router-link>
<router-link <router-link
v-if="$store.state.auth.availablePermissions['settings']" v-if="$store.state.auth.availablePermissions['settings']"
class="item" class="item"
:to="{path: '/manage/settings'}"> :to="{path: '/manage/settings'}"
<translate translate-context="*/*/*/Noun">Settings</translate> >
<translate translate-context="*/*/*/Noun">
Settings
</translate>
</router-link> </router-link>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<router-link <router-link
class="item"
v-if="$store.state.auth.authenticated" v-if="$store.state.auth.authenticated"
:to="{name: 'content.index'}"> class="item"
<i class="upload icon"></i> :to="{name: 'content.index'}"
<span class="visually-hidden">{{ labels.addContent }}</span> >
<i class="upload icon" />
<span class="visually-hidden">{{ labels.addContent }}</span>
</router-link> </router-link>
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'notifications'}"> <template v-if="width > 768">
<i class="bell icon"></i> <div class="item">
<div v-if="$store.state.ui.notifications.inbox + additionalNotifications > 0" :class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']"> <div class="ui user-dropdown dropdown">
{{ $store.state.ui.notifications.inbox + additionalNotifications }} <img
</div> v-if="$store.state.auth.authenticated && $store.state.auth.profile.avatar && $store.state.auth.profile.avatar.urls.medium_square_crop"
<span v-else class="visually-hidden">{{ labels.notifications }}</span> class="ui avatar image"
</router-link> alt=""
<div class="item"> :src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.urls.medium_square_crop)"
<div class="ui user-dropdown dropdown" > >
<img class="ui avatar image" alt="" v-if="$store.state.auth.profile.avatar && $store.state.auth.profile.avatar.urls.medium_square_crop" :src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.urls.medium_square_crop)" /> <actor-avatar
<actor-avatar v-else :actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username}" /> v-else-if="$store.state.auth.authenticated"
<div class="menu"> :actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username,}"
<router-link class="item" :to="{name: 'profile.overview', params: {username: $store.state.auth.username}}"><translate translate-context="*/*/*/Noun">Profile</translate></router-link> />
<router-link class="item" :to="{path: '/settings'}"><translate translate-context="*/*/*/Noun">Settings</translate></router-link> <i
<router-link class="item" :to="{name: 'logout'}"><translate translate-context="Sidebar/Login/List item.Link/Verb">Logout</translate></router-link> v-else
class="cog icon"
/>
<div
v-if="$store.state.ui.notifications.inbox + additionalNotifications > 0"
:class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']"
>
{{ $store.state.ui.notifications.inbox + additionalNotifications }}
</div>
<user-menu
:width="width"
v-on="$listeners"
/>
</div> </div>
</div> </div>
</div> </template>
</template> <template v-else>
<div class="item collapse-button-wrapper"> <a
href=""
<button class="item"
@click="isCollapsed = !isCollapsed" @click.prevent.exact="showUserModal = !showUserModal"
:class="['ui', 'basic', 'big', {'vibrant': !isCollapsed}, 'inverted icon', 'collapse', 'button']"> >
<i class="sidebar icon"></i></button> <img
</div> v-if="$store.state.auth.authenticated && $store.state.auth.profile.avatar && $store.state.auth.profile.avatar.urls.medium_square_crop"
</nav> class="ui avatar image"
</header> alt=""
<div class="ui basic search-wrapper segment"> :src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.urls.medium_square_crop)"
<search-bar @search="isCollapsed = false"></search-bar> >
</div> <actor-avatar
<div v-if="!$store.state.auth.authenticated" class="ui basic signup segment"> v-else-if="$store.state.auth.authenticated"
<router-link class="ui fluid tiny primary button" :to="{name: 'login'}"><translate translate-context="*/Login/*/Verb">Login</translate></router-link> :actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username,}"
<div class="ui small hidden divider"></div> />
<router-link class="ui fluid tiny button" :to="{path: '/signup'}"> <i
<translate translate-context="*/Signup/Link/Verb">Create an account</translate> v-else
</router-link> class="cog icon"
</div> />
<nav class="secondary" role="navigation" aria-labelledby="navigation-label"> <div
<h1 id="navigation-label" class="visually-hidden"> v-if="$store.state.ui.notifications.inbox + additionalNotifications > 0"
<translate translate-context="*/*/*">Main navigation</translate> :class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']"
</h1> >
<div class="ui small hidden divider"></div> {{ $store.state.ui.notifications.inbox + additionalNotifications }}
<section :class="['ui', 'bottom', 'attached', {active: selectedTab === 'library'}, 'tab']" :aria-label="labels.mainMenu"> </div>
<nav class="ui vertical large fluid inverted menu" role="navigation" :aria-label="labels.mainMenu"> </a>
<div :class="[{collapsed: !exploreExpanded}, 'collapsible item']"> </template>
<h2 class="header" role="button" @click="exploreExpanded = true" tabindex="0" @focus="exploreExpanded = true"> <user-modal
<translate translate-context="*/*/*/Verb">Explore</translate> :show="showUserModal"
<i class="angle right icon" v-if="!exploreExpanded"></i> @showThemeModalEvent="showThemeModal=true"
</h2> @showLanguageModalEvent="showLanguageModal=true"
<div class="menu"> @update:show="showUserModal = $event"
<router-link class="item" :to="{name: 'search'}"><i class="search icon"></i><translate translate-context="Sidebar/Navigation/List item.Link/Verb">Search</translate></router-link> />
<router-link class="item" :exact="true" :to="{name: 'library.index'}"><i class="music icon"></i><translate translate-context="Sidebar/Navigation/List item.Link/Verb">Browse</translate></router-link> <modal
<router-link class="item" :to="{name: 'library.podcasts.browse'}"><i class="podcast icon"></i><translate translate-context="*/*/*">Podcasts</translate></router-link> ref="languageModal"
<router-link class="item" :to="{name: 'library.albums.browse'}"><i class="compact disc icon"></i><translate translate-context="*/*/*">Albums</translate></router-link> :fullscreen="false"
<router-link class="item" :to="{name: 'library.artists.browse'}"><i class="user icon"></i><translate translate-context="*/*/*">Artists</translate></router-link> :show="showLanguageModal"
<router-link class="item" :to="{name: 'library.playlists.browse'}"><i class="list icon"></i><translate translate-context="*/*/*">Playlists</translate></router-link> @update:show="showLanguageModal = $event"
<router-link class="item" :to="{name: 'library.radios.browse'}"><i class="feed icon"></i><translate translate-context="*/*/*">Radios</translate></router-link> >
<i
role="button"
class="left chevron back inside icon"
@click.prevent.exact="showUserModal = !showUserModal"
/>
<div class="header">
<h3 class="title">
{{ labels.language }}
</h3>
</div> </div>
</div> <div class="content">
<div :class="[{collapsed: !myLibraryExpanded}, 'collapsible item']" v-if="$store.state.auth.authenticated"> <fieldset
<h3 class="header" role="button" @click="myLibraryExpanded = true" tabindex="0" @focus="myLibraryExpanded = true"> v-for="(language, key) in $language.available"
<translate translate-context="*/*/*/Noun">My Library</translate> :key="key"
<i class="angle right icon" v-if="!myLibraryExpanded"></i> >
</h3> <input
<div class="menu"> :id="key"
<router-link class="item" :exact="true" :to="{name: 'library.me'}"><i class="music icon"></i><translate translate-context="Sidebar/Navigation/List item.Link/Verb">Browse</translate></router-link> v-model="languageSelection"
<router-link class="item" :to="{name: 'library.albums.me'}"><i class="compact disc icon"></i><translate translate-context="*/*/*">Albums</translate></router-link> type="radio"
<router-link class="item" :to="{name: 'library.artists.me'}"><i class="user icon"></i><translate translate-context="*/*/*">Artists</translate></router-link> name="language"
<router-link class="item" :to="{name: 'library.playlists.me'}"><i class="list icon"></i><translate translate-context="*/*/*">Playlists</translate></router-link> :value="key"
<router-link class="item" :to="{name: 'library.radios.me'}"><i class="feed icon"></i><translate translate-context="*/*/*">Radios</translate></router-link> >
<router-link class="item" :to="{name: 'favorites'}"><i class="heart icon"></i><translate translate-context="Sidebar/Favorites/List item.Link/Noun">Favorites</translate></router-link> <label :for="key">{{ language }}</label>
</fieldset>
</div> </div>
</div> </modal>
<router-link class="header item" :to="{name: 'subscriptions'}" v-if="$store.state.auth.authenticated"> <modal
<translate translate-context="*/*/*">Channels</translate> ref="themeModal"
</router-link> :fullscreen="false"
<div class="item"> :show="showThemeModal"
<h3 class="header"> @update:show="showThemeModal = $event"
<translate translate-context="Footer/About/List item.Link">More</translate> >
</h3> <i
<div class="menu"> role="button"
<router-link class="item" to="/about"> class="left chevron back inside icon"
<i class="info icon"></i><translate translate-context="Sidebar/*/List item.Link">About this pod</translate> @click.prevent.exact="showUserModal = !showUserModal"
</router-link> />
<div class="header">
<h3 class="title">
{{ labels.theme }}
</h3>
</div> </div>
<div class="content">
<fieldset
v-for="theme in themes"
:key="theme.key"
>
<input
:id="theme.key"
v-model="themeSelection"
type="radio"
name="theme"
:value="theme.key"
>
<label :for="theme.key">{{ theme.name }}</label>
</fieldset>
</div>
</modal>
<div class="item collapse-button-wrapper">
<button
:class="['ui', 'basic', 'big', {'vibrant': !isCollapsed}, 'inverted icon', 'collapse', 'button']"
@click="isCollapsed = !isCollapsed"
>
<i class="sidebar icon" />
</button>
</div> </div>
</nav> </nav>
</section> </header>
</nav> <div class="ui basic search-wrapper segment">
</aside> <search-bar @search="isCollapsed = false" />
</div>
<div
v-if="!$store.state.auth.authenticated"
class="ui basic signup segment"
>
<router-link
class="ui fluid tiny primary button"
:to="{name: 'login'}"
>
<translate translate-context="*/Login/*/Verb">
Login
</translate>
</router-link>
<div class="ui small hidden divider" />
<router-link
class="ui fluid tiny button"
:to="{path: '/signup'}"
>
<translate translate-context="*/Signup/Link/Verb">
Create an account
</translate>
</router-link>
</div>
<nav
class="secondary"
role="navigation"
aria-labelledby="navigation-label"
>
<h1
id="navigation-label"
class="visually-hidden"
>
<translate translate-context="*/*/*">
Main navigation
</translate>
</h1>
<div class="ui small hidden divider" />
<section
:class="['ui', 'bottom', 'attached', {active: selectedTab === 'library'}, 'tab']"
:aria-label="labels.mainMenu"
>
<nav
class="ui vertical large fluid inverted menu"
role="navigation"
:aria-label="labels.mainMenu"
>
<div :class="[{collapsed: !exploreExpanded}, 'collapsible item']">
<h2
class="header"
role="button"
tabindex="0"
@click="exploreExpanded = true"
@focus="exploreExpanded = true"
>
<translate translate-context="*/*/*/Verb">
Explore
</translate>
<i
v-if="!exploreExpanded"
class="angle right icon"
/>
</h2>
<div class="menu">
<router-link
class="item"
:to="{name: 'search'}"
>
<i class="search icon" /><translate translate-context="Sidebar/Navigation/List item.Link/Verb">
Search
</translate>
</router-link>
<router-link
class="item"
:exact="true"
:to="{name: 'library.index'}"
>
<i class="music icon" /><translate translate-context="Sidebar/Navigation/List item.Link/Verb">
Browse
</translate>
</router-link>
<router-link
class="item"
:to="{name: 'library.podcasts.browse'}"
>
<i class="podcast icon" /><translate translate-context="*/*/*">
Podcasts
</translate>
</router-link>
<router-link
class="item"
:to="{name: 'library.albums.browse'}"
>
<i class="compact disc icon" /><translate translate-context="*/*/*">
Albums
</translate>
</router-link>
<router-link
class="item"
:to="{name: 'library.artists.browse'}"
>
<i class="user icon" /><translate translate-context="*/*/*">
Artists
</translate>
</router-link>
<router-link
class="item"
:to="{name: 'library.playlists.browse'}"
>
<i class="list icon" /><translate translate-context="*/*/*">
Playlists
</translate>
</router-link>
<router-link
class="item"
:to="{name: 'library.radios.browse'}"
>
<i class="feed icon" /><translate translate-context="*/*/*">
Radios
</translate>
</router-link>
</div>
</div>
<div
v-if="$store.state.auth.authenticated"
:class="[{collapsed: !myLibraryExpanded}, 'collapsible item']"
>
<h3
class="header"
role="button"
tabindex="0"
@click="myLibraryExpanded = true"
@focus="myLibraryExpanded = true"
>
<translate translate-context="*/*/*/Noun">
My Library
</translate>
<i
v-if="!myLibraryExpanded"
class="angle right icon"
/>
</h3>
<div class="menu">
<router-link
class="item"
:exact="true"
:to="{name: 'library.me'}"
>
<i class="music icon" /><translate translate-context="Sidebar/Navigation/List item.Link/Verb">
Browse
</translate>
</router-link>
<router-link
class="item"
:to="{name: 'library.albums.me'}"
>
<i class="compact disc icon" /><translate translate-context="*/*/*">
Albums
</translate>
</router-link>
<router-link
class="item"
:to="{name: 'library.artists.me'}"
>
<i class="user icon" /><translate translate-context="*/*/*">
Artists
</translate>
</router-link>
<router-link
class="item"
:to="{name: 'library.playlists.me'}"
>
<i class="list icon" /><translate translate-context="*/*/*">
Playlists
</translate>
</router-link>
<router-link
class="item"
:to="{name: 'library.radios.me'}"
>
<i class="feed icon" /><translate translate-context="*/*/*">
Radios
</translate>
</router-link>
<router-link
class="item"
:to="{name: 'favorites'}"
>
<i class="heart icon" /><translate translate-context="Sidebar/Favorites/List item.Link/Noun">
Favorites
</translate>
</router-link>
</div>
</div>
<router-link
v-if="$store.state.auth.authenticated"
class="header item"
:to="{name: 'subscriptions'}"
>
<translate translate-context="*/*/*">
Channels
</translate>
</router-link>
<div class="item">
<h3 class="header">
<translate translate-context="Footer/About/List item.Link">
More
</translate>
</h3>
<div class="menu">
<router-link
class="item"
to="/about"
>
<i class="info icon" /><translate translate-context="Sidebar/*/List item.Link">
About this pod
</translate>
</router-link>
</div>
</div>
<div
v-if="!production"
class="item"
>
<a
role="button"
href=""
class="link item"
@click.prevent="$emit('show:set-instance-modal')"
>Switch instance</a>
</div>
</nav>
</section>
</nav>
</aside>
</template> </template>
<script> <script>
import { mapState, mapActions, mapGetters } from "vuex" import { mapState, mapActions, mapGetters } from 'vuex'
import UserModal from '@/components/common/UserModal'
import Logo from '@/components/Logo'
import SearchBar from '@/components/audio/SearchBar'
import UserMenu from '@/components/common/UserMenu'
import Modal from '@/components/semantic/Modal'
import Logo from "@/components/Logo" import $ from 'jquery'
import SearchBar from "@/components/audio/SearchBar"
import $ from "jquery"
export default { export default {
name: "sidebar", name: 'Sidebar',
components: { components: {
SearchBar, SearchBar,
Logo Logo,
UserMenu,
UserModal,
Modal
}, },
data() { props: {
width: { type: Number, required: true }
},
data () {
return { return {
selectedTab: "library", selectedTab: 'library',
isCollapsed: true, isCollapsed: true,
fetchInterval: null, fetchInterval: null,
exploreExpanded: false, exploreExpanded: false,
myLibraryExpanded: false, myLibraryExpanded: false,
showUserModal: false,
showLanguageModal: false,
showThemeModal: false,
languageSelection: this.$language.current,
themeSelection: this.$store.state.ui.theme
} }
}, },
destroy() { destroy () {
if (this.fetchInterval) { if (this.fetchInterval) {
clearInterval(this.fetchInterval) clearInterval(this.fetchInterval)
} }
}, },
mounted () {
this.$nextTick(() => {
document.getElementById('fake-sidebar').classList.add('loaded')
})
},
computed: { computed: {
...mapGetters({
additionalNotifications: "ui/additionalNotifications",
}),
...mapState({ ...mapState({
queue: state => state.queue, queue: state => state.queue,
url: state => state.route.path url: state => state.route.path
}), }),
labels() { ...mapGetters({
let mainMenu = this.$pgettext('Sidebar/*/Hidden text', "Main menu") additionalNotifications: 'ui/additionalNotifications'
let selectTrack = this.$pgettext('Sidebar/Player/Hidden text', "Play this track") }),
let pendingFollows = this.$pgettext('Sidebar/Notifications/Hidden text', "Pending follow requests") labels () {
let pendingReviewEdits = this.$pgettext('Sidebar/Moderation/Hidden text', "Pending review edits") const mainMenu = this.$pgettext('Sidebar/*/Hidden text', 'Main menu')
const selectTrack = this.$pgettext('Sidebar/Player/Hidden text', 'Play this track')
const pendingFollows = this.$pgettext('Sidebar/Notifications/Hidden text', 'Pending follow requests')
const pendingReviewEdits = this.$pgettext('Sidebar/Moderation/Hidden text', 'Pending review edits')
const language = this.$pgettext(
'Sidebar/Settings/Dropdown.Label/Short, Verb',
'Language')
const theme = this.$pgettext(
'Sidebar/Settings/Dropdown.Label/Short, Verb',
'Theme')
return { return {
pendingFollows, pendingFollows,
mainMenu, mainMenu,
selectTrack, selectTrack,
pendingReviewEdits, pendingReviewEdits,
addContent: this.$pgettext("*/Library/*/Verb", 'Add content'), language,
notifications: this.$pgettext("*/Notifications/*", 'Notifications'), theme,
administration: this.$pgettext("Sidebar/Admin/Title/Noun", 'Administration'), addContent: this.$pgettext('*/Library/*/Verb', 'Add content'),
administration: this.$pgettext('Sidebar/Admin/Title/Noun', 'Administration')
} }
}, },
logoUrl() { logoUrl () {
if (this.$store.state.auth.authenticated) { if (this.$store.state.auth.authenticated) {
return "library.index" return 'library.index'
} else { } else {
return "index" return 'index'
} }
}, },
focusedMenu () { focusedMenu () {
let mapping = { const mapping = {
"search": 'exploreExpanded', search: 'exploreExpanded',
"library.index": 'exploreExpanded', 'library.index': 'exploreExpanded',
"library.podcasts.browse": 'exploreExpanded', 'library.podcasts.browse': 'exploreExpanded',
"library.albums.browse": 'exploreExpanded', 'library.albums.browse': 'exploreExpanded',
"library.albums.detail": 'exploreExpanded', 'library.albums.detail': 'exploreExpanded',
"library.artists.browse": 'exploreExpanded', 'library.artists.browse': 'exploreExpanded',
"library.artists.detail": 'exploreExpanded', 'library.artists.detail': 'exploreExpanded',
"library.tracks.detail": 'exploreExpanded', 'library.tracks.detail': 'exploreExpanded',
"library.playlists.browse": 'exploreExpanded', 'library.playlists.browse': 'exploreExpanded',
"library.playlists.detail": 'exploreExpanded', 'library.playlists.detail': 'exploreExpanded',
"library.radios.browse": 'exploreExpanded', 'library.radios.browse': 'exploreExpanded',
"library.radios.detail": 'exploreExpanded', 'library.radios.detail': 'exploreExpanded',
'library.me': "myLibraryExpanded", 'library.me': 'myLibraryExpanded',
'library.albums.me': "myLibraryExpanded", 'library.albums.me': 'myLibraryExpanded',
'library.artists.me': "myLibraryExpanded", 'library.artists.me': 'myLibraryExpanded',
'library.playlists.me': "myLibraryExpanded", 'library.playlists.me': 'myLibraryExpanded',
'library.radios.me': "myLibraryExpanded", 'library.radios.me': 'myLibraryExpanded',
'favorites': "myLibraryExpanded", favorites: 'myLibraryExpanded'
} }
let m = mapping[this.$route.name] const m = mapping[this.$route.name]
if (m) { if (m) {
return m return m
} }
@ -263,57 +586,31 @@ export default {
this.$store.state.ui.notifications.pendingReviewReports + this.$store.state.ui.notifications.pendingReviewReports +
this.$store.state.ui.notifications.pendingReviewRequests this.$store.state.ui.notifications.pendingReviewRequests
) )
}
},
methods: {
...mapActions({
cleanTrack: "queue/cleanTrack"
}),
applyContentFilters () {
let artistIds = this.$store.getters['moderation/artistFilters']().map((f) => {
return f.target.id
})
if (artistIds.length === 0) {
return
}
let self = this
let tracks = this.tracks.slice().reverse()
tracks.forEach(async (t, i) => {
// we loop from the end because removing index from the start can lead to removing the wrong tracks
let realIndex = tracks.length - i - 1
let matchArtist = artistIds.indexOf(t.artist.id) > -1
if (matchArtist) {
return await self.cleanTrack(realIndex)
}
if (t.album && artistIds.indexOf(t.album.artist.id) > -1) {
return await self.cleanTrack(realIndex)
}
})
}, },
setupDropdown (selector) { production () {
let self = this return process.env.NODE_ENV === 'production'
$(self.$el).find(selector).dropdown({ },
selectOnKeydown: false, themes () {
action: function (text, value, $el) { return [
// used ton ensure focusing the dropdown and clicking via keyboard {
// works as expected name: this.$pgettext('Sidebar/Settings/Dropdown.Label/Theme name', 'Light'),
let link = $($el).closest('a') key: 'light'
let url = link.attr('href') },
self.$router.push(url) {
$(self.$el).find(selector).dropdown('hide') name: this.$pgettext('Sidebar/Settings/Dropdown.Label/Theme name', 'Dark'),
key: 'dark'
} }
}) ]
} }
}, },
watch: { watch: {
url: function() { url: function () {
this.isCollapsed = true this.isCollapsed = true
}, },
"$store.state.moderation.lastUpdate": function () { '$store.state.moderation.lastUpdate': function () {
this.applyContentFilters() this.applyContentFilters()
}, },
"$store.state.auth.authenticated": { '$store.state.auth.authenticated': {
immediate: true, immediate: true,
handler (v) { handler (v) {
if (v) { if (v) {
@ -321,17 +618,21 @@ export default {
this.setupDropdown('.user-dropdown') this.setupDropdown('.user-dropdown')
this.setupDropdown('.admin-dropdown') this.setupDropdown('.admin-dropdown')
}) })
} else {
this.$nextTick(() => {
this.setupDropdown('.user-dropdown')
})
} }
} }
}, },
"$store.state.auth.availablePermissions": { '$store.state.auth.availablePermissions': {
immediate: true, immediate: true,
handler (v) { handler (v) {
this.$nextTick(() => { this.$nextTick(() => {
this.setupDropdown('.admin-dropdown') this.setupDropdown('.admin-dropdown')
}) })
}, },
deep: true, deep: true
}, },
focusedMenu: { focusedMenu: {
immediate: true, immediate: true,
@ -351,6 +652,93 @@ export default {
this.myLibraryExpanded = false this.myLibraryExpanded = false
} }
}, },
languageSelection: function (v) {
this.$store.dispatch('ui/currentLanguage', v)
this.$refs.languageModal.closeModal()
},
themeSelection: function (v) {
this.$store.dispatch('ui/theme', v)
this.$refs.themeModal.closeModal()
}
},
mounted () {
this.$nextTick(() => {
document.getElementById('fake-sidebar').classList.add('loaded')
})
},
methods: {
...mapActions({
cleanTrack: 'queue/cleanTrack'
}),
applyContentFilters () {
const artistIds = this.$store.getters['moderation/artistFilters']().map((f) => {
return f.target.id
})
if (artistIds.length === 0) {
return
}
const self = this
const tracks = this.tracks.slice().reverse()
tracks.forEach(async (t, i) => {
// we loop from the end because removing index from the start can lead to removing the wrong tracks
const realIndex = tracks.length - i - 1
const matchArtist = artistIds.indexOf(t.artist.id) > -1
if (matchArtist) {
return await self.cleanTrack(realIndex)
}
if (t.album && artistIds.indexOf(t.album.artist.id) > -1) {
return await self.cleanTrack(realIndex)
}
})
},
setupDropdown (selector) {
const self = this
$(self.$el).find(selector).dropdown({
selectOnKeydown: false,
action: function (text, value, $el) {
// used ton ensure focusing the dropdown and clicking via keyboard
// works as expected
const link = $($el).closest('a')
const url = link.attr('href')
self.$router.push(url)
$(self.$el).find(selector).dropdown('hide')
}
})
}
} }
} }
</script> </script>
<style>
[type="radio"] {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
[type="radio"] + label::after {
content: "";
font-size: 1.4em;
}
[type="radio"]:checked + label::after {
margin-left: 10px;
content: "\2713"; /* Checkmark */
font-size: 1.4em;
}
[type="radio"]:checked + label {
font-weight: bold;
}
fieldset {
border: none;
}
.back {
font-size: 1.25em !important;
position: absolute;
top: 0.5rem;
left: 0.5rem;
width: 2.25rem !important;
height: 2.25rem !important;
padding: 0.625rem 0 0 0;
}
</style>

View File

@ -1,15 +1,14 @@
<template> <template>
<modal <modal
@update:show="$emit('update:show', $event)" ref="modal"
:show="show" :show="show"
:scrolling="true" :scrolling="true"
:additionalClasses="['scrolling-track-options']" :additional-classes="['scrolling-track-options']"
@update:show="$emit('update:show', $event)"
> >
<div class="header"> <div class="header">
<div class="ui large centered rounded image"> <div class="ui large centered rounded image">
<img <img
alt=""
class="ui centered image"
v-if=" v-if="
track.album && track.album.cover && track.album.cover.urls.original track.album && track.album.cover && track.album.cover.urls.original
" "
@ -18,43 +17,50 @@
track.album.cover.urls.medium_square_crop track.album.cover.urls.medium_square_crop
) )
" "
/>
<img
alt="" alt=""
class="ui centered image" class="ui centered image"
>
<img
v-else-if="track.cover" v-else-if="track.cover"
v-lazy=" v-lazy="
$store.getters['instance/absoluteUrl']( $store.getters['instance/absoluteUrl'](
track.cover.urls.medium_square_crop track.cover.urls.medium_square_crop
) )
" "
/>
<img
alt="" alt=""
class="ui centered image" class="ui centered image"
>
<img
v-else-if="track.artist.cover" v-else-if="track.artist.cover"
v-lazy=" v-lazy="
$store.getters['instance/absoluteUrl']( $store.getters['instance/absoluteUrl'](
track.artist.cover.urls.medium_square_crop track.artist.cover.urls.medium_square_crop
) )
" "
/>
<img
alt="" alt=""
class="ui centered image" class="ui centered image"
>
<img
v-else v-else
alt=""
class="ui centered image"
src="../../../assets/audio/default-cover.png" src="../../../assets/audio/default-cover.png"
/> >
</div> </div>
<h3 class="track-modal-title">{{ track.title }}</h3> <h3 class="track-modal-title">
<h4 class="track-modal-subtitle">{{ track.artist.name }}</h4> {{ track.title }}
</h3>
<h4 class="track-modal-subtitle">
{{ track.artist.name }}
</h4>
</div> </div>
<div class="ui hidden divider"></div> <div class="ui hidden divider" />
<div class="content"> <div class="content">
<div class="ui one column unstackable grid"> <div class="ui one column unstackable grid">
<div <div
v-if="$store.state.auth.authenticated && track.artist.content_category !== 'podcast'"
class="row" class="row"
v-if="$store.state.auth.authenticated && this.track.artist.content_category !== 'podcast'"> >
<div <div
tabindex="0" tabindex="0"
class="column" class="column"
@ -80,11 +86,11 @@
<div <div
class="column" class="column"
role="button" role="button"
:aria-label="labels.addToQueue"
@click.stop.prevent=" @click.stop.prevent="
add(); add();
closeModal(); $refs.modal.closeModal();
" "
:aria-label="labels.addToQueue"
> >
<i class="plus icon track-modal list-icon" /> <i class="plus icon track-modal list-icon" />
<span class="track-modal list-item">{{ labels.addToQueue }}</span> <span class="track-modal list-item">{{ labels.addToQueue }}</span>
@ -94,11 +100,11 @@
<div <div
class="column" class="column"
role="button" role="button"
:aria-label="labels.playNext"
@click.stop.prevent=" @click.stop.prevent="
addNext(true); addNext(true);
closeModal(); $refs.modal.closeModal();
" "
:aria-label="labels.playNext"
> >
<i class="step forward icon track-modal list-icon" /> <i class="step forward icon track-modal list-icon" />
<span class="track-modal list-item">{{ labels.playNext }}</span> <span class="track-modal list-item">{{ labels.playNext }}</span>
@ -108,14 +114,14 @@
<div <div
class="column" class="column"
role="button" role="button"
:aria-label="labels.startRadio"
@click.stop.prevent=" @click.stop.prevent="
$store.dispatch('radios/start', { $store.dispatch('radios/start', {
type: 'similar', type: 'similar',
objectId: track.id, objectId: track.id,
}); });
closeModal(); $refs.modal.closeModal();
" "
:aria-label="labels.startRadio"
> >
<i class="rss icon track-modal list-icon" /> <i class="rss icon track-modal list-icon" />
<span class="track-modal list-item">{{ labels.startRadio }}</span> <span class="track-modal list-item">{{ labels.startRadio }}</span>
@ -125,8 +131,8 @@
<div <div
class="column" class="column"
role="button" role="button"
@click.stop="$store.commit('playlists/chooseTrack', track)"
:aria-label="labels.addToPlaylist" :aria-label="labels.addToPlaylist"
@click.stop="$store.commit('playlists/chooseTrack', track)"
> >
<i class="list icon track-modal list-icon" /> <i class="list icon track-modal list-icon" />
<span class="track-modal list-item">{{ <span class="track-modal list-item">{{
@ -134,8 +140,11 @@
}}</span> }}</span>
</div> </div>
</div> </div>
<div class="ui divider"></div> <div class="ui divider" />
<div v-if="!isAlbum && track.album" class="row"> <div
v-if="!isAlbum && track.album"
class="row"
>
<div <div
class="column" class="column"
role="button" role="button"
@ -153,7 +162,10 @@
}}</span> }}</span>
</div> </div>
</div> </div>
<div v-if="!isArtist" class="row"> <div
v-if="!isArtist"
class="row"
>
<div <div
class="column" class="column"
role="button" role="button"
@ -189,7 +201,7 @@
}}</span> }}</span>
</div> </div>
</div> </div>
<div class="ui divider"></div> <div class="ui divider" />
<div <div
v-for="obj in getReportableObjs({ v-for="obj in getReportableObjs({
track, track,
@ -197,16 +209,15 @@
artist, artist,
})" })"
:key="obj.target.type + obj.target.id" :key="obj.target.type + obj.target.id"
class="row"
:ref="`report${obj.target.type}${obj.target.id}`" :ref="`report${obj.target.type}${obj.target.id}`"
class="row"
:data-ref="`report${obj.target.type}${obj.target.id}`" :data-ref="`report${obj.target.type}${obj.target.id}`"
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)" @click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
> >
<div class="column"> <div class="column">
<i class="share icon track-modal list-icon" /><span <i class="share icon track-modal list-icon" /><span
class="track-modal list-item" class="track-modal list-item"
>{{ obj.label }}</span >{{ obj.label }}</span>
>
</div> </div>
</div> </div>
</div> </div>
@ -215,90 +226,83 @@
</template> </template>
<script> <script>
import Modal from "@/components/semantic/Modal"; import Modal from '@/components/semantic/Modal'
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
import ReportMixin from '@/components/mixins/Report' import ReportMixin from '@/components/mixins/Report'
import PlayOptionsMixin from '@/components/mixins/PlayOptions' import PlayOptionsMixin from '@/components/mixins/PlayOptions'
export default { export default {
components: {
Modal
},
mixins: [ReportMixin, PlayOptionsMixin], mixins: [ReportMixin, PlayOptionsMixin],
props: { props: {
show: { type: Boolean, required: true, default: false }, show: { type: Boolean, required: true, default: false },
track: { type: Object, required: true }, track: { type: Object, required: true },
index: { type: Number, required: true }, index: { type: Number, required: true },
isArtist: { type: Boolean, required: false, default: false }, isArtist: { type: Boolean, required: false, default: false },
isAlbum: { type: Boolean, required: false, default: false }, isAlbum: { type: Boolean, required: false, default: false }
}, },
components: { data () {
Modal,
TrackFavoriteIcon,
},
data() {
return { return {
isShowing: this.show, isShowing: this.show,
tracks: [this.track], tracks: [this.track],
album: this.track.album, album: this.track.album,
artist: this.track.artist, artist: this.track.artist
}; }
}, },
computed: { computed: {
isFavorite() { isFavorite () {
return this.$store.getters["favorites/isFavorite"](this.track.id); return this.$store.getters['favorites/isFavorite'](this.track.id)
}, },
favoriteButton() { favoriteButton () {
if (this.isFavorite) { if (this.isFavorite) {
return this.$pgettext( return this.$pgettext(
"Content/Track/Icon.Tooltip/Verb", 'Content/Track/Icon.Tooltip/Verb',
"Remove from favorites" 'Remove from favorites'
); )
} else { } else {
return this.$pgettext("Content/Track/*/Verb", "Add to favorites"); return this.$pgettext('Content/Track/*/Verb', 'Add to favorites')
} }
}, },
trackDetailsButton() { trackDetailsButton () {
if (this.track.artist.content_category === 'podcast') { if (this.track.artist.content_category === 'podcast') {
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Episode details") return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'Episode details')
} else { } else {
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Track details") return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'Track details')
} }
}, },
albumDetailsButton() { albumDetailsButton () {
if (this.track.artist.content_category === 'podcast') { if (this.track.artist.content_category === 'podcast') {
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View series") return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View series')
} else { } else {
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View album") return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View album')
} }
}, },
artistDetailsButton() { artistDetailsButton () {
if (this.track.artist.content_category === 'podcast') { if (this.track.artist.content_category === 'podcast') {
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View channel") return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View channel')
} else { } else {
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View artist") return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View artist')
} }
}, },
labels() { labels () {
return { return {
startRadio: this.$pgettext( startRadio: this.$pgettext(
"*/Queue/Dropdown/Button/Title", '*/Queue/Dropdown/Button/Title',
"Play radio" 'Play radio'
), ),
playNow: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play now"), playNow: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play now'),
addToQueue: this.$pgettext( addToQueue: this.$pgettext(
"*/Queue/Dropdown/Button/Title", '*/Queue/Dropdown/Button/Title',
"Add to queue" 'Add to queue'
), ),
playNext: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play next"), playNext: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play next'),
addToPlaylist: this.$pgettext( addToPlaylist: this.$pgettext(
"Sidebar/Player/Icon.Tooltip/Verb", 'Sidebar/Player/Icon.Tooltip/Verb',
"Add to playlist…" 'Add to playlist…'
), )
}; }
}, }
}, }
methods: { }
closeModal() {
this.$emit("update:show", false);
},
},
};
</script> </script>

View File

@ -1,15 +1,14 @@
<template> <template>
<modal <modal
@update:show="$emit('update:show', $event)" ref="modal"
:show="show" :show="show"
:scrolling="true" :scrolling="true"
:additionalClasses="['scrolling-track-options']" :additional-classes="['scrolling-track-options']"
@update:show="$emit('update:show', $event)"
> >
<div class="header"> <div class="header">
<div class="ui large centered rounded image"> <div class="ui large centered rounded image">
<img <img
alt=""
class="ui centered image"
v-if=" v-if="
track.album && track.album.cover && track.album.cover.urls.original track.album && track.album.cover && track.album.cover.urls.original
" "
@ -18,43 +17,50 @@
track.album.cover.urls.medium_square_crop track.album.cover.urls.medium_square_crop
) )
" "
/>
<img
alt="" alt=""
class="ui centered image" class="ui centered image"
>
<img
v-else-if="track.cover" v-else-if="track.cover"
v-lazy=" v-lazy="
$store.getters['instance/absoluteUrl']( $store.getters['instance/absoluteUrl'](
track.cover.urls.medium_square_crop track.cover.urls.medium_square_crop
) )
" "
/>
<img
alt="" alt=""
class="ui centered image" class="ui centered image"
>
<img
v-else-if="track.artist.cover" v-else-if="track.artist.cover"
v-lazy=" v-lazy="
$store.getters['instance/absoluteUrl']( $store.getters['instance/absoluteUrl'](
track.artist.cover.urls.medium_square_crop track.artist.cover.urls.medium_square_crop
) )
" "
/>
<img
alt="" alt=""
class="ui centered image" class="ui centered image"
>
<img
v-else v-else
alt=""
class="ui centered image"
src="../../../assets/audio/default-cover.png" src="../../../assets/audio/default-cover.png"
/> >
</div> </div>
<h3 class="track-modal-title">{{ track.title }}</h3> <h3 class="track-modal-title">
<h4 class="track-modal-subtitle">{{ track.artist.name }}</h4> {{ track.title }}
</h3>
<h4 class="track-modal-subtitle">
{{ track.artist.name }}
</h4>
</div> </div>
<div class="ui hidden divider"></div> <div class="ui hidden divider" />
<div class="content"> <div class="content">
<div class="ui one column unstackable grid"> <div class="ui one column unstackable grid">
<div <div
v-if="$store.state.auth.authenticated && track.artist.content_category !== 'podcast'"
class="row" class="row"
v-if="$store.state.auth.authenticated && this.track.artist.content_category !== 'podcast'"> >
<div <div
tabindex="0" tabindex="0"
class="column" class="column"
@ -80,11 +86,11 @@
<div <div
class="column" class="column"
role="button" role="button"
:aria-label="labels.addToQueue"
@click.stop.prevent=" @click.stop.prevent="
add(); add();
closeModal(); $refs.modal.closeModal();
" "
:aria-label="labels.addToQueue"
> >
<i class="plus icon track-modal list-icon" /> <i class="plus icon track-modal list-icon" />
<span class="track-modal list-item">{{ labels.addToQueue }}</span> <span class="track-modal list-item">{{ labels.addToQueue }}</span>
@ -94,11 +100,11 @@
<div <div
class="column" class="column"
role="button" role="button"
:aria-label="labels.playNext"
@click.stop.prevent=" @click.stop.prevent="
addNext(true); addNext(true);
closeModal(); $refs.modal.closeModal();
" "
:aria-label="labels.playNext"
> >
<i class="step forward icon track-modal list-icon" /> <i class="step forward icon track-modal list-icon" />
<span class="track-modal list-item">{{ labels.playNext }}</span> <span class="track-modal list-item">{{ labels.playNext }}</span>
@ -108,14 +114,14 @@
<div <div
class="column" class="column"
role="button" role="button"
:aria-label="labels.startRadio"
@click.stop.prevent=" @click.stop.prevent="
$store.dispatch('radios/start', { $store.dispatch('radios/start', {
type: 'similar', type: 'similar',
objectId: track.id, objectId: track.id,
}); });
closeModal(); $refs.modal.closeModal();
" "
:aria-label="labels.startRadio"
> >
<i class="rss icon track-modal list-icon" /> <i class="rss icon track-modal list-icon" />
<span class="track-modal list-item">{{ labels.startRadio }}</span> <span class="track-modal list-item">{{ labels.startRadio }}</span>
@ -125,8 +131,8 @@
<div <div
class="column" class="column"
role="button" role="button"
@click.stop="$store.commit('playlists/chooseTrack', track)"
:aria-label="labels.addToPlaylist" :aria-label="labels.addToPlaylist"
@click.stop="$store.commit('playlists/chooseTrack', track)"
> >
<i class="list icon track-modal list-icon" /> <i class="list icon track-modal list-icon" />
<span class="track-modal list-item">{{ <span class="track-modal list-item">{{
@ -134,8 +140,11 @@
}}</span> }}</span>
</div> </div>
</div> </div>
<div class="ui divider"></div> <div class="ui divider" />
<div v-if="!isAlbum && track.album" class="row"> <div
v-if="!isAlbum && track.album"
class="row"
>
<div <div
class="column" class="column"
role="button" role="button"
@ -153,7 +162,10 @@
}}</span> }}</span>
</div> </div>
</div> </div>
<div v-if="!isArtist" class="row"> <div
v-if="!isArtist"
class="row"
>
<div <div
class="column" class="column"
role="button" role="button"
@ -189,7 +201,7 @@
}}</span> }}</span>
</div> </div>
</div> </div>
<div class="ui divider"></div> <div class="ui divider" />
<div <div
v-for="obj in getReportableObjs({ v-for="obj in getReportableObjs({
track, track,
@ -197,16 +209,15 @@
artist, artist,
})" })"
:key="obj.target.type + obj.target.id" :key="obj.target.type + obj.target.id"
class="row"
:ref="`report${obj.target.type}${obj.target.id}`" :ref="`report${obj.target.type}${obj.target.id}`"
class="row"
:data-ref="`report${obj.target.type}${obj.target.id}`" :data-ref="`report${obj.target.type}${obj.target.id}`"
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)" @click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
> >
<div class="column"> <div class="column">
<i class="share icon track-modal list-icon" /><span <i class="share icon track-modal list-icon" /><span
class="track-modal list-item" class="track-modal list-item"
>{{ obj.label }}</span >{{ obj.label }}</span>
>
</div> </div>
</div> </div>
</div> </div>
@ -215,90 +226,83 @@
</template> </template>
<script> <script>
import Modal from "@/components/semantic/Modal"; import Modal from '@/components/semantic/Modal'
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
import ReportMixin from '@/components/mixins/Report' import ReportMixin from '@/components/mixins/Report'
import PlayOptionsMixin from '@/components/mixins/PlayOptions' import PlayOptionsMixin from '@/components/mixins/PlayOptions'
export default { export default {
components: {
Modal
},
mixins: [ReportMixin, PlayOptionsMixin], mixins: [ReportMixin, PlayOptionsMixin],
props: { props: {
show: { type: Boolean, required: true, default: false }, show: { type: Boolean, required: true, default: false },
track: { type: Object, required: true }, track: { type: Object, required: true },
index: { type: Number, required: true }, index: { type: Number, required: true },
isArtist: { type: Boolean, required: false, default: false }, isArtist: { type: Boolean, required: false, default: false },
isAlbum: { type: Boolean, required: false, default: false }, isAlbum: { type: Boolean, required: false, default: false }
}, },
components: { data () {
Modal,
TrackFavoriteIcon,
},
data() {
return { return {
isShowing: this.show, isShowing: this.show,
tracks: [this.track], tracks: [this.track],
album: this.track.album, album: this.track.album,
artist: this.track.artist, artist: this.track.artist
}; }
}, },
computed: { computed: {
isFavorite() { isFavorite () {
return this.$store.getters["favorites/isFavorite"](this.track.id); return this.$store.getters['favorites/isFavorite'](this.track.id)
}, },
favoriteButton() { favoriteButton () {
if (this.isFavorite) { if (this.isFavorite) {
return this.$pgettext( return this.$pgettext(
"Content/Track/Icon.Tooltip/Verb", 'Content/Track/Icon.Tooltip/Verb',
"Remove from favorites" 'Remove from favorites'
); )
} else { } else {
return this.$pgettext("Content/Track/*/Verb", "Add to favorites"); return this.$pgettext('Content/Track/*/Verb', 'Add to favorites')
} }
}, },
trackDetailsButton() { trackDetailsButton () {
if (this.track.artist.content_category === 'podcast') { if (this.track.artist.content_category === 'podcast') {
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Episode details") return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'Episode details')
} else { } else {
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Track details") return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'Track details')
} }
}, },
albumDetailsButton() { albumDetailsButton () {
if (this.track.artist.content_category === 'podcast') { if (this.track.artist.content_category === 'podcast') {
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View series") return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View series')
} else { } else {
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View album") return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View album')
} }
}, },
artistDetailsButton() { artistDetailsButton () {
if (this.track.artist.content_category === 'podcast') { if (this.track.artist.content_category === 'podcast') {
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View channel") return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View channel')
} else { } else {
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View artist") return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View artist')
} }
}, },
labels() { labels () {
return { return {
startRadio: this.$pgettext( startRadio: this.$pgettext(
"*/Queue/Dropdown/Button/Title", '*/Queue/Dropdown/Button/Title',
"Play radio" 'Play radio'
), ),
playNow: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play now"), playNow: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play now'),
addToQueue: this.$pgettext( addToQueue: this.$pgettext(
"*/Queue/Dropdown/Button/Title", '*/Queue/Dropdown/Button/Title',
"Add to queue" 'Add to queue'
), ),
playNext: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play next"), playNext: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play next'),
addToPlaylist: this.$pgettext( addToPlaylist: this.$pgettext(
"Sidebar/Player/Icon.Tooltip/Verb", 'Sidebar/Player/Icon.Tooltip/Verb',
"Add to playlist…" 'Add to playlist…'
), )
}; }
}, }
}, }
methods: { }
closeModal() {
this.$emit("update:show", false);
},
},
};
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,204 @@
<template>
<div class="ui menu">
<div class="ui scrolling dropdown item">
<i class="language icon" />
{{ labels.language }}
<i class="dropdown icon" />
<div
id="language-select"
class="menu"
>
<a
v-for="(language, key) in $language.available"
:key="key"
:class="[{'active': $language.current === key},'item']"
:value="key"
@click="$store.dispatch('ui/currentLanguage', key)"
>{{ language }}</a>
</div>
</div>
<div class="ui dropdown item">
<i class="palette icon" />
{{ labels.theme }}
<i class="dropdown icon" />
<div
id="theme-select"
class="menu"
>
<a
v-for="theme in themes"
:key="theme.key"
:class="[{'active': $store.state.ui.theme === theme.key}, 'item']"
:value="theme.key"
@click="$store.dispatch('ui/theme', theme.key)"
>
<i :class="theme.icon" />
{{ theme.name }}
</a>
</div>
</div>
<template v-if="$store.state.auth.authenticated">
<div class="divider" />
<router-link
class="item"
:to="{name: 'profile.overview', params: { username: $store.state.auth.username },}"
>
<i class="user icon" />
{{ labels.profile }}
</router-link>
<router-link
v-if="$store.state.auth.authenticated"
class="item"
:to="{name: 'notifications'}"
>
<i class="bell icon" />
{{ labels.notifications }}
</router-link>
<router-link
class="item"
:to="{ path: '/settings' }"
>
<i class="cog icon" />
{{ labels.settings }}
</router-link>
</template>
<div class="divider" />
<div class="ui dropdown item">
<i class="life ring outline icon" />
{{ labels.support }}
<i class="dropdown icon" />
<div class="menu">
<a
href="https://forum.funkwhale.audio"
class="item"
target="_blank"
>
<i class="users icon" />
{{ labels.forum }}
</a>
<a
href="https://matrix.to/#/#funkwhale-troubleshooting:matrix.org"
class="item"
target="_blank"
>
<i class="comment icon" />
{{ labels.chat }}
</a>
<a
href="https://dev.funkwhale.audio/funkwhale/funkwhale/issues"
class="item"
target="_blank"
>
<i class="gitlab icon" />
{{ labels.git }}
</a>
</div>
</div>
<a
href="https://docs.funkwhale.audio"
class="item"
target="_blank"
>
<i class="book open icon" />
{{ labels.docs }}
</a>
<a
href=""
class="item"
@click.prevent="showShortcuts"
>
<i class="keyboard icon" />
{{ labels.shortcuts }}
</a>
<router-link
v-if="$route.path != '/about'"
class="item"
:to="{ name: 'about' }"
>
<i class="question circle outline icon" />
{{ labels.about }}
</router-link>
<template v-if="$store.state.auth.authenticated && $route.path != '/logout'">
<div class="divider" />
<router-link
class="item"
style="color: var(--danger-color)!important;"
:to="{ name: 'logout' }"
>
<i class="sign out alternate icon" />
{{ labels.logout }}
</router-link>
</template>
<template v-if="!$store.state.auth.authenticated">
<div class="divider" />
<router-link
class="item"
:to="{ name: 'login' }"
>
<i class="sign in alternate icon" />
{{ labels.login }}
</router-link>
</template>
<template v-if="!$store.state.auth.authenticated && $store.state.instance.settings.users.registration_enabled.value">
<router-link
class="item"
:to="{ name: 'signup' }"
>
<i class="user icon" />
{{ labels.signup }}
</router-link>
</template>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
labels () {
return {
profile: this.$pgettext('*/*/*/Noun', 'Profile'),
settings: this.$pgettext('*/*/*/Noun', 'Settings'),
logout: this.$pgettext('Sidebar/Login/List item.Link/Verb', 'Log out'),
about: this.$pgettext('Sidebar/About/List item.Link', 'About'),
shortcuts: this.$pgettext('*/*/*/Noun', 'Keyboard shortcuts'),
support: this.$pgettext('Sidebar/*/Listitem.Link', 'Help'),
forum: this.$pgettext('Sidebar/*/Listitem.Link', 'Forum'),
docs: this.$pgettext('Sidebar/*/Listitem.Link', 'Documentation'),
language: this.$pgettext('Footer/Settings/Dropdown.Label/Short, Verb', 'Change language'),
theme: this.$pgettext('Footer/Settings/Dropdown.Label/Short, Verb', 'Change theme'),
chat: this.$pgettext('Sidebar/*/Listitem.Link', 'Chat room'),
git: this.$pgettext('Footer/*/List item.Link', 'Issue tracker'),
login: this.$pgettext('*/*/Button.Label/Verb', 'Log in'),
signup: this.$pgettext('*/*/Button.Label/Verb', 'Sign up'),
notifications: this.$pgettext('*/Notifications/*', 'Notifications')
}
},
themes () {
return [
{
icon: 'sun icon',
name: this.$pgettext('Footer/Settings/Dropdown.Label/Theme name', 'Light'),
key: 'light'
},
{
icon: 'moon icon',
name: this.$pgettext('Footer/Settings/Dropdown.Label/Theme name', 'Dark'),
key: 'dark'
}
]
},
...mapGetters({
additionalNotifications: 'ui/additionalNotifications'
})
},
methods: {
showShortcuts () {
this.$emit('show:shortcuts-modal')
console.log(this.$store.getters['ui/windowSize'])
}
}
}
</script>

View File

@ -0,0 +1,239 @@
<template>
<!-- TODO make generic and move to semantic/modal? -->
<modal
:show="show"
:scrolling="true"
:fullscreen="false"
@update:show="$emit('update:show', $event)"
>
<div
v-if="$store.state.auth.authenticated"
class="header"
>
<img
v-if="$store.state.auth.profile.avatar && $store.state.auth.profile.avatar.urls.medium_square_crop"
v-lazy="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.urls.medium_square_crop)"
alt=""
class="ui centered small circular image"
>
<actor-avatar
v-else
:actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username,}"
/>
<h3 class="user-modal title">
{{ labels.header }}
</h3>
</div>
<div
v-else
class="header"
>
<h3 class="ui center aligned icon header">
{{ labels.header }}
</h3>
</div>
<div class="content">
<div class="ui one column unstackable grid">
<div class="row">
<div
class="column"
role="button"
@click="[$emit('update:show', false), $emit('showLanguageModalEvent')]"
>
<i class="language icon user-modal list-icon" />
<span class="user-modal list-item">{{ labels.language }}:</span>
<div class="right floated">
<span class="user-modal list-item">{{ $language.available[$language.current] }}</span>
<i class="action-hint chevron right icon" />
</div>
</div>
</div>
<div class="row">
<div
class="column"
role="button"
@click="[$emit('update:show', false), $emit('showThemeModalEvent')]"
>
<i class="palette icon user-modal list-icon" />
<span class="user-modal list-item">{{ labels.theme }}:</span>
<div class="right floated">
<span class="user-modal list-item"> {{ themes.find(x => x.key ===$store.state.ui.theme).name }}</span>
<i class="action-hint chevron right icon user-modal" />
</div>
</div>
</div>
<div class="ui divider" />
<template v-if="$store.state.auth.authenticated">
<div class="row">
<div
class="column"
role="button"
@click.prevent.exact="$router.push({name: 'profile.overview', params: { username: $store.state.auth.username }})"
>
<i class="user icon user-modal list-icon" />
<span class="user-modal list-item">{{ labels.profile }}</span>
</div>
</div>
<div class="row">
<router-link
v-if="$store.state.auth.authenticated"
tag="div"
class="column"
:to="{name: 'notifications'}"
role="button"
>
<i class="user-modal list-icon bell icon" />
<span class="user-modal list-item">{{ labels.notifications }}</span>
</router-link>
</div>
<div class="row">
<router-link
tag="div"
class="column"
:to="{ path: '/settings' }"
role="button"
>
<i class="user-modal list-icon cog icon" />
<span class="user-modal list-item">{{ labels.settings }}</span>
</router-link>
</div>
<div class="ui divider" />
</template>
<div class="row">
<a
class="column"
href="https://docs.funkwhale.audio"
target="_blank"
>
<i class="user-modal list-icon book open icon" />
<span class="user-modal list-item">{{ labels.docs }}</span>
</a>
</div>
<div class="row">
<router-link
tag="div"
class="column"
:to="{ name: 'about' }"
role="button"
>
<i class="user-modal list-icon question circle outline icon" />
<span class="user-modal list-item">{{ labels.about }}</span>
</router-link>
</div>
<div class="ui divider" />
<template v-if="$store.state.auth.authenticated">
<router-link
tag="div"
class="column"
:to="{ name: 'logout' }"
role="button"
>
<i class="user-modal list-icon sign out alternate icon" />
<span class="user-modal list-item">{{ labels.logout }}</span>
</router-link>
</template>
<template v-if="!$store.state.auth.authenticated">
<router-link
tag="div"
class="column"
:to="{ name: 'login' }"
role="button"
>
<i class="user-modal list-icon sign in alternate icon" />
<span class="user-modal list-item">{{ labels.login }}</span>
</router-link>
</template>
<template
v-if="!$store.state.auth.authenticated"
&&
$store.state.instance.settings.users.registration_enabled.value
>
<router-link
tag="div"
class="column"
:to="{ name: 'signup' }"
role="button"
>
<i class="user-modal list-item user icon" />
<span class="user-modal list-item">{{ labels.signup }}</span>
</router-link>
</template>
</div>
</div>
</modal>
</template>
<script>
import Modal from '@/components/semantic/Modal'
import { mapGetters } from 'vuex'
export default {
components: {
Modal
},
props: {
show: { type: Boolean, required: true }
},
computed: {
labels () {
return {
header: this.$pgettext('Popup/Title/Noun', 'Options'),
profile: this.$pgettext('*/*/*/Noun', 'Profile'),
settings: this.$pgettext('*/*/*/Noun', 'Settings'),
logout: this.$pgettext('Sidebar/Login/List item.Link/Verb', 'Log out'),
about: this.$pgettext('Sidebar/About/List item.Link', 'About'),
shortcuts: this.$pgettext('*/*/*/Noun', 'Keyboard shortcuts'),
support: this.$pgettext('Sidebar/*/Listitem.Link', 'Help'),
forum: this.$pgettext('Sidebar/*/Listitem.Link', 'Forum'),
docs: this.$pgettext('Sidebar/*/Listitem.Link', 'Documentation'),
language: this.$pgettext(
'Sidebar/Settings/Dropdown.Label/Short, Verb',
'Language'
),
theme: this.$pgettext(
'Sidebar/Settings/Dropdown.Label/Short, Verb',
'Theme'
),
chat: this.$pgettext('Sidebar/*/Listitem.Link', 'Chat room'),
git: this.$pgettext('Sidebar/*/List item.Link', 'Issue tracker'),
login: this.$pgettext('*/*/Button.Label/Verb', 'Log in'),
signup: this.$pgettext('*/*/Button.Label/Verb', 'Sign up'),
notifications: this.$pgettext('*/Notifications/*', 'Notifications'),
useOtherInstance: this.$pgettext(
'Sidebar/*/List item.Link',
'Use another instance'
)
}
},
themes () {
return [
{
icon: 'sun icon',
name: this.$pgettext(
'Footer/Settings/Dropdown.Label/Theme name',
'Light'
),
key: 'light'
},
{
icon: 'moon icon',
name: this.$pgettext(
'Footer/Settings/Dropdown.Label/Theme name',
'Dark'
),
key: 'dark'
}
]
},
...mapGetters({
additionalNotifications: 'ui/additionalNotifications'
})
}
}
</script>
<style>
.action-hint {
margin-left: 1rem !important;
}
</style>

View File

@ -1,9 +1,10 @@
<template> <template>
<div :class="additionalClasses.concat(['ui', {'active': show}, {'scrolling': scrolling} ,{'overlay fullscreen': fullscreen && ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal'])"> <div :class="additionalClasses.concat(['ui', {'active': show}, {'scrolling': scrolling} ,{'overlay fullscreen': fullscreen && ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal'])">
<i tabindex=0 class="close inside icon"></i> <i
<slot v-if="show"> tabindex="0"
class="close inside icon"
</slot> />
<slot v-if="show" />
</div> </div>
</template> </template>
@ -13,15 +14,41 @@ import createFocusTrap from 'focus-trap'
export default { export default {
props: { props: {
show: {type: Boolean, required: true}, show: { type: Boolean, required: true },
fullscreen: {type: Boolean, default: true}, fullscreen: { type: Boolean, default: true },
scrolling: {type: Boolean, required: false, default: false}, scrolling: { type: Boolean, required: false, default: false },
additionalClasses: {type: Array, required: false, default: () => []} additionalClasses: { type: Array, required: false, default: () => [] }
}, },
data () { data () {
return { return {
control: null, control: null,
focusTrap: null, focusTrap: null
}
},
watch: {
show: {
handler (newValue) {
if (newValue) {
this.initModal()
this.$emit('show')
this.control.modal('show')
this.focusTrap.activate()
this.focusTrap.unpause()
document.body.classList.add('scrolling')
} else {
if (this.control) {
this.$emit('hide')
this.control.modal('hide')
this.control.remove()
this.focusTrap.deactivate()
this.focusTrap.pause()
document.body.classList.remove('scrolling')
}
}
}
},
$route (to, from) {
this.closeModal()
} }
}, },
mounted () { mounted () {
@ -52,29 +79,9 @@ export default {
this.focusTrap.unpause() this.focusTrap.unpause()
}.bind(this) }.bind(this)
}) })
} },
}, closeModal () {
watch: { this.$emit('update:show', false)
show: {
handler (newValue) {
if (newValue) {
this.initModal()
this.$emit('show')
this.control.modal('show')
this.focusTrap.activate()
this.focusTrap.unpause()
document.body.classList.add('scrolling')
} else {
if (this.control) {
this.$emit('hide')
this.control.modal('hide')
this.control.remove()
this.focusTrap.deactivate()
this.focusTrap.pause()
document.body.classList.remove('scrolling')
}
}
}
} }
} }

View File

@ -224,7 +224,7 @@ export default new Router({
) )
}, },
{ {
path: 'activity', path: '/activity',
name: `profile${route.suffix}.activity`, name: `profile${route.suffix}.activity`,
component: () => component: () =>
import( import(
@ -318,7 +318,7 @@ export default new Router({
import(/* webpackChunkName: "admin" */ '@/views/admin/library/Base'), import(/* webpackChunkName: "admin" */ '@/views/admin/library/Base'),
children: [ children: [
{ {
path: 'edits', path: '/edits',
name: 'manage.library.edits', name: 'manage.library.edits',
component: () => component: () =>
import( import(
@ -331,7 +331,7 @@ export default new Router({
} }
}, },
{ {
path: 'artists', path: '/artists',
name: 'manage.library.artists', name: 'manage.library.artists',
component: () => component: () =>
import( import(
@ -344,7 +344,7 @@ export default new Router({
} }
}, },
{ {
path: 'artists/:id', path: '/artists/:id',
name: 'manage.library.artists.detail', name: 'manage.library.artists.detail',
component: () => component: () =>
import( import(
@ -353,7 +353,7 @@ export default new Router({
props: true props: true
}, },
{ {
path: 'channels', path: '/channels',
name: 'manage.channels', name: 'manage.channels',
component: () => component: () =>
import( import(
@ -366,7 +366,7 @@ export default new Router({
} }
}, },
{ {
path: 'channels/:id', path: '/channels/:id',
name: 'manage.channels.detail', name: 'manage.channels.detail',
component: () => component: () =>
import( import(
@ -375,7 +375,7 @@ export default new Router({
props: true props: true
}, },
{ {
path: 'albums', path: '/albums',
name: 'manage.library.albums', name: 'manage.library.albums',
component: () => component: () =>
import( import(
@ -388,7 +388,7 @@ export default new Router({
} }
}, },
{ {
path: 'albums/:id', path: '/albums/:id',
name: 'manage.library.albums.detail', name: 'manage.library.albums.detail',
component: () => component: () =>
import( import(
@ -397,7 +397,7 @@ export default new Router({
props: true props: true
}, },
{ {
path: 'tracks', path: '/tracks',
name: 'manage.library.tracks', name: 'manage.library.tracks',
component: () => component: () =>
import( import(
@ -410,7 +410,7 @@ export default new Router({
} }
}, },
{ {
path: 'tracks/:id', path: '/tracks/:id',
name: 'manage.library.tracks.detail', name: 'manage.library.tracks.detail',
component: () => component: () =>
import( import(
@ -419,7 +419,7 @@ export default new Router({
props: true props: true
}, },
{ {
path: 'libraries', path: '/libraries',
name: 'manage.library.libraries', name: 'manage.library.libraries',
component: () => component: () =>
import( import(
@ -432,7 +432,7 @@ export default new Router({
} }
}, },
{ {
path: 'libraries/:id', path: '/libraries/:id',
name: 'manage.library.libraries.detail', name: 'manage.library.libraries.detail',
component: () => component: () =>
import( import(
@ -441,7 +441,7 @@ export default new Router({
props: true props: true
}, },
{ {
path: 'uploads', path: '/uploads',
name: 'manage.library.uploads', name: 'manage.library.uploads',
component: () => component: () =>
import( import(
@ -454,7 +454,7 @@ export default new Router({
} }
}, },
{ {
path: 'uploads/:id', path: '/uploads/:id',
name: 'manage.library.uploads.detail', name: 'manage.library.uploads.detail',
component: () => component: () =>
import( import(
@ -463,7 +463,7 @@ export default new Router({
props: true props: true
}, },
{ {
path: 'tags', path: '/tags',
name: 'manage.library.tags', name: 'manage.library.tags',
component: () => component: () =>
import( import(
@ -476,7 +476,7 @@ export default new Router({
} }
}, },
{ {
path: 'tags/:id', path: '/tags/:id',
name: 'manage.library.tags.detail', name: 'manage.library.tags.detail',
component: () => component: () =>
import( import(
@ -493,7 +493,7 @@ export default new Router({
import(/* webpackChunkName: "admin" */ '@/views/admin/users/Base'), import(/* webpackChunkName: "admin" */ '@/views/admin/users/Base'),
children: [ children: [
{ {
path: 'users', path: '/users',
name: 'manage.users.users.list', name: 'manage.users.users.list',
component: () => component: () =>
import( import(
@ -501,7 +501,7 @@ export default new Router({
) )
}, },
{ {
path: 'invitations', path: '/invitations',
name: 'manage.users.invitations.list', name: 'manage.users.invitations.list',
component: () => component: () =>
import( import(
@ -517,7 +517,7 @@ export default new Router({
import(/* webpackChunkName: "admin" */ '@/views/admin/moderation/Base'), import(/* webpackChunkName: "admin" */ '@/views/admin/moderation/Base'),
children: [ children: [
{ {
path: 'domains', path: '/domains',
name: 'manage.moderation.domains.list', name: 'manage.moderation.domains.list',
component: () => component: () =>
import( import(
@ -525,7 +525,7 @@ export default new Router({
) )
}, },
{ {
path: 'domains/:id', path: '/domains/:id',
name: 'manage.moderation.domains.detail', name: 'manage.moderation.domains.detail',
component: () => component: () =>
import( import(
@ -534,7 +534,7 @@ export default new Router({
props: true props: true
}, },
{ {
path: 'accounts', path: '/accounts',
name: 'manage.moderation.accounts.list', name: 'manage.moderation.accounts.list',
component: () => component: () =>
import( import(
@ -547,7 +547,7 @@ export default new Router({
} }
}, },
{ {
path: 'accounts/:id', path: '/accounts/:id',
name: 'manage.moderation.accounts.detail', name: 'manage.moderation.accounts.detail',
component: () => component: () =>
import( import(
@ -556,7 +556,7 @@ export default new Router({
props: true props: true
}, },
{ {
path: 'reports', path: '/reports',
name: 'manage.moderation.reports.list', name: 'manage.moderation.reports.list',
component: () => component: () =>
import( import(
@ -570,7 +570,7 @@ export default new Router({
} }
}, },
{ {
path: 'reports/:id', path: '/reports/:id',
name: 'manage.moderation.reports.detail', name: 'manage.moderation.reports.detail',
component: () => component: () =>
import( import(
@ -579,7 +579,7 @@ export default new Router({
props: true props: true
}, },
{ {
path: 'requests', path: '/requests',
name: 'manage.moderation.requests.list', name: 'manage.moderation.requests.list',
component: () => component: () =>
import( import(
@ -593,7 +593,7 @@ export default new Router({
} }
}, },
{ {
path: 'requests/:id', path: '/requests/:id',
name: 'manage.moderation.requests.detail', name: 'manage.moderation.requests.detail',
component: () => component: () =>
import( import(
@ -609,13 +609,13 @@ export default new Router({
import(/* webpackChunkName: "core" */ '@/components/library/Library'), import(/* webpackChunkName: "core" */ '@/components/library/Library'),
children: [ children: [
{ {
path: '', path: '/',
component: () => component: () =>
import(/* webpackChunkName: "core" */ '@/components/library/Home'), import(/* webpackChunkName: "core" */ '@/components/library/Home'),
name: 'library.index' name: 'library.index'
}, },
{ {
path: 'me', path: '/me',
component: () => component: () =>
import(/* webpackChunkName: "core" */ '@/components/library/Home'), import(/* webpackChunkName: "core" */ '@/components/library/Home'),
name: 'library.me', name: 'library.me',
@ -624,7 +624,7 @@ export default new Router({
}) })
}, },
{ {
path: 'artists/', path: '/artists/',
name: 'library.artists.browse', name: 'library.artists.browse',
component: () => component: () =>
import( import(
@ -641,7 +641,7 @@ export default new Router({
}) })
}, },
{ {
path: 'me/artists', path: '/me/artists',
name: 'library.artists.me', name: 'library.artists.me',
component: () => component: () =>
import( import(
@ -659,7 +659,7 @@ export default new Router({
}) })
}, },
{ {
path: 'albums/', path: '/albums/',
name: 'library.albums.browse', name: 'library.albums.browse',
component: () => component: () =>
import( import(
@ -676,7 +676,7 @@ export default new Router({
}) })
}, },
{ {
path: 'podcasts/', path: '/podcasts/',
name: 'library.podcasts.browse', name: 'library.podcasts.browse',
component: () => component: () =>
import( import(
@ -693,7 +693,7 @@ export default new Router({
}) })
}, },
{ {
path: 'me/albums', path: '/me/albums',
name: 'library.albums.me', name: 'library.albums.me',
component: () => component: () =>
import( import(
@ -711,7 +711,7 @@ export default new Router({
}) })
}, },
{ {
path: 'radios/', path: '/radios/',
name: 'library.radios.browse', name: 'library.radios.browse',
component: () => component: () =>
import( import(
@ -725,7 +725,7 @@ export default new Router({
}) })
}, },
{ {
path: 'me/radios/', path: '/me/radios/',
name: 'library.radios.me', name: 'library.radios.me',
component: () => component: () =>
import( import(
@ -740,7 +740,7 @@ export default new Router({
}) })
}, },
{ {
path: 'radios/build', path: '/radios/build',
name: 'library.radios.build', name: 'library.radios.build',
component: () => component: () =>
import( import(
@ -749,7 +749,7 @@ export default new Router({
props: true props: true
}, },
{ {
path: 'radios/build/:id', path: '/radios/build/:id',
name: 'library.radios.edit', name: 'library.radios.edit',
component: () => component: () =>
import( import(
@ -758,14 +758,14 @@ export default new Router({
props: true props: true
}, },
{ {
path: 'radios/:id', path: '/radios/:id',
name: 'library.radios.detail', name: 'library.radios.detail',
component: () => component: () =>
import(/* webpackChunkName: "radios" */ '@/views/radios/Detail'), import(/* webpackChunkName: "radios" */ '@/views/radios/Detail'),
props: true props: true
}, },
{ {
path: 'playlists/', path: '/playlists/',
name: 'library.playlists.browse', name: 'library.playlists.browse',
component: () => component: () =>
import(/* webpackChunkName: "playlists" */ '@/views/playlists/List'), import(/* webpackChunkName: "playlists" */ '@/views/playlists/List'),
@ -777,7 +777,7 @@ export default new Router({
}) })
}, },
{ {
path: 'me/playlists/', path: '/me/playlists/',
name: 'library.playlists.me', name: 'library.playlists.me',
component: () => component: () =>
import(/* webpackChunkName: "playlists" */ '@/views/playlists/List'), import(/* webpackChunkName: "playlists" */ '@/views/playlists/List'),
@ -790,7 +790,7 @@ export default new Router({
}) })
}, },
{ {
path: 'playlists/:id', path: '/playlists/:id',
name: 'library.playlists.detail', name: 'library.playlists.detail',
component: () => component: () =>
import(/* webpackChunkName: "playlists" */ '@/views/playlists/Detail'), import(/* webpackChunkName: "playlists" */ '@/views/playlists/Detail'),
@ -800,7 +800,7 @@ export default new Router({
}) })
}, },
{ {
path: 'tags/:id', path: '/tags/:id',
name: 'library.tags.detail', name: 'library.tags.detail',
component: () => component: () =>
import( import(
@ -809,7 +809,7 @@ export default new Router({
props: true props: true
}, },
{ {
path: 'artists/:id', path: '/artists/:id',
component: () => component: () =>
import( import(
/* webpackChunkName: "artists" */ '@/components/library/ArtistBase' /* webpackChunkName: "artists" */ '@/components/library/ArtistBase'
@ -817,7 +817,7 @@ export default new Router({
props: true, props: true,
children: [ children: [
{ {
path: '', path: '/',
name: 'library.artists.detail', name: 'library.artists.detail',
component: () => component: () =>
import( import(
@ -825,7 +825,7 @@ export default new Router({
) )
}, },
{ {
path: 'edit', path: '/edit',
name: 'library.artists.edit', name: 'library.artists.edit',
component: () => component: () =>
import( import(
@ -833,7 +833,7 @@ export default new Router({
) )
}, },
{ {
path: 'edit/:editId', path: '/edit/:editId',
name: 'library.artists.edit.detail', name: 'library.artists.edit.detail',
component: () => component: () =>
import( import(
@ -844,7 +844,7 @@ export default new Router({
] ]
}, },
{ {
path: 'albums/:id', path: '/albums/:id',
component: () => component: () =>
import( import(
/* webpackChunkName: "albums" */ '@/components/library/AlbumBase' /* webpackChunkName: "albums" */ '@/components/library/AlbumBase'
@ -852,7 +852,7 @@ export default new Router({
props: true, props: true,
children: [ children: [
{ {
path: '', path: '/',
name: 'library.albums.detail', name: 'library.albums.detail',
component: () => component: () =>
import( import(
@ -860,7 +860,7 @@ export default new Router({
) )
}, },
{ {
path: 'edit', path: '/edit',
name: 'library.albums.edit', name: 'library.albums.edit',
component: () => component: () =>
import( import(
@ -868,7 +868,7 @@ export default new Router({
) )
}, },
{ {
path: 'edit/:editId', path: '/edit/:editId',
name: 'library.albums.edit.detail', name: 'library.albums.edit.detail',
component: () => component: () =>
import( import(
@ -879,7 +879,7 @@ export default new Router({
] ]
}, },
{ {
path: 'tracks/:id', path: '/tracks/:id',
component: () => component: () =>
import( import(
/* webpackChunkName: "tracks" */ '@/components/library/TrackBase' /* webpackChunkName: "tracks" */ '@/components/library/TrackBase'
@ -887,7 +887,7 @@ export default new Router({
props: true, props: true,
children: [ children: [
{ {
path: '', path: '/',
name: 'library.tracks.detail', name: 'library.tracks.detail',
component: () => component: () =>
import( import(
@ -895,7 +895,7 @@ export default new Router({
) )
}, },
{ {
path: 'edit', path: '/edit',
name: 'library.tracks.edit', name: 'library.tracks.edit',
component: () => component: () =>
import( import(
@ -903,7 +903,7 @@ export default new Router({
) )
}, },
{ {
path: 'edit/:editId', path: '/edit/:editId',
name: 'library.tracks.edit.detail', name: 'library.tracks.edit.detail',
component: () => component: () =>
import( import(
@ -914,7 +914,7 @@ export default new Router({
] ]
}, },
{ {
path: 'uploads/:id', path: '/uploads/:id',
name: 'library.uploads.detail', name: 'library.uploads.detail',
props: true, props: true,
component: () => component: () =>
@ -924,7 +924,7 @@ export default new Router({
}, },
{ {
// browse a single library via it's uuid // browse a single library via it's uuid
path: ':id([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})', path: '/:id([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})',
props: true, props: true,
component: () => component: () =>
import( import(
@ -932,7 +932,7 @@ export default new Router({
), ),
children: [ children: [
{ {
path: '', path: '/',
name: 'library.detail', name: 'library.detail',
component: () => component: () =>
import( import(
@ -940,7 +940,7 @@ export default new Router({
) )
}, },
{ {
path: 'albums', path: '/albums',
name: 'library.detail.albums', name: 'library.detail.albums',
component: () => component: () =>
import( import(
@ -948,7 +948,7 @@ export default new Router({
) )
}, },
{ {
path: 'tracks', path: '/tracks',
name: 'library.detail.tracks', name: 'library.detail.tracks',
component: () => component: () =>
import( import(
@ -956,7 +956,7 @@ export default new Router({
) )
}, },
{ {
path: 'edit', path: '/edit',
name: 'library.detail.edit', name: 'library.detail.edit',
component: () => component: () =>
import( import(
@ -964,7 +964,7 @@ export default new Router({
) )
}, },
{ {
path: 'upload', path: '/upload',
name: 'library.detail.upload', name: 'library.detail.upload',
component: () => component: () =>
import( import(
@ -995,7 +995,7 @@ export default new Router({
), ),
children: [ children: [
{ {
path: '', path: '/',
name: 'channels.detail', name: 'channels.detail',
component: () => component: () =>
import( import(
@ -1003,7 +1003,7 @@ export default new Router({
) )
}, },
{ {
path: 'episodes', path: '/episodes',
name: 'channels.detail.episodes', name: 'channels.detail.episodes',
component: () => component: () =>
import( import(

View File

@ -48,6 +48,7 @@ $bottom-player-height: 4rem;
@import "./components/_track_widget.scss"; @import "./components/_track_widget.scss";
@import "./components/_track_table.scss"; @import "./components/_track_table.scss";
@import "./components/_user_link.scss"; @import "./components/_user_link.scss";
@import "./components/user_modal.scss";
@import "./components/_volume_control.scss"; @import "./components/_volume_control.scss";
@import "./components/_loaders.scss"; @import "./components/_loaders.scss";

View File

@ -214,6 +214,10 @@
} }
} }
} }
.ui.user-dropdown .ui.menu {
left: auto;
right: 0;
}
.ui.user-dropdown>.text>.label { .ui.user-dropdown>.text>.label {
margin-right: 0; margin-right: 0;
} }
@ -234,4 +238,4 @@
} }
} }
} }
} }

View File

@ -0,0 +1,31 @@
.ui.overlay.fullscreen.modal {
.user-modal-title,
.user-modal-subtitle {
margin: 0.1em;
}
.user-modal-subtitle {
font-weight: normal;
}
.user-modal.list-icon {
margin-right: 1em;
}
.user-modal.list-item {
font-weight: bold;
font-size: large;
}
a {
color: var(--text-color);
text-decoration: none ;
}
}
.scrolling.dimmable.dimmed {
> .dimmer {
overflow: auto;
--webkit-overflow-scrolling: touch;
}
::-webkit-scrollbar {
width: 0px;
background: transparent;
}
}