Profile menu redesign
This commit is contained in:
parent
274bdd1d3e
commit
58df0d4529
|
@ -165,6 +165,16 @@ def discard_unused_icons(rule):
|
|||
".wrench",
|
||||
".x",
|
||||
".key",
|
||||
".cog",
|
||||
".life.ring",
|
||||
".language",
|
||||
".palette",
|
||||
".sun",
|
||||
".moon",
|
||||
".gitlab",
|
||||
".chevron",
|
||||
".right",
|
||||
".left"
|
||||
]
|
||||
if ":before" not in rule["lines"][0]:
|
||||
return False
|
||||
|
|
|
@ -1,35 +1,47 @@
|
|||
<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 -->
|
||||
<link
|
||||
v-for="url in customStylesheets"
|
||||
:key="url"
|
||||
rel="stylesheet"
|
||||
property="stylesheet"
|
||||
:href="url"
|
||||
:key="url"
|
||||
>
|
||||
<template>
|
||||
<sidebar></sidebar>
|
||||
<set-instance-modal @update:show="showSetInstanceModal = $event" :show="showSetInstanceModal"></set-instance-modal>
|
||||
<service-messages></service-messages>
|
||||
<transition name="queue">
|
||||
<queue @touch-progress="$refs.player.setCurrentTime($event)" v-if="$store.state.ui.queueFocused"></queue>
|
||||
</transition>
|
||||
<router-view role="main" :class="{hidden: $store.state.ui.queueFocused}"></router-view>
|
||||
<player ref="player"></player>
|
||||
<app-footer
|
||||
:class="{hidden: $store.state.ui.queueFocused}"
|
||||
:version="version"
|
||||
@show:shortcuts-modal="showShortcutsModal = !showShortcutsModal"
|
||||
<sidebar
|
||||
:width="width"
|
||||
@show:set-instance-modal="showSetInstanceModal = !showSetInstanceModal"
|
||||
></app-footer>
|
||||
<playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
|
||||
<channel-upload-modal v-if="$store.state.auth.authenticated"></channel-upload-modal>
|
||||
<filter-modal v-if="$store.state.auth.authenticated"></filter-modal>
|
||||
<report-modal></report-modal>
|
||||
<shortcuts-modal @update:show="showShortcutsModal = $event" :show="showShortcutsModal"></shortcuts-modal>
|
||||
@show:shortcuts-modal="showShortcutsModal = !showShortcutsModal"
|
||||
/>
|
||||
<set-instance-modal
|
||||
:show="showSetInstanceModal"
|
||||
@update:show="showSetInstanceModal = $event"
|
||||
/>
|
||||
<service-messages />
|
||||
<transition name="queue">
|
||||
<queue
|
||||
v-if="$store.state.ui.queueFocused"
|
||||
@touch-progress="$refs.player.setCurrentTime($event)"
|
||||
/>
|
||||
</transition>
|
||||
<router-view
|
||||
role="main"
|
||||
:class="{hidden: $store.state.ui.queueFocused}"
|
||||
/>
|
||||
<player ref="player" />
|
||||
<playlist-modal v-if="$store.state.auth.authenticated" />
|
||||
<channel-upload-modal v-if="$store.state.auth.authenticated" />
|
||||
<filter-modal v-if="$store.state.auth.authenticated" />
|
||||
<report-modal />
|
||||
<shortcuts-modal
|
||||
:show="showShortcutsModal"
|
||||
@update:show="showShortcutsModal = $event"
|
||||
/>
|
||||
<GlobalEvents @keydown.h.exact="showShortcutsModal = !showShortcutsModal" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -37,28 +49,26 @@
|
|||
import Vue from 'vue'
|
||||
import axios from 'axios'
|
||||
import _ from '@/lodash'
|
||||
import {mapState, mapGetters, mapActions} from 'vuex'
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import { WebSocketBridge } from 'django-channels'
|
||||
import GlobalEvents from '@/components/utils/global-events'
|
||||
import moment from 'moment'
|
||||
import locales from './locales'
|
||||
import { getClientOnlyRadio } from '@/radios'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
name: 'App',
|
||||
components: {
|
||||
Player: () => import(/* webpackChunkName: "audio" */ "@/components/audio/Player"),
|
||||
Queue: () => import(/* webpackChunkName: "audio" */ "@/components/Queue"),
|
||||
PlaylistModal: () => import(/* webpackChunkName: "auth-audio" */ "@/components/playlists/PlaylistModal"),
|
||||
ChannelUploadModal: () => import(/* webpackChunkName: "auth-audio" */ "@/components/channels/UploadModal"),
|
||||
Sidebar: () => import(/* webpackChunkName: "core" */ "@/components/Sidebar"),
|
||||
AppFooter: () => import(/* webpackChunkName: "core" */ "@/components/Footer"),
|
||||
ServiceMessages: () => import(/* webpackChunkName: "core" */ "@/components/ServiceMessages"),
|
||||
SetInstanceModal: () => import(/* webpackChunkName: "core" */ "@/components/SetInstanceModal"),
|
||||
ShortcutsModal: () => import(/* webpackChunkName: "core" */ "@/components/ShortcutsModal"),
|
||||
FilterModal: () => import(/* webpackChunkName: "moderation" */ "@/components/moderation/FilterModal"),
|
||||
ReportModal: () => import(/* webpackChunkName: "moderation" */ "@/components/moderation/ReportModal"),
|
||||
GlobalEvents,
|
||||
Player: () => import(/* webpackChunkName: "audio" */ '@/components/audio/Player'),
|
||||
Queue: () => import(/* webpackChunkName: "audio" */ '@/components/Queue'),
|
||||
PlaylistModal: () => import(/* webpackChunkName: "auth-audio" */ '@/components/playlists/PlaylistModal'),
|
||||
ChannelUploadModal: () => import(/* webpackChunkName: "auth-audio" */ '@/components/channels/UploadModal'),
|
||||
Sidebar: () => import(/* webpackChunkName: "core" */ '@/components/Sidebar'),
|
||||
ServiceMessages: () => import(/* webpackChunkName: "core" */ '@/components/ServiceMessages'),
|
||||
SetInstanceModal: () => import(/* webpackChunkName: "core" */ '@/components/SetInstanceModal'),
|
||||
ShortcutsModal: () => import(/* webpackChunkName: "core" */ '@/components/ShortcutsModal'),
|
||||
FilterModal: () => import(/* webpackChunkName: "moderation" */ '@/components/moderation/FilterModal'),
|
||||
ReportModal: () => import(/* webpackChunkName: "moderation" */ '@/components/moderation/ReportModal'),
|
||||
GlobalEvents
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -70,23 +80,168 @@ export default {
|
|||
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 () {
|
||||
|
||||
if (navigator.serviceWorker) {
|
||||
navigator.serviceWorker.addEventListener(
|
||||
'controllerchange', () => {
|
||||
if (this.serviceWorker.refreshing) return;
|
||||
if (this.serviceWorker.refreshing) return
|
||||
this.$store.commit('ui/serviceWorker', {
|
||||
refreshing: true
|
||||
})
|
||||
window.location.reload();
|
||||
window.location.reload()
|
||||
}
|
||||
);
|
||||
)
|
||||
}
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
this.handleResize()
|
||||
this.openWebsocket()
|
||||
let self = this
|
||||
const self = this
|
||||
if (!this.$store.state.ui.selectedLanguage) {
|
||||
this.autodetectLanguage()
|
||||
}
|
||||
|
@ -94,7 +249,7 @@ export default {
|
|||
// used to redraw ago dates every minute
|
||||
self.$store.commit('ui/computeLastDate')
|
||||
}, 1000 * 60)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const serverUrl = urlParams.get('_server')
|
||||
if (serverUrl) {
|
||||
this.$store.commit('instance/instanceUrl', serverUrl)
|
||||
|
@ -102,13 +257,12 @@ export default {
|
|||
const url = urlParams.get('_url')
|
||||
if (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:
|
||||
// 1. use the url provided in settings.json, if any
|
||||
// 2. use the url specified when building via VUE_APP_INSTANCE_URL
|
||||
// 3. use the current url
|
||||
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)
|
||||
} else {
|
||||
// needed to trigger initialization of axios / service worker
|
||||
|
@ -153,43 +307,42 @@ export default {
|
|||
})
|
||||
},
|
||||
mounted () {
|
||||
let self = this
|
||||
const self = this
|
||||
// slight hack to allow use to have internal links in <translate> tags
|
||||
// while preserving router behaviour
|
||||
document.documentElement.addEventListener('click', function (event) {
|
||||
if (!event.target.matches('a.internal')) return;
|
||||
if (!event.target.matches('a.internal')) return
|
||||
self.$router.push(event.target.getAttribute('href'))
|
||||
event.preventDefault();
|
||||
}, false);
|
||||
event.preventDefault()
|
||||
}, false)
|
||||
this.$nextTick(() => {
|
||||
document.getElementById('fake-content').classList.add('loaded')
|
||||
})
|
||||
|
||||
},
|
||||
destroyed () {
|
||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||
eventName: 'inbox.item_added',
|
||||
id: 'sidebarCount',
|
||||
id: 'sidebarCount'
|
||||
})
|
||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||
eventName: 'mutation.created',
|
||||
id: 'sidebarReviewEditCount',
|
||||
id: 'sidebarReviewEditCount'
|
||||
})
|
||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||
eventName: 'mutation.updated',
|
||||
id: 'sidebarReviewEditCount',
|
||||
id: 'sidebarReviewEditCount'
|
||||
})
|
||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||
eventName: 'mutation.updated',
|
||||
id: 'sidebarPendingReviewReportCount',
|
||||
id: 'sidebarPendingReviewReportCount'
|
||||
})
|
||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||
eventName: 'user_request.created',
|
||||
id: 'sidebarPendingReviewRequestCount',
|
||||
id: 'sidebarPendingReviewRequestCount'
|
||||
})
|
||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||
eventName: 'Listen',
|
||||
id: 'handleListen',
|
||||
id: 'handleListen'
|
||||
})
|
||||
this.disconnect()
|
||||
},
|
||||
|
@ -208,25 +361,24 @@ export default {
|
|||
},
|
||||
handleListen (event) {
|
||||
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') {
|
||||
getClientOnlyRadio(current).handleListen(current, event, this.$store)
|
||||
}
|
||||
}
|
||||
},
|
||||
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)
|
||||
},
|
||||
autodetectLanguage () {
|
||||
let userLanguage = navigator.language || navigator.userLanguage
|
||||
let available = locales.locales.map(e => { return e.code })
|
||||
let self = this
|
||||
const userLanguage = navigator.language || navigator.userLanguage
|
||||
const available = locales.locales.map(e => { return e.code })
|
||||
let candidate
|
||||
let matching = available.filter((a) => {
|
||||
const matching = available.filter((a) => {
|
||||
return userLanguage.replace('-', '_') === a
|
||||
})
|
||||
let almostMatching = available.filter((a) => {
|
||||
const almostMatching = available.filter((a) => {
|
||||
return userLanguage.replace('-', '_').split('_')[0] === a.split('_')[0]
|
||||
})
|
||||
if (matching.length > 0) {
|
||||
|
@ -249,8 +401,8 @@ export default {
|
|||
return
|
||||
}
|
||||
this.disconnect()
|
||||
let self = this
|
||||
let token = this.$store.state.auth.token
|
||||
const self = this
|
||||
const token = this.$store.state.auth.token
|
||||
// let token = 'test'
|
||||
const bridge = new WebSocketBridge()
|
||||
this.bridge = bridge
|
||||
|
@ -277,9 +429,10 @@ export default {
|
|||
return text
|
||||
},
|
||||
updateDocumentTitle () {
|
||||
let parts = []
|
||||
const parts = []
|
||||
const currentTrackPart = (
|
||||
(this.currentTrack) ? this.getTrackInformationText(this.currentTrack)
|
||||
(this.currentTrack)
|
||||
? this.getTrackInformationText(this.currentTrack)
|
||||
: null)
|
||||
if (currentTrackPart) {
|
||||
parts.push(currentTrackPart)
|
||||
|
@ -293,157 +446,12 @@ export default {
|
|||
|
||||
updateApp () {
|
||||
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' })
|
||||
},
|
||||
handleResize () {
|
||||
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>
|
||||
|
|
|
@ -1,161 +1,469 @@
|
|||
<template>
|
||||
<aside :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar', 'component-sidebar']">
|
||||
<header class="ui basic segment header-wrapper">
|
||||
<router-link :title="'Funkwhale'" :to="{name: logoUrl}">
|
||||
<router-link
|
||||
:title="'Funkwhale'"
|
||||
:to="{name: logoUrl}"
|
||||
>
|
||||
<i class="logo bordered inverted vibrant big icon">
|
||||
<logo class="logo"></logo>
|
||||
<logo class="logo" />
|
||||
<span class="visually-hidden">Home</span>
|
||||
</i>
|
||||
</router-link>
|
||||
<router-link v-if="!$store.state.auth.authenticated" class="logo-wrapper" :to="{name: logoUrl}" :title="'Funkwhale'">
|
||||
<img src="../assets/logo/text-white.svg" alt="" />
|
||||
</router-link>
|
||||
<nav class="top ui compact right aligned inverted text menu">
|
||||
<template v-if="$store.state.auth.authenticated">
|
||||
|
||||
<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">
|
||||
<i class="wrench icon"></i>
|
||||
<i class="wrench icon" />
|
||||
<div
|
||||
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">
|
||||
<h3 class="header">
|
||||
<translate translate-context="Sidebar/Admin/Title/Noun">Administration</translate>
|
||||
<translate translate-context="Sidebar/Admin/Title/Noun">
|
||||
Administration
|
||||
</translate>
|
||||
</h3>
|
||||
<div class="divider"></div>
|
||||
<div class="divider" />
|
||||
<router-link
|
||||
v-if="$store.state.auth.availablePermissions['library']"
|
||||
class="item"
|
||||
:to="{name: 'manage.library.edits', query: {q: 'is_approved:null'}}">
|
||||
:to="{name: 'manage.library.edits', query: {q: 'is_approved:null'}}"
|
||||
>
|
||||
<div
|
||||
v-if="$store.state.ui.notifications.pendingReviewEdits > 0"
|
||||
:title="labels.pendingReviewEdits"
|
||||
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']">
|
||||
{{ $store.state.ui.notifications.pendingReviewEdits }}</div>
|
||||
<translate translate-context="*/*/*/Noun">Library</translate>
|
||||
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']"
|
||||
>
|
||||
{{ $store.state.ui.notifications.pendingReviewEdits }}
|
||||
</div>
|
||||
<translate translate-context="*/*/*/Noun">
|
||||
Library
|
||||
</translate>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="$store.state.auth.availablePermissions['moderation']"
|
||||
class="item"
|
||||
:to="{name: 'manage.moderation.reports.list', query: {q: 'resolved:no'}}">
|
||||
:to="{name: 'manage.moderation.reports.list', query: {q: 'resolved:no'}}"
|
||||
>
|
||||
<div
|
||||
v-if="$store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests> 0"
|
||||
:title="labels.pendingReviewReports"
|
||||
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']">{{ $store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests }}</div>
|
||||
<translate translate-context="*/Moderation/*">Moderation</translate>
|
||||
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']"
|
||||
>
|
||||
{{ $store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests }}
|
||||
</div>
|
||||
<translate translate-context="*/Moderation/*">
|
||||
Moderation
|
||||
</translate>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="$store.state.auth.availablePermissions['settings']"
|
||||
class="item"
|
||||
:to="{name: 'manage.users.users.list'}">
|
||||
<translate translate-context="*/*/*/Noun">Users</translate>
|
||||
:to="{name: 'manage.users.users.list'}"
|
||||
>
|
||||
<translate translate-context="*/*/*/Noun">
|
||||
Users
|
||||
</translate>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="$store.state.auth.availablePermissions['settings']"
|
||||
class="item"
|
||||
:to="{path: '/manage/settings'}">
|
||||
<translate translate-context="*/*/*/Noun">Settings</translate>
|
||||
:to="{path: '/manage/settings'}"
|
||||
>
|
||||
<translate translate-context="*/*/*/Noun">
|
||||
Settings
|
||||
</translate>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<router-link
|
||||
class="item"
|
||||
v-if="$store.state.auth.authenticated"
|
||||
:to="{name: 'content.index'}">
|
||||
<i class="upload icon"></i>
|
||||
class="item"
|
||||
:to="{name: 'content.index'}"
|
||||
>
|
||||
<i class="upload icon" />
|
||||
<span class="visually-hidden">{{ labels.addContent }}</span>
|
||||
</router-link>
|
||||
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'notifications'}">
|
||||
<i class="bell icon"></i>
|
||||
<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>
|
||||
<span v-else class="visually-hidden">{{ labels.notifications }}</span>
|
||||
</router-link>
|
||||
<template v-if="width > 768">
|
||||
<div class="item">
|
||||
<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 v-else :actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username}" />
|
||||
<div class="menu">
|
||||
<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>
|
||||
<router-link class="item" :to="{name: 'logout'}"><translate translate-context="Sidebar/Login/List item.Link/Verb">Logout</translate></router-link>
|
||||
<img
|
||||
v-if="$store.state.auth.authenticated && $store.state.auth.profile.avatar && $store.state.auth.profile.avatar.urls.medium_square_crop"
|
||||
class="ui avatar image"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.urls.medium_square_crop)"
|
||||
>
|
||||
<actor-avatar
|
||||
v-else-if="$store.state.auth.authenticated"
|
||||
:actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username,}"
|
||||
/>
|
||||
<i
|
||||
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>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a
|
||||
href=""
|
||||
class="item"
|
||||
@click.prevent.exact="showUserModal = !showUserModal"
|
||||
>
|
||||
<img
|
||||
v-if="$store.state.auth.authenticated && $store.state.auth.profile.avatar && $store.state.auth.profile.avatar.urls.medium_square_crop"
|
||||
class="ui avatar image"
|
||||
alt=""
|
||||
:src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.urls.medium_square_crop)"
|
||||
>
|
||||
<actor-avatar
|
||||
v-else-if="$store.state.auth.authenticated"
|
||||
:actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username,}"
|
||||
/>
|
||||
<i
|
||||
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>
|
||||
</a>
|
||||
</template>
|
||||
<user-modal
|
||||
:show="showUserModal"
|
||||
@showThemeModalEvent="showThemeModal=true"
|
||||
@showLanguageModalEvent="showLanguageModal=true"
|
||||
@update:show="showUserModal = $event"
|
||||
/>
|
||||
<modal
|
||||
ref="languageModal"
|
||||
:fullscreen="false"
|
||||
:show="showLanguageModal"
|
||||
@update:show="showLanguageModal = $event"
|
||||
>
|
||||
<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 class="content">
|
||||
<fieldset
|
||||
v-for="(language, key) in $language.available"
|
||||
:key="key"
|
||||
>
|
||||
<input
|
||||
:id="key"
|
||||
v-model="languageSelection"
|
||||
type="radio"
|
||||
name="language"
|
||||
:value="key"
|
||||
>
|
||||
<label :for="key">{{ language }}</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
</modal>
|
||||
<modal
|
||||
ref="themeModal"
|
||||
:fullscreen="false"
|
||||
:show="showThemeModal"
|
||||
@update:show="showThemeModal = $event"
|
||||
>
|
||||
<i
|
||||
role="button"
|
||||
class="left chevron back inside icon"
|
||||
@click.prevent.exact="showUserModal = !showUserModal"
|
||||
/>
|
||||
<div class="header">
|
||||
<h3 class="title">
|
||||
{{ labels.theme }}
|
||||
</h3>
|
||||
</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"
|
||||
:class="['ui', 'basic', 'big', {'vibrant': !isCollapsed}, 'inverted icon', 'collapse', 'button']">
|
||||
<i class="sidebar icon"></i></button>
|
||||
>
|
||||
<i class="sidebar icon" />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="ui basic search-wrapper segment">
|
||||
<search-bar @search="isCollapsed = false"></search-bar>
|
||||
<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"></div>
|
||||
<router-link class="ui fluid tiny button" :to="{path: '/signup'}">
|
||||
<translate translate-context="*/Signup/Link/Verb">Create an account</translate>
|
||||
<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>
|
||||
<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"></div>
|
||||
<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="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" @click="exploreExpanded = true" tabindex="0" @focus="exploreExpanded = true">
|
||||
<translate translate-context="*/*/*/Verb">Explore</translate>
|
||||
<i class="angle right icon" v-if="!exploreExpanded"></i>
|
||||
<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"></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>
|
||||
<router-link class="item" :to="{name: 'library.podcasts.browse'}"><i class="podcast icon"></i><translate translate-context="*/*/*">Podcasts</translate></router-link>
|
||||
<router-link class="item" :to="{name: 'library.albums.browse'}"><i class="compact disc icon"></i><translate translate-context="*/*/*">Albums</translate></router-link>
|
||||
<router-link class="item" :to="{name: 'library.artists.browse'}"><i class="user icon"></i><translate translate-context="*/*/*">Artists</translate></router-link>
|
||||
<router-link class="item" :to="{name: 'library.playlists.browse'}"><i class="list icon"></i><translate translate-context="*/*/*">Playlists</translate></router-link>
|
||||
<router-link class="item" :to="{name: 'library.radios.browse'}"><i class="feed icon"></i><translate translate-context="*/*/*">Radios</translate></router-link>
|
||||
<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 :class="[{collapsed: !myLibraryExpanded}, 'collapsible item']" v-if="$store.state.auth.authenticated">
|
||||
<h3 class="header" role="button" @click="myLibraryExpanded = true" tabindex="0" @focus="myLibraryExpanded = true">
|
||||
<translate translate-context="*/*/*/Noun">My Library</translate>
|
||||
<i class="angle right icon" v-if="!myLibraryExpanded"></i>
|
||||
<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"></i><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"></i><translate translate-context="*/*/*">Albums</translate></router-link>
|
||||
<router-link class="item" :to="{name: 'library.artists.me'}"><i class="user icon"></i><translate translate-context="*/*/*">Artists</translate></router-link>
|
||||
<router-link class="item" :to="{name: 'library.playlists.me'}"><i class="list icon"></i><translate translate-context="*/*/*">Playlists</translate></router-link>
|
||||
<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>
|
||||
<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 class="header item" :to="{name: 'subscriptions'}" v-if="$store.state.auth.authenticated">
|
||||
<translate translate-context="*/*/*">Channels</translate>
|
||||
<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>
|
||||
<translate translate-context="Footer/About/List item.Link">
|
||||
More
|
||||
</translate>
|
||||
</h3>
|
||||
<div class="menu">
|
||||
<router-link class="item" to="/about">
|
||||
<i class="info icon"></i><translate translate-context="Sidebar/*/List item.Link">About this pod</translate>
|
||||
<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>
|
||||
|
@ -163,26 +471,39 @@
|
|||
</template>
|
||||
|
||||
<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 SearchBar from "@/components/audio/SearchBar"
|
||||
|
||||
import $ from "jquery"
|
||||
import $ from 'jquery'
|
||||
|
||||
export default {
|
||||
name: "sidebar",
|
||||
name: 'Sidebar',
|
||||
components: {
|
||||
SearchBar,
|
||||
Logo
|
||||
Logo,
|
||||
UserMenu,
|
||||
UserModal,
|
||||
Modal
|
||||
},
|
||||
props: {
|
||||
width: { type: Number, required: true }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selectedTab: "library",
|
||||
selectedTab: 'library',
|
||||
isCollapsed: true,
|
||||
fetchInterval: null,
|
||||
exploreExpanded: false,
|
||||
myLibraryExpanded: false,
|
||||
showUserModal: false,
|
||||
showLanguageModal: false,
|
||||
showThemeModal: false,
|
||||
languageSelection: this.$language.current,
|
||||
themeSelection: this.$store.state.ui.theme
|
||||
}
|
||||
},
|
||||
destroy () {
|
||||
|
@ -190,63 +511,65 @@ export default {
|
|||
clearInterval(this.fetchInterval)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
document.getElementById('fake-sidebar').classList.add('loaded')
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
additionalNotifications: "ui/additionalNotifications",
|
||||
}),
|
||||
...mapState({
|
||||
queue: state => state.queue,
|
||||
url: state => state.route.path
|
||||
}),
|
||||
...mapGetters({
|
||||
additionalNotifications: 'ui/additionalNotifications'
|
||||
}),
|
||||
labels () {
|
||||
let mainMenu = this.$pgettext('Sidebar/*/Hidden text', "Main menu")
|
||||
let selectTrack = this.$pgettext('Sidebar/Player/Hidden text', "Play this track")
|
||||
let pendingFollows = this.$pgettext('Sidebar/Notifications/Hidden text', "Pending follow requests")
|
||||
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 {
|
||||
pendingFollows,
|
||||
mainMenu,
|
||||
selectTrack,
|
||||
pendingReviewEdits,
|
||||
addContent: this.$pgettext("*/Library/*/Verb", 'Add content'),
|
||||
notifications: this.$pgettext("*/Notifications/*", 'Notifications'),
|
||||
administration: this.$pgettext("Sidebar/Admin/Title/Noun", 'Administration'),
|
||||
language,
|
||||
theme,
|
||||
addContent: this.$pgettext('*/Library/*/Verb', 'Add content'),
|
||||
administration: this.$pgettext('Sidebar/Admin/Title/Noun', 'Administration')
|
||||
}
|
||||
},
|
||||
logoUrl () {
|
||||
if (this.$store.state.auth.authenticated) {
|
||||
return "library.index"
|
||||
return 'library.index'
|
||||
} else {
|
||||
return "index"
|
||||
return 'index'
|
||||
}
|
||||
},
|
||||
focusedMenu () {
|
||||
let mapping = {
|
||||
"search": 'exploreExpanded',
|
||||
"library.index": 'exploreExpanded',
|
||||
"library.podcasts.browse": 'exploreExpanded',
|
||||
"library.albums.browse": 'exploreExpanded',
|
||||
"library.albums.detail": 'exploreExpanded',
|
||||
"library.artists.browse": 'exploreExpanded',
|
||||
"library.artists.detail": 'exploreExpanded',
|
||||
"library.tracks.detail": 'exploreExpanded',
|
||||
"library.playlists.browse": 'exploreExpanded',
|
||||
"library.playlists.detail": 'exploreExpanded',
|
||||
"library.radios.browse": 'exploreExpanded',
|
||||
"library.radios.detail": 'exploreExpanded',
|
||||
'library.me': "myLibraryExpanded",
|
||||
'library.albums.me': "myLibraryExpanded",
|
||||
'library.artists.me': "myLibraryExpanded",
|
||||
'library.playlists.me': "myLibraryExpanded",
|
||||
'library.radios.me': "myLibraryExpanded",
|
||||
'favorites': "myLibraryExpanded",
|
||||
const mapping = {
|
||||
search: 'exploreExpanded',
|
||||
'library.index': 'exploreExpanded',
|
||||
'library.podcasts.browse': 'exploreExpanded',
|
||||
'library.albums.browse': 'exploreExpanded',
|
||||
'library.albums.detail': 'exploreExpanded',
|
||||
'library.artists.browse': 'exploreExpanded',
|
||||
'library.artists.detail': 'exploreExpanded',
|
||||
'library.tracks.detail': 'exploreExpanded',
|
||||
'library.playlists.browse': 'exploreExpanded',
|
||||
'library.playlists.detail': 'exploreExpanded',
|
||||
'library.radios.browse': 'exploreExpanded',
|
||||
'library.radios.detail': 'exploreExpanded',
|
||||
'library.me': 'myLibraryExpanded',
|
||||
'library.albums.me': 'myLibraryExpanded',
|
||||
'library.artists.me': 'myLibraryExpanded',
|
||||
'library.playlists.me': 'myLibraryExpanded',
|
||||
'library.radios.me': 'myLibraryExpanded',
|
||||
favorites: 'myLibraryExpanded'
|
||||
}
|
||||
let m = mapping[this.$route.name]
|
||||
const m = mapping[this.$route.name]
|
||||
if (m) {
|
||||
return m
|
||||
}
|
||||
|
@ -263,57 +586,31 @@ export default {
|
|||
this.$store.state.ui.notifications.pendingReviewReports +
|
||||
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)
|
||||
}
|
||||
})
|
||||
production () {
|
||||
return process.env.NODE_ENV === 'production'
|
||||
},
|
||||
setupDropdown (selector) {
|
||||
let 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
|
||||
let link = $($el).closest('a')
|
||||
let url = link.attr('href')
|
||||
self.$router.push(url)
|
||||
$(self.$el).find(selector).dropdown('hide')
|
||||
themes () {
|
||||
return [
|
||||
{
|
||||
name: this.$pgettext('Sidebar/Settings/Dropdown.Label/Theme name', 'Light'),
|
||||
key: 'light'
|
||||
},
|
||||
{
|
||||
name: this.$pgettext('Sidebar/Settings/Dropdown.Label/Theme name', 'Dark'),
|
||||
key: 'dark'
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
url: function () {
|
||||
this.isCollapsed = true
|
||||
},
|
||||
"$store.state.moderation.lastUpdate": function () {
|
||||
'$store.state.moderation.lastUpdate': function () {
|
||||
this.applyContentFilters()
|
||||
},
|
||||
"$store.state.auth.authenticated": {
|
||||
'$store.state.auth.authenticated': {
|
||||
immediate: true,
|
||||
handler (v) {
|
||||
if (v) {
|
||||
|
@ -321,17 +618,21 @@ export default {
|
|||
this.setupDropdown('.user-dropdown')
|
||||
this.setupDropdown('.admin-dropdown')
|
||||
})
|
||||
} else {
|
||||
this.$nextTick(() => {
|
||||
this.setupDropdown('.user-dropdown')
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
"$store.state.auth.availablePermissions": {
|
||||
'$store.state.auth.availablePermissions': {
|
||||
immediate: true,
|
||||
handler (v) {
|
||||
this.$nextTick(() => {
|
||||
this.setupDropdown('.admin-dropdown')
|
||||
})
|
||||
},
|
||||
deep: true,
|
||||
deep: true
|
||||
},
|
||||
focusedMenu: {
|
||||
immediate: true,
|
||||
|
@ -351,6 +652,93 @@ export default {
|
|||
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>
|
||||
<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>
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
<template>
|
||||
<modal
|
||||
@update:show="$emit('update:show', $event)"
|
||||
ref="modal"
|
||||
:show="show"
|
||||
:scrolling="true"
|
||||
:additionalClasses="['scrolling-track-options']"
|
||||
:additional-classes="['scrolling-track-options']"
|
||||
@update:show="$emit('update:show', $event)"
|
||||
>
|
||||
<div class="header">
|
||||
<div class="ui large centered rounded image">
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
v-if="
|
||||
track.album && track.album.cover && track.album.cover.urls.original
|
||||
"
|
||||
|
@ -18,43 +17,50 @@
|
|||
track.album.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
>
|
||||
<img
|
||||
v-else-if="track.cover"
|
||||
v-lazy="
|
||||
$store.getters['instance/absoluteUrl'](
|
||||
track.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
>
|
||||
<img
|
||||
v-else-if="track.artist.cover"
|
||||
v-lazy="
|
||||
$store.getters['instance/absoluteUrl'](
|
||||
track.artist.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
src="../../../assets/audio/default-cover.png"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<h3 class="track-modal-title">{{ track.title }}</h3>
|
||||
<h4 class="track-modal-subtitle">{{ track.artist.name }}</h4>
|
||||
<h3 class="track-modal-title">
|
||||
{{ track.title }}
|
||||
</h3>
|
||||
<h4 class="track-modal-subtitle">
|
||||
{{ track.artist.name }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui hidden divider" />
|
||||
<div class="content">
|
||||
<div class="ui one column unstackable grid">
|
||||
<div
|
||||
v-if="$store.state.auth.authenticated && track.artist.content_category !== 'podcast'"
|
||||
class="row"
|
||||
v-if="$store.state.auth.authenticated && this.track.artist.content_category !== 'podcast'">
|
||||
>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="column"
|
||||
|
@ -80,11 +86,11 @@
|
|||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="labels.addToQueue"
|
||||
@click.stop.prevent="
|
||||
add();
|
||||
closeModal();
|
||||
$refs.modal.closeModal();
|
||||
"
|
||||
:aria-label="labels.addToQueue"
|
||||
>
|
||||
<i class="plus icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{ labels.addToQueue }}</span>
|
||||
|
@ -94,11 +100,11 @@
|
|||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="labels.playNext"
|
||||
@click.stop.prevent="
|
||||
addNext(true);
|
||||
closeModal();
|
||||
$refs.modal.closeModal();
|
||||
"
|
||||
:aria-label="labels.playNext"
|
||||
>
|
||||
<i class="step forward icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{ labels.playNext }}</span>
|
||||
|
@ -108,14 +114,14 @@
|
|||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="labels.startRadio"
|
||||
@click.stop.prevent="
|
||||
$store.dispatch('radios/start', {
|
||||
type: 'similar',
|
||||
objectId: track.id,
|
||||
});
|
||||
closeModal();
|
||||
$refs.modal.closeModal();
|
||||
"
|
||||
:aria-label="labels.startRadio"
|
||||
>
|
||||
<i class="rss icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{ labels.startRadio }}</span>
|
||||
|
@ -125,8 +131,8 @@
|
|||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
@click.stop="$store.commit('playlists/chooseTrack', track)"
|
||||
:aria-label="labels.addToPlaylist"
|
||||
@click.stop="$store.commit('playlists/chooseTrack', track)"
|
||||
>
|
||||
<i class="list icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{
|
||||
|
@ -134,8 +140,11 @@
|
|||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div v-if="!isAlbum && track.album" class="row">
|
||||
<div class="ui divider" />
|
||||
<div
|
||||
v-if="!isAlbum && track.album"
|
||||
class="row"
|
||||
>
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
|
@ -153,7 +162,10 @@
|
|||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isArtist" class="row">
|
||||
<div
|
||||
v-if="!isArtist"
|
||||
class="row"
|
||||
>
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
|
@ -189,7 +201,7 @@
|
|||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui divider" />
|
||||
<div
|
||||
v-for="obj in getReportableObjs({
|
||||
track,
|
||||
|
@ -197,16 +209,15 @@
|
|||
artist,
|
||||
})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
class="row"
|
||||
:ref="`report${obj.target.type}${obj.target.id}`"
|
||||
class="row"
|
||||
:data-ref="`report${obj.target.type}${obj.target.id}`"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
|
||||
>
|
||||
<div class="column">
|
||||
<i class="share icon track-modal list-icon" /><span
|
||||
class="track-modal list-item"
|
||||
>{{ obj.label }}</span
|
||||
>
|
||||
>{{ obj.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -215,90 +226,83 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "@/components/semantic/Modal";
|
||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
||||
import Modal from '@/components/semantic/Modal'
|
||||
import ReportMixin from '@/components/mixins/Report'
|
||||
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal
|
||||
},
|
||||
mixins: [ReportMixin, PlayOptionsMixin],
|
||||
props: {
|
||||
show: { type: Boolean, required: true, default: false },
|
||||
track: { type: Object, required: true },
|
||||
index: { type: Number, required: true },
|
||||
isArtist: { type: Boolean, required: false, default: false },
|
||||
isAlbum: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
components: {
|
||||
Modal,
|
||||
TrackFavoriteIcon,
|
||||
isAlbum: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isShowing: this.show,
|
||||
tracks: [this.track],
|
||||
album: this.track.album,
|
||||
artist: this.track.artist,
|
||||
};
|
||||
artist: this.track.artist
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isFavorite () {
|
||||
return this.$store.getters["favorites/isFavorite"](this.track.id);
|
||||
return this.$store.getters['favorites/isFavorite'](this.track.id)
|
||||
},
|
||||
favoriteButton () {
|
||||
if (this.isFavorite) {
|
||||
return this.$pgettext(
|
||||
"Content/Track/Icon.Tooltip/Verb",
|
||||
"Remove from favorites"
|
||||
);
|
||||
'Content/Track/Icon.Tooltip/Verb',
|
||||
'Remove from favorites'
|
||||
)
|
||||
} else {
|
||||
return this.$pgettext("Content/Track/*/Verb", "Add to favorites");
|
||||
return this.$pgettext('Content/Track/*/Verb', 'Add to favorites')
|
||||
}
|
||||
},
|
||||
trackDetailsButton () {
|
||||
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 {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Track details")
|
||||
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'Track details')
|
||||
}
|
||||
},
|
||||
albumDetailsButton () {
|
||||
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 {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View album")
|
||||
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View album')
|
||||
}
|
||||
},
|
||||
artistDetailsButton () {
|
||||
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 {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View artist")
|
||||
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View artist')
|
||||
}
|
||||
},
|
||||
labels () {
|
||||
return {
|
||||
startRadio: this.$pgettext(
|
||||
"*/Queue/Dropdown/Button/Title",
|
||||
"Play radio"
|
||||
'*/Queue/Dropdown/Button/Title',
|
||||
'Play radio'
|
||||
),
|
||||
playNow: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play now"),
|
||||
playNow: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play now'),
|
||||
addToQueue: this.$pgettext(
|
||||
"*/Queue/Dropdown/Button/Title",
|
||||
"Add to queue"
|
||||
'*/Queue/Dropdown/Button/Title',
|
||||
'Add to queue'
|
||||
),
|
||||
playNext: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play next"),
|
||||
playNext: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play next'),
|
||||
addToPlaylist: this.$pgettext(
|
||||
"Sidebar/Player/Icon.Tooltip/Verb",
|
||||
"Add to playlist…"
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeModal() {
|
||||
this.$emit("update:show", false);
|
||||
},
|
||||
},
|
||||
};
|
||||
'Sidebar/Player/Icon.Tooltip/Verb',
|
||||
'Add to playlist…'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
<template>
|
||||
<modal
|
||||
@update:show="$emit('update:show', $event)"
|
||||
ref="modal"
|
||||
:show="show"
|
||||
:scrolling="true"
|
||||
:additionalClasses="['scrolling-track-options']"
|
||||
:additional-classes="['scrolling-track-options']"
|
||||
@update:show="$emit('update:show', $event)"
|
||||
>
|
||||
<div class="header">
|
||||
<div class="ui large centered rounded image">
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
v-if="
|
||||
track.album && track.album.cover && track.album.cover.urls.original
|
||||
"
|
||||
|
@ -18,43 +17,50 @@
|
|||
track.album.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
>
|
||||
<img
|
||||
v-else-if="track.cover"
|
||||
v-lazy="
|
||||
$store.getters['instance/absoluteUrl'](
|
||||
track.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
>
|
||||
<img
|
||||
v-else-if="track.artist.cover"
|
||||
v-lazy="
|
||||
$store.getters['instance/absoluteUrl'](
|
||||
track.artist.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
src="../../../assets/audio/default-cover.png"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<h3 class="track-modal-title">{{ track.title }}</h3>
|
||||
<h4 class="track-modal-subtitle">{{ track.artist.name }}</h4>
|
||||
<h3 class="track-modal-title">
|
||||
{{ track.title }}
|
||||
</h3>
|
||||
<h4 class="track-modal-subtitle">
|
||||
{{ track.artist.name }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui hidden divider" />
|
||||
<div class="content">
|
||||
<div class="ui one column unstackable grid">
|
||||
<div
|
||||
v-if="$store.state.auth.authenticated && track.artist.content_category !== 'podcast'"
|
||||
class="row"
|
||||
v-if="$store.state.auth.authenticated && this.track.artist.content_category !== 'podcast'">
|
||||
>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="column"
|
||||
|
@ -80,11 +86,11 @@
|
|||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="labels.addToQueue"
|
||||
@click.stop.prevent="
|
||||
add();
|
||||
closeModal();
|
||||
$refs.modal.closeModal();
|
||||
"
|
||||
:aria-label="labels.addToQueue"
|
||||
>
|
||||
<i class="plus icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{ labels.addToQueue }}</span>
|
||||
|
@ -94,11 +100,11 @@
|
|||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="labels.playNext"
|
||||
@click.stop.prevent="
|
||||
addNext(true);
|
||||
closeModal();
|
||||
$refs.modal.closeModal();
|
||||
"
|
||||
:aria-label="labels.playNext"
|
||||
>
|
||||
<i class="step forward icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{ labels.playNext }}</span>
|
||||
|
@ -108,14 +114,14 @@
|
|||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="labels.startRadio"
|
||||
@click.stop.prevent="
|
||||
$store.dispatch('radios/start', {
|
||||
type: 'similar',
|
||||
objectId: track.id,
|
||||
});
|
||||
closeModal();
|
||||
$refs.modal.closeModal();
|
||||
"
|
||||
:aria-label="labels.startRadio"
|
||||
>
|
||||
<i class="rss icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{ labels.startRadio }}</span>
|
||||
|
@ -125,8 +131,8 @@
|
|||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
@click.stop="$store.commit('playlists/chooseTrack', track)"
|
||||
:aria-label="labels.addToPlaylist"
|
||||
@click.stop="$store.commit('playlists/chooseTrack', track)"
|
||||
>
|
||||
<i class="list icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{
|
||||
|
@ -134,8 +140,11 @@
|
|||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div v-if="!isAlbum && track.album" class="row">
|
||||
<div class="ui divider" />
|
||||
<div
|
||||
v-if="!isAlbum && track.album"
|
||||
class="row"
|
||||
>
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
|
@ -153,7 +162,10 @@
|
|||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isArtist" class="row">
|
||||
<div
|
||||
v-if="!isArtist"
|
||||
class="row"
|
||||
>
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
|
@ -189,7 +201,7 @@
|
|||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui divider" />
|
||||
<div
|
||||
v-for="obj in getReportableObjs({
|
||||
track,
|
||||
|
@ -197,16 +209,15 @@
|
|||
artist,
|
||||
})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
class="row"
|
||||
:ref="`report${obj.target.type}${obj.target.id}`"
|
||||
class="row"
|
||||
:data-ref="`report${obj.target.type}${obj.target.id}`"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
|
||||
>
|
||||
<div class="column">
|
||||
<i class="share icon track-modal list-icon" /><span
|
||||
class="track-modal list-item"
|
||||
>{{ obj.label }}</span
|
||||
>
|
||||
>{{ obj.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -215,90 +226,83 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "@/components/semantic/Modal";
|
||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
||||
import Modal from '@/components/semantic/Modal'
|
||||
import ReportMixin from '@/components/mixins/Report'
|
||||
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal
|
||||
},
|
||||
mixins: [ReportMixin, PlayOptionsMixin],
|
||||
props: {
|
||||
show: { type: Boolean, required: true, default: false },
|
||||
track: { type: Object, required: true },
|
||||
index: { type: Number, required: true },
|
||||
isArtist: { type: Boolean, required: false, default: false },
|
||||
isAlbum: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
components: {
|
||||
Modal,
|
||||
TrackFavoriteIcon,
|
||||
isAlbum: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isShowing: this.show,
|
||||
tracks: [this.track],
|
||||
album: this.track.album,
|
||||
artist: this.track.artist,
|
||||
};
|
||||
artist: this.track.artist
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isFavorite () {
|
||||
return this.$store.getters["favorites/isFavorite"](this.track.id);
|
||||
return this.$store.getters['favorites/isFavorite'](this.track.id)
|
||||
},
|
||||
favoriteButton () {
|
||||
if (this.isFavorite) {
|
||||
return this.$pgettext(
|
||||
"Content/Track/Icon.Tooltip/Verb",
|
||||
"Remove from favorites"
|
||||
);
|
||||
'Content/Track/Icon.Tooltip/Verb',
|
||||
'Remove from favorites'
|
||||
)
|
||||
} else {
|
||||
return this.$pgettext("Content/Track/*/Verb", "Add to favorites");
|
||||
return this.$pgettext('Content/Track/*/Verb', 'Add to favorites')
|
||||
}
|
||||
},
|
||||
trackDetailsButton () {
|
||||
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 {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Track details")
|
||||
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'Track details')
|
||||
}
|
||||
},
|
||||
albumDetailsButton () {
|
||||
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 {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View album")
|
||||
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View album')
|
||||
}
|
||||
},
|
||||
artistDetailsButton () {
|
||||
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 {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View artist")
|
||||
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View artist')
|
||||
}
|
||||
},
|
||||
labels () {
|
||||
return {
|
||||
startRadio: this.$pgettext(
|
||||
"*/Queue/Dropdown/Button/Title",
|
||||
"Play radio"
|
||||
'*/Queue/Dropdown/Button/Title',
|
||||
'Play radio'
|
||||
),
|
||||
playNow: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play now"),
|
||||
playNow: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play now'),
|
||||
addToQueue: this.$pgettext(
|
||||
"*/Queue/Dropdown/Button/Title",
|
||||
"Add to queue"
|
||||
'*/Queue/Dropdown/Button/Title',
|
||||
'Add to queue'
|
||||
),
|
||||
playNext: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play next"),
|
||||
playNext: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play next'),
|
||||
addToPlaylist: this.$pgettext(
|
||||
"Sidebar/Player/Icon.Tooltip/Verb",
|
||||
"Add to playlist…"
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeModal() {
|
||||
this.$emit("update:show", false);
|
||||
},
|
||||
},
|
||||
};
|
||||
'Sidebar/Player/Icon.Tooltip/Verb',
|
||||
'Add to playlist…'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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>
|
|
@ -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>
|
|
@ -1,9 +1,10 @@
|
|||
<template>
|
||||
<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>
|
||||
<slot v-if="show">
|
||||
|
||||
</slot>
|
||||
<i
|
||||
tabindex="0"
|
||||
class="close inside icon"
|
||||
/>
|
||||
<slot v-if="show" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -21,7 +22,33 @@ export default {
|
|||
data () {
|
||||
return {
|
||||
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 () {
|
||||
|
@ -52,29 +79,9 @@ export default {
|
|||
this.focusTrap.unpause()
|
||||
}.bind(this)
|
||||
})
|
||||
}
|
||||
},
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
closeModal () {
|
||||
this.$emit('update:show', false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -224,7 +224,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'activity',
|
||||
path: '/activity',
|
||||
name: `profile${route.suffix}.activity`,
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -318,7 +318,7 @@ export default new Router({
|
|||
import(/* webpackChunkName: "admin" */ '@/views/admin/library/Base'),
|
||||
children: [
|
||||
{
|
||||
path: 'edits',
|
||||
path: '/edits',
|
||||
name: 'manage.library.edits',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -331,7 +331,7 @@ export default new Router({
|
|||
}
|
||||
},
|
||||
{
|
||||
path: 'artists',
|
||||
path: '/artists',
|
||||
name: 'manage.library.artists',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -344,7 +344,7 @@ export default new Router({
|
|||
}
|
||||
},
|
||||
{
|
||||
path: 'artists/:id',
|
||||
path: '/artists/:id',
|
||||
name: 'manage.library.artists.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -353,7 +353,7 @@ export default new Router({
|
|||
props: true
|
||||
},
|
||||
{
|
||||
path: 'channels',
|
||||
path: '/channels',
|
||||
name: 'manage.channels',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -366,7 +366,7 @@ export default new Router({
|
|||
}
|
||||
},
|
||||
{
|
||||
path: 'channels/:id',
|
||||
path: '/channels/:id',
|
||||
name: 'manage.channels.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -375,7 +375,7 @@ export default new Router({
|
|||
props: true
|
||||
},
|
||||
{
|
||||
path: 'albums',
|
||||
path: '/albums',
|
||||
name: 'manage.library.albums',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -388,7 +388,7 @@ export default new Router({
|
|||
}
|
||||
},
|
||||
{
|
||||
path: 'albums/:id',
|
||||
path: '/albums/:id',
|
||||
name: 'manage.library.albums.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -397,7 +397,7 @@ export default new Router({
|
|||
props: true
|
||||
},
|
||||
{
|
||||
path: 'tracks',
|
||||
path: '/tracks',
|
||||
name: 'manage.library.tracks',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -410,7 +410,7 @@ export default new Router({
|
|||
}
|
||||
},
|
||||
{
|
||||
path: 'tracks/:id',
|
||||
path: '/tracks/:id',
|
||||
name: 'manage.library.tracks.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -419,7 +419,7 @@ export default new Router({
|
|||
props: true
|
||||
},
|
||||
{
|
||||
path: 'libraries',
|
||||
path: '/libraries',
|
||||
name: 'manage.library.libraries',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -432,7 +432,7 @@ export default new Router({
|
|||
}
|
||||
},
|
||||
{
|
||||
path: 'libraries/:id',
|
||||
path: '/libraries/:id',
|
||||
name: 'manage.library.libraries.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -441,7 +441,7 @@ export default new Router({
|
|||
props: true
|
||||
},
|
||||
{
|
||||
path: 'uploads',
|
||||
path: '/uploads',
|
||||
name: 'manage.library.uploads',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -454,7 +454,7 @@ export default new Router({
|
|||
}
|
||||
},
|
||||
{
|
||||
path: 'uploads/:id',
|
||||
path: '/uploads/:id',
|
||||
name: 'manage.library.uploads.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -463,7 +463,7 @@ export default new Router({
|
|||
props: true
|
||||
},
|
||||
{
|
||||
path: 'tags',
|
||||
path: '/tags',
|
||||
name: 'manage.library.tags',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -476,7 +476,7 @@ export default new Router({
|
|||
}
|
||||
},
|
||||
{
|
||||
path: 'tags/:id',
|
||||
path: '/tags/:id',
|
||||
name: 'manage.library.tags.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -493,7 +493,7 @@ export default new Router({
|
|||
import(/* webpackChunkName: "admin" */ '@/views/admin/users/Base'),
|
||||
children: [
|
||||
{
|
||||
path: 'users',
|
||||
path: '/users',
|
||||
name: 'manage.users.users.list',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -501,7 +501,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'invitations',
|
||||
path: '/invitations',
|
||||
name: 'manage.users.invitations.list',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -517,7 +517,7 @@ export default new Router({
|
|||
import(/* webpackChunkName: "admin" */ '@/views/admin/moderation/Base'),
|
||||
children: [
|
||||
{
|
||||
path: 'domains',
|
||||
path: '/domains',
|
||||
name: 'manage.moderation.domains.list',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -525,7 +525,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'domains/:id',
|
||||
path: '/domains/:id',
|
||||
name: 'manage.moderation.domains.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -534,7 +534,7 @@ export default new Router({
|
|||
props: true
|
||||
},
|
||||
{
|
||||
path: 'accounts',
|
||||
path: '/accounts',
|
||||
name: 'manage.moderation.accounts.list',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -547,7 +547,7 @@ export default new Router({
|
|||
}
|
||||
},
|
||||
{
|
||||
path: 'accounts/:id',
|
||||
path: '/accounts/:id',
|
||||
name: 'manage.moderation.accounts.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -556,7 +556,7 @@ export default new Router({
|
|||
props: true
|
||||
},
|
||||
{
|
||||
path: 'reports',
|
||||
path: '/reports',
|
||||
name: 'manage.moderation.reports.list',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -570,7 +570,7 @@ export default new Router({
|
|||
}
|
||||
},
|
||||
{
|
||||
path: 'reports/:id',
|
||||
path: '/reports/:id',
|
||||
name: 'manage.moderation.reports.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -579,7 +579,7 @@ export default new Router({
|
|||
props: true
|
||||
},
|
||||
{
|
||||
path: 'requests',
|
||||
path: '/requests',
|
||||
name: 'manage.moderation.requests.list',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -593,7 +593,7 @@ export default new Router({
|
|||
}
|
||||
},
|
||||
{
|
||||
path: 'requests/:id',
|
||||
path: '/requests/:id',
|
||||
name: 'manage.moderation.requests.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -609,13 +609,13 @@ export default new Router({
|
|||
import(/* webpackChunkName: "core" */ '@/components/library/Library'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
path: '/',
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "core" */ '@/components/library/Home'),
|
||||
name: 'library.index'
|
||||
},
|
||||
{
|
||||
path: 'me',
|
||||
path: '/me',
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "core" */ '@/components/library/Home'),
|
||||
name: 'library.me',
|
||||
|
@ -624,7 +624,7 @@ export default new Router({
|
|||
})
|
||||
},
|
||||
{
|
||||
path: 'artists/',
|
||||
path: '/artists/',
|
||||
name: 'library.artists.browse',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -641,7 +641,7 @@ export default new Router({
|
|||
})
|
||||
},
|
||||
{
|
||||
path: 'me/artists',
|
||||
path: '/me/artists',
|
||||
name: 'library.artists.me',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -659,7 +659,7 @@ export default new Router({
|
|||
})
|
||||
},
|
||||
{
|
||||
path: 'albums/',
|
||||
path: '/albums/',
|
||||
name: 'library.albums.browse',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -676,7 +676,7 @@ export default new Router({
|
|||
})
|
||||
},
|
||||
{
|
||||
path: 'podcasts/',
|
||||
path: '/podcasts/',
|
||||
name: 'library.podcasts.browse',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -693,7 +693,7 @@ export default new Router({
|
|||
})
|
||||
},
|
||||
{
|
||||
path: 'me/albums',
|
||||
path: '/me/albums',
|
||||
name: 'library.albums.me',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -711,7 +711,7 @@ export default new Router({
|
|||
})
|
||||
},
|
||||
{
|
||||
path: 'radios/',
|
||||
path: '/radios/',
|
||||
name: 'library.radios.browse',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -725,7 +725,7 @@ export default new Router({
|
|||
})
|
||||
},
|
||||
{
|
||||
path: 'me/radios/',
|
||||
path: '/me/radios/',
|
||||
name: 'library.radios.me',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -740,7 +740,7 @@ export default new Router({
|
|||
})
|
||||
},
|
||||
{
|
||||
path: 'radios/build',
|
||||
path: '/radios/build',
|
||||
name: 'library.radios.build',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -749,7 +749,7 @@ export default new Router({
|
|||
props: true
|
||||
},
|
||||
{
|
||||
path: 'radios/build/:id',
|
||||
path: '/radios/build/:id',
|
||||
name: 'library.radios.edit',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -758,14 +758,14 @@ export default new Router({
|
|||
props: true
|
||||
},
|
||||
{
|
||||
path: 'radios/:id',
|
||||
path: '/radios/:id',
|
||||
name: 'library.radios.detail',
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "radios" */ '@/views/radios/Detail'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: 'playlists/',
|
||||
path: '/playlists/',
|
||||
name: 'library.playlists.browse',
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "playlists" */ '@/views/playlists/List'),
|
||||
|
@ -777,7 +777,7 @@ export default new Router({
|
|||
})
|
||||
},
|
||||
{
|
||||
path: 'me/playlists/',
|
||||
path: '/me/playlists/',
|
||||
name: 'library.playlists.me',
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "playlists" */ '@/views/playlists/List'),
|
||||
|
@ -790,7 +790,7 @@ export default new Router({
|
|||
})
|
||||
},
|
||||
{
|
||||
path: 'playlists/:id',
|
||||
path: '/playlists/:id',
|
||||
name: 'library.playlists.detail',
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "playlists" */ '@/views/playlists/Detail'),
|
||||
|
@ -800,7 +800,7 @@ export default new Router({
|
|||
})
|
||||
},
|
||||
{
|
||||
path: 'tags/:id',
|
||||
path: '/tags/:id',
|
||||
name: 'library.tags.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -809,7 +809,7 @@ export default new Router({
|
|||
props: true
|
||||
},
|
||||
{
|
||||
path: 'artists/:id',
|
||||
path: '/artists/:id',
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "artists" */ '@/components/library/ArtistBase'
|
||||
|
@ -817,7 +817,7 @@ export default new Router({
|
|||
props: true,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
path: '/',
|
||||
name: 'library.artists.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -825,7 +825,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'edit',
|
||||
path: '/edit',
|
||||
name: 'library.artists.edit',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -833,7 +833,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'edit/:editId',
|
||||
path: '/edit/:editId',
|
||||
name: 'library.artists.edit.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -844,7 +844,7 @@ export default new Router({
|
|||
]
|
||||
},
|
||||
{
|
||||
path: 'albums/:id',
|
||||
path: '/albums/:id',
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "albums" */ '@/components/library/AlbumBase'
|
||||
|
@ -852,7 +852,7 @@ export default new Router({
|
|||
props: true,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
path: '/',
|
||||
name: 'library.albums.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -860,7 +860,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'edit',
|
||||
path: '/edit',
|
||||
name: 'library.albums.edit',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -868,7 +868,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'edit/:editId',
|
||||
path: '/edit/:editId',
|
||||
name: 'library.albums.edit.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -879,7 +879,7 @@ export default new Router({
|
|||
]
|
||||
},
|
||||
{
|
||||
path: 'tracks/:id',
|
||||
path: '/tracks/:id',
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "tracks" */ '@/components/library/TrackBase'
|
||||
|
@ -887,7 +887,7 @@ export default new Router({
|
|||
props: true,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
path: '/',
|
||||
name: 'library.tracks.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -895,7 +895,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'edit',
|
||||
path: '/edit',
|
||||
name: 'library.tracks.edit',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -903,7 +903,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'edit/:editId',
|
||||
path: '/edit/:editId',
|
||||
name: 'library.tracks.edit.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -914,7 +914,7 @@ export default new Router({
|
|||
]
|
||||
},
|
||||
{
|
||||
path: 'uploads/:id',
|
||||
path: '/uploads/:id',
|
||||
name: 'library.uploads.detail',
|
||||
props: true,
|
||||
component: () =>
|
||||
|
@ -924,7 +924,7 @@ export default new Router({
|
|||
},
|
||||
{
|
||||
// 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,
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -932,7 +932,7 @@ export default new Router({
|
|||
),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
path: '/',
|
||||
name: 'library.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -940,7 +940,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'albums',
|
||||
path: '/albums',
|
||||
name: 'library.detail.albums',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -948,7 +948,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'tracks',
|
||||
path: '/tracks',
|
||||
name: 'library.detail.tracks',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -956,7 +956,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'edit',
|
||||
path: '/edit',
|
||||
name: 'library.detail.edit',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -964,7 +964,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'upload',
|
||||
path: '/upload',
|
||||
name: 'library.detail.upload',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -995,7 +995,7 @@ export default new Router({
|
|||
),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
path: '/',
|
||||
name: 'channels.detail',
|
||||
component: () =>
|
||||
import(
|
||||
|
@ -1003,7 +1003,7 @@ export default new Router({
|
|||
)
|
||||
},
|
||||
{
|
||||
path: 'episodes',
|
||||
path: '/episodes',
|
||||
name: 'channels.detail.episodes',
|
||||
component: () =>
|
||||
import(
|
||||
|
|
|
@ -48,6 +48,7 @@ $bottom-player-height: 4rem;
|
|||
@import "./components/_track_widget.scss";
|
||||
@import "./components/_track_table.scss";
|
||||
@import "./components/_user_link.scss";
|
||||
@import "./components/user_modal.scss";
|
||||
@import "./components/_volume_control.scss";
|
||||
@import "./components/_loaders.scss";
|
||||
|
||||
|
|
|
@ -214,6 +214,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.ui.user-dropdown .ui.menu {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
.ui.user-dropdown>.text>.label {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue