Profile menu redesign
This commit is contained in:
parent
274bdd1d3e
commit
58df0d4529
|
@ -165,6 +165,16 @@ def discard_unused_icons(rule):
|
||||||
".wrench",
|
".wrench",
|
||||||
".x",
|
".x",
|
||||||
".key",
|
".key",
|
||||||
|
".cog",
|
||||||
|
".life.ring",
|
||||||
|
".language",
|
||||||
|
".palette",
|
||||||
|
".sun",
|
||||||
|
".moon",
|
||||||
|
".gitlab",
|
||||||
|
".chevron",
|
||||||
|
".right",
|
||||||
|
".left"
|
||||||
]
|
]
|
||||||
if ":before" not in rule["lines"][0]:
|
if ":before" not in rule["lines"][0]:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -1,35 +1,47 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app" :key="String($store.state.instance.instanceUrl)" :class="[$store.state.ui.queueFocused ? 'queue-focused' : '', {'has-bottom-player': $store.state.queue.tracks.length > 0}, `is-${ $store.getters['ui/windowSize']}`]">
|
<div
|
||||||
|
id="app"
|
||||||
|
:key="String($store.state.instance.instanceUrl)"
|
||||||
|
:class="[$store.state.ui.queueFocused ? 'queue-focused' : '', {'has-bottom-player': $store.state.queue.tracks.length > 0}, `is-${ $store.getters['ui/windowSize']}`]"
|
||||||
|
>
|
||||||
<!-- here, we display custom stylesheets, if any -->
|
<!-- here, we display custom stylesheets, if any -->
|
||||||
<link
|
<link
|
||||||
v-for="url in customStylesheets"
|
v-for="url in customStylesheets"
|
||||||
|
:key="url"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
property="stylesheet"
|
property="stylesheet"
|
||||||
:href="url"
|
:href="url"
|
||||||
:key="url"
|
|
||||||
>
|
>
|
||||||
<template>
|
<sidebar
|
||||||
<sidebar></sidebar>
|
:width="width"
|
||||||
<set-instance-modal @update:show="showSetInstanceModal = $event" :show="showSetInstanceModal"></set-instance-modal>
|
@show:set-instance-modal="showSetInstanceModal = !showSetInstanceModal"
|
||||||
<service-messages></service-messages>
|
@show:shortcuts-modal="showShortcutsModal = !showShortcutsModal"
|
||||||
<transition name="queue">
|
/>
|
||||||
<queue @touch-progress="$refs.player.setCurrentTime($event)" v-if="$store.state.ui.queueFocused"></queue>
|
<set-instance-modal
|
||||||
</transition>
|
:show="showSetInstanceModal"
|
||||||
<router-view role="main" :class="{hidden: $store.state.ui.queueFocused}"></router-view>
|
@update:show="showSetInstanceModal = $event"
|
||||||
<player ref="player"></player>
|
/>
|
||||||
<app-footer
|
<service-messages />
|
||||||
:class="{hidden: $store.state.ui.queueFocused}"
|
<transition name="queue">
|
||||||
:version="version"
|
<queue
|
||||||
@show:shortcuts-modal="showShortcutsModal = !showShortcutsModal"
|
v-if="$store.state.ui.queueFocused"
|
||||||
@show:set-instance-modal="showSetInstanceModal = !showSetInstanceModal"
|
@touch-progress="$refs.player.setCurrentTime($event)"
|
||||||
></app-footer>
|
/>
|
||||||
<playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
|
</transition>
|
||||||
<channel-upload-modal v-if="$store.state.auth.authenticated"></channel-upload-modal>
|
<router-view
|
||||||
<filter-modal v-if="$store.state.auth.authenticated"></filter-modal>
|
role="main"
|
||||||
<report-modal></report-modal>
|
:class="{hidden: $store.state.ui.queueFocused}"
|
||||||
<shortcuts-modal @update:show="showShortcutsModal = $event" :show="showShortcutsModal"></shortcuts-modal>
|
/>
|
||||||
<GlobalEvents @keydown.h.exact="showShortcutsModal = !showShortcutsModal"/>
|
<player ref="player" />
|
||||||
</template>
|
<playlist-modal v-if="$store.state.auth.authenticated" />
|
||||||
|
<channel-upload-modal v-if="$store.state.auth.authenticated" />
|
||||||
|
<filter-modal v-if="$store.state.auth.authenticated" />
|
||||||
|
<report-modal />
|
||||||
|
<shortcuts-modal
|
||||||
|
:show="showShortcutsModal"
|
||||||
|
@update:show="showShortcutsModal = $event"
|
||||||
|
/>
|
||||||
|
<GlobalEvents @keydown.h.exact="showShortcutsModal = !showShortcutsModal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -37,28 +49,26 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _ from '@/lodash'
|
import _ from '@/lodash'
|
||||||
import {mapState, mapGetters, mapActions} from 'vuex'
|
import { mapState, mapGetters } from 'vuex'
|
||||||
import { WebSocketBridge } from 'django-channels'
|
import { WebSocketBridge } from 'django-channels'
|
||||||
import GlobalEvents from '@/components/utils/global-events'
|
import GlobalEvents from '@/components/utils/global-events'
|
||||||
import moment from 'moment'
|
|
||||||
import locales from './locales'
|
import locales from './locales'
|
||||||
import {getClientOnlyRadio} from '@/radios'
|
import { getClientOnlyRadio } from '@/radios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
Player: () => import(/* webpackChunkName: "audio" */ "@/components/audio/Player"),
|
Player: () => import(/* webpackChunkName: "audio" */ '@/components/audio/Player'),
|
||||||
Queue: () => import(/* webpackChunkName: "audio" */ "@/components/Queue"),
|
Queue: () => import(/* webpackChunkName: "audio" */ '@/components/Queue'),
|
||||||
PlaylistModal: () => import(/* webpackChunkName: "auth-audio" */ "@/components/playlists/PlaylistModal"),
|
PlaylistModal: () => import(/* webpackChunkName: "auth-audio" */ '@/components/playlists/PlaylistModal'),
|
||||||
ChannelUploadModal: () => import(/* webpackChunkName: "auth-audio" */ "@/components/channels/UploadModal"),
|
ChannelUploadModal: () => import(/* webpackChunkName: "auth-audio" */ '@/components/channels/UploadModal'),
|
||||||
Sidebar: () => import(/* webpackChunkName: "core" */ "@/components/Sidebar"),
|
Sidebar: () => import(/* webpackChunkName: "core" */ '@/components/Sidebar'),
|
||||||
AppFooter: () => import(/* webpackChunkName: "core" */ "@/components/Footer"),
|
ServiceMessages: () => import(/* webpackChunkName: "core" */ '@/components/ServiceMessages'),
|
||||||
ServiceMessages: () => import(/* webpackChunkName: "core" */ "@/components/ServiceMessages"),
|
SetInstanceModal: () => import(/* webpackChunkName: "core" */ '@/components/SetInstanceModal'),
|
||||||
SetInstanceModal: () => import(/* webpackChunkName: "core" */ "@/components/SetInstanceModal"),
|
ShortcutsModal: () => import(/* webpackChunkName: "core" */ '@/components/ShortcutsModal'),
|
||||||
ShortcutsModal: () => import(/* webpackChunkName: "core" */ "@/components/ShortcutsModal"),
|
FilterModal: () => import(/* webpackChunkName: "moderation" */ '@/components/moderation/FilterModal'),
|
||||||
FilterModal: () => import(/* webpackChunkName: "moderation" */ "@/components/moderation/FilterModal"),
|
ReportModal: () => import(/* webpackChunkName: "moderation" */ '@/components/moderation/ReportModal'),
|
||||||
ReportModal: () => import(/* webpackChunkName: "moderation" */ "@/components/moderation/ReportModal"),
|
GlobalEvents
|
||||||
GlobalEvents,
|
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -70,23 +80,168 @@ export default {
|
||||||
width: window.innerWidth
|
width: window.innerWidth
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
messages: state => state.ui.messages,
|
||||||
|
nodeinfo: state => state.instance.nodeinfo,
|
||||||
|
playing: state => state.player.playing,
|
||||||
|
bufferProgress: state => state.player.bufferProgress,
|
||||||
|
isLoadingAudio: state => state.player.isLoadingAudio,
|
||||||
|
serviceWorker: state => state.ui.serviceWorker
|
||||||
|
}),
|
||||||
|
...mapGetters({
|
||||||
|
hasNext: 'queue/hasNext',
|
||||||
|
currentTrack: 'queue/currentTrack',
|
||||||
|
progress: 'player/progress'
|
||||||
|
}),
|
||||||
|
labels () {
|
||||||
|
const play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Play track')
|
||||||
|
const pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Pause track')
|
||||||
|
const next = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Next track')
|
||||||
|
const expandQueue = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Expand queue')
|
||||||
|
return {
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
next,
|
||||||
|
expandQueue
|
||||||
|
}
|
||||||
|
},
|
||||||
|
suggestedInstances () {
|
||||||
|
const instances = this.$store.state.instance.knownInstances.slice(0)
|
||||||
|
if (this.$store.state.instance.frontSettings.defaultServerUrl) {
|
||||||
|
let serverUrl = this.$store.state.instance.frontSettings.defaultServerUrl
|
||||||
|
if (!serverUrl.endsWith('/')) {
|
||||||
|
serverUrl = serverUrl + '/'
|
||||||
|
}
|
||||||
|
instances.push(serverUrl)
|
||||||
|
}
|
||||||
|
instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/')
|
||||||
|
return _.uniq(instances.filter((e) => { return e }))
|
||||||
|
},
|
||||||
|
version () {
|
||||||
|
if (!this.nodeinfo) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return _.get(this.nodeinfo, 'software.version')
|
||||||
|
},
|
||||||
|
customStylesheets () {
|
||||||
|
if (this.$store.state.instance.frontSettings) {
|
||||||
|
return this.$store.state.instance.frontSettings.additionalStylesheets || []
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$store.state.instance.instanceUrl' (v) {
|
||||||
|
this.$store.dispatch('instance/fetchSettings')
|
||||||
|
this.fetchNodeInfo()
|
||||||
|
},
|
||||||
|
'$store.state.ui.theme': {
|
||||||
|
immediate: true,
|
||||||
|
handler (newValue, oldValue) {
|
||||||
|
const oldTheme = oldValue || 'light'
|
||||||
|
document.body.classList.remove(`theme-${oldTheme}`)
|
||||||
|
document.body.classList.add(`theme-${newValue}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'$store.state.auth.authenticated' (newValue) {
|
||||||
|
if (!newValue) {
|
||||||
|
this.disconnect()
|
||||||
|
} else {
|
||||||
|
this.openWebsocket()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'$store.state.ui.currentLanguage': {
|
||||||
|
immediate: true,
|
||||||
|
handler (newValue) {
|
||||||
|
const self = this
|
||||||
|
const htmlLocale = newValue.toLowerCase().replace('_', '-')
|
||||||
|
document.documentElement.setAttribute('lang', htmlLocale)
|
||||||
|
if (newValue === 'en_US') {
|
||||||
|
self.$language.current = 'noop'
|
||||||
|
self.$language.current = newValue
|
||||||
|
return self.$store.commit('ui/momentLocale', 'en')
|
||||||
|
}
|
||||||
|
import(/* webpackChunkName: "locale-[request]" */ `./translations/${newValue}.json`).then((response) => {
|
||||||
|
Vue.$translations[newValue] = response.default[newValue]
|
||||||
|
}).finally(() => {
|
||||||
|
// set current language twice, otherwise we seem to have a cache somewhere
|
||||||
|
// and rendering does not happen
|
||||||
|
self.$language.current = 'noop'
|
||||||
|
self.$language.current = newValue
|
||||||
|
})
|
||||||
|
const momentLocale = newValue.replace('_', '-').toLowerCase()
|
||||||
|
import(/* webpackChunkName: "moment-locale-[request]" */ `moment/locale/${momentLocale}.js`).then(() => {
|
||||||
|
self.$store.commit('ui/momentLocale', momentLocale)
|
||||||
|
}).catch(() => {
|
||||||
|
console.log('No momentjs locale available for', momentLocale)
|
||||||
|
const shortLocale = momentLocale.split('-')[0]
|
||||||
|
import(/* webpackChunkName: "moment-locale-[request]" */ `moment/locale/${shortLocale}.js`).then(() => {
|
||||||
|
self.$store.commit('ui/momentLocale', shortLocale)
|
||||||
|
}).catch(() => {
|
||||||
|
console.log('No momentjs locale available for', shortLocale)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
currentTrack: {
|
||||||
|
immediate: true,
|
||||||
|
handler (newValue) {
|
||||||
|
this.updateDocumentTitle()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'$store.state.ui.pageTitle': {
|
||||||
|
immediate: true,
|
||||||
|
handler (newValue) {
|
||||||
|
this.updateDocumentTitle()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'serviceWorker.updateAvailable': {
|
||||||
|
handler (v) {
|
||||||
|
if (!v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const self = this
|
||||||
|
this.$store.commit('ui/addMessage', {
|
||||||
|
content: this.$pgettext('App/Message/Paragraph', 'A new version of the app is available.'),
|
||||||
|
date: new Date(),
|
||||||
|
key: 'refreshApp',
|
||||||
|
displayTime: 0,
|
||||||
|
classActions: 'bottom attached opaque',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text: this.$pgettext('App/Message/Paragraph', 'Update'),
|
||||||
|
class: 'primary',
|
||||||
|
click: function () {
|
||||||
|
self.updateApp()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$pgettext('App/Message/Paragraph', 'Later'),
|
||||||
|
class: 'basic'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
async created () {
|
async created () {
|
||||||
|
|
||||||
if (navigator.serviceWorker) {
|
if (navigator.serviceWorker) {
|
||||||
navigator.serviceWorker.addEventListener(
|
navigator.serviceWorker.addEventListener(
|
||||||
'controllerchange', () => {
|
'controllerchange', () => {
|
||||||
if (this.serviceWorker.refreshing) return;
|
if (this.serviceWorker.refreshing) return
|
||||||
this.$store.commit('ui/serviceWorker', {
|
this.$store.commit('ui/serviceWorker', {
|
||||||
refreshing: true
|
refreshing: true
|
||||||
})
|
})
|
||||||
window.location.reload();
|
window.location.reload()
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', this.handleResize);
|
window.addEventListener('resize', this.handleResize)
|
||||||
this.handleResize();
|
this.handleResize()
|
||||||
this.openWebsocket()
|
this.openWebsocket()
|
||||||
let self = this
|
const self = this
|
||||||
if (!this.$store.state.ui.selectedLanguage) {
|
if (!this.$store.state.ui.selectedLanguage) {
|
||||||
this.autodetectLanguage()
|
this.autodetectLanguage()
|
||||||
}
|
}
|
||||||
|
@ -94,7 +249,7 @@ export default {
|
||||||
// used to redraw ago dates every minute
|
// used to redraw ago dates every minute
|
||||||
self.$store.commit('ui/computeLastDate')
|
self.$store.commit('ui/computeLastDate')
|
||||||
}, 1000 * 60)
|
}, 1000 * 60)
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const serverUrl = urlParams.get('_server')
|
const serverUrl = urlParams.get('_server')
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
this.$store.commit('instance/instanceUrl', serverUrl)
|
this.$store.commit('instance/instanceUrl', serverUrl)
|
||||||
|
@ -102,13 +257,12 @@ export default {
|
||||||
const url = urlParams.get('_url')
|
const url = urlParams.get('_url')
|
||||||
if (url) {
|
if (url) {
|
||||||
this.$router.replace(url)
|
this.$router.replace(url)
|
||||||
}
|
} else if (!this.$store.state.instance.instanceUrl) {
|
||||||
else if (!this.$store.state.instance.instanceUrl) {
|
|
||||||
// we have several way to guess the API server url. By order of precedence:
|
// we have several way to guess the API server url. By order of precedence:
|
||||||
// 1. use the url provided in settings.json, if any
|
// 1. use the url provided in settings.json, if any
|
||||||
// 2. use the url specified when building via VUE_APP_INSTANCE_URL
|
// 2. use the url specified when building via VUE_APP_INSTANCE_URL
|
||||||
// 3. use the current url
|
// 3. use the current url
|
||||||
let defaultInstanceUrl = this.$store.state.instance.frontSettings.defaultServerUrl || process.env.VUE_APP_INSTANCE_URL || this.$store.getters['instance/defaultUrl']()
|
const defaultInstanceUrl = this.$store.state.instance.frontSettings.defaultServerUrl || process.env.VUE_APP_INSTANCE_URL || this.$store.getters['instance/defaultUrl']()
|
||||||
this.$store.commit('instance/instanceUrl', defaultInstanceUrl)
|
this.$store.commit('instance/instanceUrl', defaultInstanceUrl)
|
||||||
} else {
|
} else {
|
||||||
// needed to trigger initialization of axios / service worker
|
// needed to trigger initialization of axios / service worker
|
||||||
|
@ -153,80 +307,78 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
let self = this
|
const self = this
|
||||||
// slight hack to allow use to have internal links in <translate> tags
|
// slight hack to allow use to have internal links in <translate> tags
|
||||||
// while preserving router behaviour
|
// while preserving router behaviour
|
||||||
document.documentElement.addEventListener('click', function (event) {
|
document.documentElement.addEventListener('click', function (event) {
|
||||||
if (!event.target.matches('a.internal')) return;
|
if (!event.target.matches('a.internal')) return
|
||||||
self.$router.push(event.target.getAttribute('href'))
|
self.$router.push(event.target.getAttribute('href'))
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
}, false);
|
}, false)
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
document.getElementById('fake-content').classList.add('loaded')
|
document.getElementById('fake-content').classList.add('loaded')
|
||||||
})
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||||
eventName: 'inbox.item_added',
|
eventName: 'inbox.item_added',
|
||||||
id: 'sidebarCount',
|
id: 'sidebarCount'
|
||||||
})
|
})
|
||||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||||
eventName: 'mutation.created',
|
eventName: 'mutation.created',
|
||||||
id: 'sidebarReviewEditCount',
|
id: 'sidebarReviewEditCount'
|
||||||
})
|
})
|
||||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||||
eventName: 'mutation.updated',
|
eventName: 'mutation.updated',
|
||||||
id: 'sidebarReviewEditCount',
|
id: 'sidebarReviewEditCount'
|
||||||
})
|
})
|
||||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||||
eventName: 'mutation.updated',
|
eventName: 'mutation.updated',
|
||||||
id: 'sidebarPendingReviewReportCount',
|
id: 'sidebarPendingReviewReportCount'
|
||||||
})
|
})
|
||||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||||
eventName: 'user_request.created',
|
eventName: 'user_request.created',
|
||||||
id: 'sidebarPendingReviewRequestCount',
|
id: 'sidebarPendingReviewRequestCount'
|
||||||
})
|
})
|
||||||
this.$store.commit('ui/removeWebsocketEventHandler', {
|
this.$store.commit('ui/removeWebsocketEventHandler', {
|
||||||
eventName: 'Listen',
|
eventName: 'Listen',
|
||||||
id: 'handleListen',
|
id: 'handleListen'
|
||||||
})
|
})
|
||||||
this.disconnect()
|
this.disconnect()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
incrementNotificationCountInSidebar (event) {
|
incrementNotificationCountInSidebar (event) {
|
||||||
this.$store.commit('ui/incrementNotifications', {type: 'inbox', count: 1})
|
this.$store.commit('ui/incrementNotifications', { type: 'inbox', count: 1 })
|
||||||
},
|
},
|
||||||
incrementReviewEditCountInSidebar (event) {
|
incrementReviewEditCountInSidebar (event) {
|
||||||
this.$store.commit('ui/incrementNotifications', {type: 'pendingReviewEdits', value: event.pending_review_count})
|
this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewEdits', value: event.pending_review_count })
|
||||||
},
|
},
|
||||||
incrementPendingReviewReportsCountInSidebar (event) {
|
incrementPendingReviewReportsCountInSidebar (event) {
|
||||||
this.$store.commit('ui/incrementNotifications', {type: 'pendingReviewReports', value: event.unresolved_count})
|
this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewReports', value: event.unresolved_count })
|
||||||
},
|
},
|
||||||
incrementPendingReviewRequestsCountInSidebar (event) {
|
incrementPendingReviewRequestsCountInSidebar (event) {
|
||||||
this.$store.commit('ui/incrementNotifications', {type: 'pendingReviewRequests', value: event.pending_count})
|
this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewRequests', value: event.pending_count })
|
||||||
},
|
},
|
||||||
handleListen (event) {
|
handleListen (event) {
|
||||||
if (this.$store.state.radios.current && this.$store.state.radios.running) {
|
if (this.$store.state.radios.current && this.$store.state.radios.running) {
|
||||||
let current = this.$store.state.radios.current
|
const current = this.$store.state.radios.current
|
||||||
if (current.clientOnly && current.type === 'account') {
|
if (current.clientOnly && current.type === 'account') {
|
||||||
getClientOnlyRadio(current).handleListen(current, event, this.$store)
|
getClientOnlyRadio(current).handleListen(current, event, this.$store)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async fetchNodeInfo () {
|
async fetchNodeInfo () {
|
||||||
let response = await axios.get('instance/nodeinfo/2.0/')
|
const response = await axios.get('instance/nodeinfo/2.0/')
|
||||||
this.$store.commit('instance/nodeinfo', response.data)
|
this.$store.commit('instance/nodeinfo', response.data)
|
||||||
},
|
},
|
||||||
autodetectLanguage () {
|
autodetectLanguage () {
|
||||||
let userLanguage = navigator.language || navigator.userLanguage
|
const userLanguage = navigator.language || navigator.userLanguage
|
||||||
let available = locales.locales.map(e => { return e.code })
|
const available = locales.locales.map(e => { return e.code })
|
||||||
let self = this
|
|
||||||
let candidate
|
let candidate
|
||||||
let matching = available.filter((a) => {
|
const matching = available.filter((a) => {
|
||||||
return userLanguage.replace('-', '_') === a
|
return userLanguage.replace('-', '_') === a
|
||||||
})
|
})
|
||||||
let almostMatching = available.filter((a) => {
|
const almostMatching = available.filter((a) => {
|
||||||
return userLanguage.replace('-', '_').split('_')[0] === a.split('_')[0]
|
return userLanguage.replace('-', '_').split('_')[0] === a.split('_')[0]
|
||||||
})
|
})
|
||||||
if (matching.length > 0) {
|
if (matching.length > 0) {
|
||||||
|
@ -242,15 +394,15 @@ export default {
|
||||||
if (!this.bridge) {
|
if (!this.bridge) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.bridge.socket.close(1000, 'goodbye', {keepClosed: true})
|
this.bridge.socket.close(1000, 'goodbye', { keepClosed: true })
|
||||||
},
|
},
|
||||||
openWebsocket () {
|
openWebsocket () {
|
||||||
if (!this.$store.state.auth.authenticated) {
|
if (!this.$store.state.auth.authenticated) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.disconnect()
|
this.disconnect()
|
||||||
let self = this
|
const self = this
|
||||||
let token = this.$store.state.auth.token
|
const token = this.$store.state.auth.token
|
||||||
// let token = 'test'
|
// let token = 'test'
|
||||||
const bridge = new WebSocketBridge()
|
const bridge = new WebSocketBridge()
|
||||||
this.bridge = bridge
|
this.bridge = bridge
|
||||||
|
@ -260,7 +412,7 @@ export default {
|
||||||
bridge.connect(
|
bridge.connect(
|
||||||
url,
|
url,
|
||||||
[],
|
[],
|
||||||
{reconnectInterval: 1000 * 60})
|
{ reconnectInterval: 1000 * 60 })
|
||||||
bridge.listen(function (event) {
|
bridge.listen(function (event) {
|
||||||
self.$store.dispatch('ui/websocketEvent', event)
|
self.$store.dispatch('ui/websocketEvent', event)
|
||||||
})
|
})
|
||||||
|
@ -268,7 +420,7 @@ export default {
|
||||||
console.log('Connected to WebSocket')
|
console.log('Connected to WebSocket')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getTrackInformationText(track) {
|
getTrackInformationText (track) {
|
||||||
const trackTitle = track.title
|
const trackTitle = track.title
|
||||||
const albumArtist = (track.album) ? track.album.artist.name : null
|
const albumArtist = (track.album) ? track.album.artist.name : null
|
||||||
const artistName = (
|
const artistName = (
|
||||||
|
@ -276,11 +428,12 @@ export default {
|
||||||
const text = `♫ ${trackTitle} – ${artistName} ♫`
|
const text = `♫ ${trackTitle} – ${artistName} ♫`
|
||||||
return text
|
return text
|
||||||
},
|
},
|
||||||
updateDocumentTitle() {
|
updateDocumentTitle () {
|
||||||
let parts = []
|
const parts = []
|
||||||
const currentTrackPart = (
|
const currentTrackPart = (
|
||||||
(this.currentTrack) ? this.getTrackInformationText(this.currentTrack)
|
(this.currentTrack)
|
||||||
: null)
|
? this.getTrackInformationText(this.currentTrack)
|
||||||
|
: null)
|
||||||
if (currentTrackPart) {
|
if (currentTrackPart) {
|
||||||
parts.push(currentTrackPart)
|
parts.push(currentTrackPart)
|
||||||
}
|
}
|
||||||
|
@ -292,158 +445,13 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
updateApp () {
|
updateApp () {
|
||||||
this.$store.commit('ui/serviceWorker', {updateAvailable: false})
|
this.$store.commit('ui/serviceWorker', { updateAvailable: false })
|
||||||
if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return; }
|
if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return }
|
||||||
this.serviceWorker.registration.waiting.postMessage({command: 'skipWaiting'})
|
this.serviceWorker.registration.waiting.postMessage({ command: 'skipWaiting' })
|
||||||
},
|
},
|
||||||
handleResize() {
|
handleResize () {
|
||||||
this.width = window.innerWidth
|
this.width = window.innerWidth
|
||||||
}
|
}
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState({
|
|
||||||
messages: state => state.ui.messages,
|
|
||||||
nodeinfo: state => state.instance.nodeinfo,
|
|
||||||
playing: state => state.player.playing,
|
|
||||||
bufferProgress: state => state.player.bufferProgress,
|
|
||||||
isLoadingAudio: state => state.player.isLoadingAudio,
|
|
||||||
serviceWorker: state => state.ui.serviceWorker,
|
|
||||||
}),
|
|
||||||
...mapGetters({
|
|
||||||
hasNext: "queue/hasNext",
|
|
||||||
currentTrack: 'queue/currentTrack',
|
|
||||||
progress: "player/progress",
|
|
||||||
}),
|
|
||||||
labels() {
|
|
||||||
let play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Play track")
|
|
||||||
let pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Pause track")
|
|
||||||
let next = this.$pgettext('Sidebar/Player/Icon.Tooltip', "Next track")
|
|
||||||
let expandQueue = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Expand queue")
|
|
||||||
return {
|
|
||||||
play,
|
|
||||||
pause,
|
|
||||||
next,
|
|
||||||
expandQueue,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
suggestedInstances () {
|
|
||||||
let instances = this.$store.state.instance.knownInstances.slice(0)
|
|
||||||
if (this.$store.state.instance.frontSettings.defaultServerUrl) {
|
|
||||||
let serverUrl = this.$store.state.instance.frontSettings.defaultServerUrl
|
|
||||||
if (!serverUrl.endsWith('/')) {
|
|
||||||
serverUrl = serverUrl + '/'
|
|
||||||
}
|
|
||||||
instances.push(serverUrl)
|
|
||||||
}
|
|
||||||
instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/')
|
|
||||||
return _.uniq(instances.filter((e) => {return e}))
|
|
||||||
},
|
|
||||||
version () {
|
|
||||||
if (!this.nodeinfo) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return _.get(this.nodeinfo, 'software.version')
|
|
||||||
},
|
|
||||||
customStylesheets () {
|
|
||||||
if (this.$store.state.instance.frontSettings) {
|
|
||||||
return this.$store.state.instance.frontSettings.additionalStylesheets || []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'$store.state.instance.instanceUrl' (v) {
|
|
||||||
this.$store.dispatch('instance/fetchSettings')
|
|
||||||
this.fetchNodeInfo()
|
|
||||||
},
|
|
||||||
'$store.state.ui.theme': {
|
|
||||||
immediate: true,
|
|
||||||
handler (newValue, oldValue) {
|
|
||||||
let oldTheme = oldValue || 'light'
|
|
||||||
document.body.classList.remove(`theme-${oldTheme}`)
|
|
||||||
document.body.classList.add(`theme-${newValue}`)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'$store.state.auth.authenticated' (newValue) {
|
|
||||||
if (!newValue) {
|
|
||||||
this.disconnect()
|
|
||||||
} else {
|
|
||||||
this.openWebsocket()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'$store.state.ui.currentLanguage': {
|
|
||||||
immediate: true,
|
|
||||||
handler(newValue) {
|
|
||||||
let self = this
|
|
||||||
let htmlLocale = newValue.toLowerCase().replace('_', '-')
|
|
||||||
document.documentElement.setAttribute('lang', htmlLocale);
|
|
||||||
if (newValue === 'en_US') {
|
|
||||||
self.$language.current = 'noop'
|
|
||||||
self.$language.current = newValue
|
|
||||||
return self.$store.commit('ui/momentLocale', 'en')
|
|
||||||
}
|
|
||||||
import(/* webpackChunkName: "locale-[request]" */ `./translations/${newValue}.json`).then((response) =>{
|
|
||||||
Vue.$translations[newValue] = response.default[newValue]
|
|
||||||
}).finally(() => {
|
|
||||||
// set current language twice, otherwise we seem to have a cache somewhere
|
|
||||||
// and rendering does not happen
|
|
||||||
self.$language.current = 'noop'
|
|
||||||
self.$language.current = newValue
|
|
||||||
})
|
|
||||||
let momentLocale = newValue.replace('_', '-').toLowerCase()
|
|
||||||
import(/* webpackChunkName: "moment-locale-[request]" */ `moment/locale/${momentLocale}.js`).then(() => {
|
|
||||||
self.$store.commit('ui/momentLocale', momentLocale)
|
|
||||||
}).catch(() => {
|
|
||||||
console.log('No momentjs locale available for', momentLocale)
|
|
||||||
let shortLocale = momentLocale.split('-')[0]
|
|
||||||
import(/* webpackChunkName: "moment-locale-[request]" */ `moment/locale/${shortLocale}.js`).then(() => {
|
|
||||||
self.$store.commit('ui/momentLocale', shortLocale)
|
|
||||||
}).catch(() => {
|
|
||||||
console.log('No momentjs locale available for', shortLocale)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'currentTrack': {
|
|
||||||
immediate: true,
|
|
||||||
handler(newValue) {
|
|
||||||
this.updateDocumentTitle()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'$store.state.ui.pageTitle': {
|
|
||||||
immediate: true,
|
|
||||||
handler(newValue) {
|
|
||||||
this.updateDocumentTitle()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'serviceWorker.updateAvailable': {
|
|
||||||
handler (v) {
|
|
||||||
if (!v) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let self = this
|
|
||||||
this.$store.commit('ui/addMessage', {
|
|
||||||
content: this.$pgettext("App/Message/Paragraph", "A new version of the app is available."),
|
|
||||||
date: new Date(),
|
|
||||||
key: 'refreshApp',
|
|
||||||
displayTime: 0,
|
|
||||||
classActions: 'bottom attached opaque',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
text: this.$pgettext("App/Message/Paragraph", "Update"),
|
|
||||||
class: "primary",
|
|
||||||
click: function () {
|
|
||||||
self.updateApp()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: this.$pgettext("App/Message/Paragraph", "Later"),
|
|
||||||
class: "basic",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
immediate: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,252 +1,575 @@
|
||||||
<template>
|
<template>
|
||||||
<aside :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar', 'component-sidebar']">
|
<aside :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar', 'component-sidebar']">
|
||||||
<header class="ui basic segment header-wrapper">
|
<header class="ui basic segment header-wrapper">
|
||||||
<router-link :title="'Funkwhale'" :to="{name: logoUrl}">
|
<router-link
|
||||||
<i class="logo bordered inverted vibrant big icon">
|
:title="'Funkwhale'"
|
||||||
<logo class="logo"></logo>
|
:to="{name: logoUrl}"
|
||||||
<span class="visually-hidden">Home</span>
|
>
|
||||||
</i>
|
<i class="logo bordered inverted vibrant big icon">
|
||||||
</router-link>
|
<logo class="logo" />
|
||||||
<router-link v-if="!$store.state.auth.authenticated" class="logo-wrapper" :to="{name: logoUrl}" :title="'Funkwhale'">
|
<span class="visually-hidden">Home</span>
|
||||||
<img src="../assets/logo/text-white.svg" alt="" />
|
</i>
|
||||||
</router-link>
|
</router-link>
|
||||||
<nav class="top ui compact right aligned inverted text menu">
|
<nav class="top ui compact right aligned inverted text menu">
|
||||||
<template v-if="$store.state.auth.authenticated">
|
|
||||||
|
|
||||||
<div class="right menu">
|
<div class="right menu">
|
||||||
<div class="item" :title="labels.administration" v-if="$store.state.auth.availablePermissions['settings'] || $store.state.auth.availablePermissions['moderation']">
|
<div
|
||||||
|
v-if="$store.state.auth.availablePermissions['settings'] || $store.state.auth.availablePermissions['moderation']"
|
||||||
|
class="item"
|
||||||
|
:title="labels.administration"
|
||||||
|
>
|
||||||
<div class="item ui inline admin-dropdown dropdown">
|
<div class="item ui inline admin-dropdown dropdown">
|
||||||
<i class="wrench icon"></i>
|
<i class="wrench icon" />
|
||||||
<div
|
<div
|
||||||
v-if="moderationNotifications > 0"
|
v-if="moderationNotifications > 0"
|
||||||
:class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']">{{ moderationNotifications }}</div>
|
:class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']"
|
||||||
|
>
|
||||||
|
{{ moderationNotifications }}
|
||||||
|
</div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<h3 class="header">
|
<h3 class="header">
|
||||||
<translate translate-context="Sidebar/Admin/Title/Noun">Administration</translate>
|
<translate translate-context="Sidebar/Admin/Title/Noun">
|
||||||
|
Administration
|
||||||
|
</translate>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="divider"></div>
|
<div class="divider" />
|
||||||
<router-link
|
<router-link
|
||||||
v-if="$store.state.auth.availablePermissions['library']"
|
v-if="$store.state.auth.availablePermissions['library']"
|
||||||
class="item"
|
class="item"
|
||||||
:to="{name: 'manage.library.edits', query: {q: 'is_approved:null'}}">
|
:to="{name: 'manage.library.edits', query: {q: 'is_approved:null'}}"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="$store.state.ui.notifications.pendingReviewEdits > 0"
|
v-if="$store.state.ui.notifications.pendingReviewEdits > 0"
|
||||||
:title="labels.pendingReviewEdits"
|
:title="labels.pendingReviewEdits"
|
||||||
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']">
|
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']"
|
||||||
{{ $store.state.ui.notifications.pendingReviewEdits }}</div>
|
>
|
||||||
<translate translate-context="*/*/*/Noun">Library</translate>
|
{{ $store.state.ui.notifications.pendingReviewEdits }}
|
||||||
|
</div>
|
||||||
|
<translate translate-context="*/*/*/Noun">
|
||||||
|
Library
|
||||||
|
</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="$store.state.auth.availablePermissions['moderation']"
|
v-if="$store.state.auth.availablePermissions['moderation']"
|
||||||
class="item"
|
class="item"
|
||||||
:to="{name: 'manage.moderation.reports.list', query: {q: 'resolved:no'}}">
|
:to="{name: 'manage.moderation.reports.list', query: {q: 'resolved:no'}}"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="$store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests> 0"
|
v-if="$store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests> 0"
|
||||||
:title="labels.pendingReviewReports"
|
:title="labels.pendingReviewReports"
|
||||||
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']">{{ $store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests }}</div>
|
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']"
|
||||||
<translate translate-context="*/Moderation/*">Moderation</translate>
|
>
|
||||||
|
{{ $store.state.ui.notifications.pendingReviewReports + $store.state.ui.notifications.pendingReviewRequests }}
|
||||||
|
</div>
|
||||||
|
<translate translate-context="*/Moderation/*">
|
||||||
|
Moderation
|
||||||
|
</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="$store.state.auth.availablePermissions['settings']"
|
v-if="$store.state.auth.availablePermissions['settings']"
|
||||||
class="item"
|
class="item"
|
||||||
:to="{name: 'manage.users.users.list'}">
|
:to="{name: 'manage.users.users.list'}"
|
||||||
<translate translate-context="*/*/*/Noun">Users</translate>
|
>
|
||||||
|
<translate translate-context="*/*/*/Noun">
|
||||||
|
Users
|
||||||
|
</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="$store.state.auth.availablePermissions['settings']"
|
v-if="$store.state.auth.availablePermissions['settings']"
|
||||||
class="item"
|
class="item"
|
||||||
:to="{path: '/manage/settings'}">
|
:to="{path: '/manage/settings'}"
|
||||||
<translate translate-context="*/*/*/Noun">Settings</translate>
|
>
|
||||||
|
<translate translate-context="*/*/*/Noun">
|
||||||
|
Settings
|
||||||
|
</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
class="item"
|
|
||||||
v-if="$store.state.auth.authenticated"
|
v-if="$store.state.auth.authenticated"
|
||||||
:to="{name: 'content.index'}">
|
class="item"
|
||||||
<i class="upload icon"></i>
|
:to="{name: 'content.index'}"
|
||||||
<span class="visually-hidden">{{ labels.addContent }}</span>
|
>
|
||||||
|
<i class="upload icon" />
|
||||||
|
<span class="visually-hidden">{{ labels.addContent }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'notifications'}">
|
<template v-if="width > 768">
|
||||||
<i class="bell icon"></i>
|
<div class="item">
|
||||||
<div v-if="$store.state.ui.notifications.inbox + additionalNotifications > 0" :class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']">
|
<div class="ui user-dropdown dropdown">
|
||||||
{{ $store.state.ui.notifications.inbox + additionalNotifications }}
|
<img
|
||||||
</div>
|
v-if="$store.state.auth.authenticated && $store.state.auth.profile.avatar && $store.state.auth.profile.avatar.urls.medium_square_crop"
|
||||||
<span v-else class="visually-hidden">{{ labels.notifications }}</span>
|
class="ui avatar image"
|
||||||
</router-link>
|
alt=""
|
||||||
<div class="item">
|
:src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.urls.medium_square_crop)"
|
||||||
<div class="ui user-dropdown dropdown" >
|
>
|
||||||
<img class="ui avatar image" alt="" v-if="$store.state.auth.profile.avatar && $store.state.auth.profile.avatar.urls.medium_square_crop" :src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.urls.medium_square_crop)" />
|
<actor-avatar
|
||||||
<actor-avatar v-else :actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username}" />
|
v-else-if="$store.state.auth.authenticated"
|
||||||
<div class="menu">
|
:actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username,}"
|
||||||
<router-link class="item" :to="{name: 'profile.overview', params: {username: $store.state.auth.username}}"><translate translate-context="*/*/*/Noun">Profile</translate></router-link>
|
/>
|
||||||
<router-link class="item" :to="{path: '/settings'}"><translate translate-context="*/*/*/Noun">Settings</translate></router-link>
|
<i
|
||||||
<router-link class="item" :to="{name: 'logout'}"><translate translate-context="Sidebar/Login/List item.Link/Verb">Logout</translate></router-link>
|
v-else
|
||||||
|
class="cog icon"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="$store.state.ui.notifications.inbox + additionalNotifications > 0"
|
||||||
|
:class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']"
|
||||||
|
>
|
||||||
|
{{ $store.state.ui.notifications.inbox + additionalNotifications }}
|
||||||
|
</div>
|
||||||
|
<user-menu
|
||||||
|
:width="width"
|
||||||
|
v-on="$listeners"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
<template v-else>
|
||||||
<div class="item collapse-button-wrapper">
|
<a
|
||||||
|
href=""
|
||||||
<button
|
class="item"
|
||||||
@click="isCollapsed = !isCollapsed"
|
@click.prevent.exact="showUserModal = !showUserModal"
|
||||||
:class="['ui', 'basic', 'big', {'vibrant': !isCollapsed}, 'inverted icon', 'collapse', 'button']">
|
>
|
||||||
<i class="sidebar icon"></i></button>
|
<img
|
||||||
</div>
|
v-if="$store.state.auth.authenticated && $store.state.auth.profile.avatar && $store.state.auth.profile.avatar.urls.medium_square_crop"
|
||||||
</nav>
|
class="ui avatar image"
|
||||||
</header>
|
alt=""
|
||||||
<div class="ui basic search-wrapper segment">
|
:src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.urls.medium_square_crop)"
|
||||||
<search-bar @search="isCollapsed = false"></search-bar>
|
>
|
||||||
</div>
|
<actor-avatar
|
||||||
<div v-if="!$store.state.auth.authenticated" class="ui basic signup segment">
|
v-else-if="$store.state.auth.authenticated"
|
||||||
<router-link class="ui fluid tiny primary button" :to="{name: 'login'}"><translate translate-context="*/Login/*/Verb">Login</translate></router-link>
|
:actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username,}"
|
||||||
<div class="ui small hidden divider"></div>
|
/>
|
||||||
<router-link class="ui fluid tiny button" :to="{path: '/signup'}">
|
<i
|
||||||
<translate translate-context="*/Signup/Link/Verb">Create an account</translate>
|
v-else
|
||||||
</router-link>
|
class="cog icon"
|
||||||
</div>
|
/>
|
||||||
<nav class="secondary" role="navigation" aria-labelledby="navigation-label">
|
<div
|
||||||
<h1 id="navigation-label" class="visually-hidden">
|
v-if="$store.state.ui.notifications.inbox + additionalNotifications > 0"
|
||||||
<translate translate-context="*/*/*">Main navigation</translate>
|
:class="['ui', 'accent', 'mini', 'bottom floating', 'circular', 'label']"
|
||||||
</h1>
|
>
|
||||||
<div class="ui small hidden divider"></div>
|
{{ $store.state.ui.notifications.inbox + additionalNotifications }}
|
||||||
<section :class="['ui', 'bottom', 'attached', {active: selectedTab === 'library'}, 'tab']" :aria-label="labels.mainMenu">
|
</div>
|
||||||
<nav class="ui vertical large fluid inverted menu" role="navigation" :aria-label="labels.mainMenu">
|
</a>
|
||||||
<div :class="[{collapsed: !exploreExpanded}, 'collapsible item']">
|
</template>
|
||||||
<h2 class="header" role="button" @click="exploreExpanded = true" tabindex="0" @focus="exploreExpanded = true">
|
<user-modal
|
||||||
<translate translate-context="*/*/*/Verb">Explore</translate>
|
:show="showUserModal"
|
||||||
<i class="angle right icon" v-if="!exploreExpanded"></i>
|
@showThemeModalEvent="showThemeModal=true"
|
||||||
</h2>
|
@showLanguageModalEvent="showLanguageModal=true"
|
||||||
<div class="menu">
|
@update:show="showUserModal = $event"
|
||||||
<router-link class="item" :to="{name: 'search'}"><i class="search icon"></i><translate translate-context="Sidebar/Navigation/List item.Link/Verb">Search</translate></router-link>
|
/>
|
||||||
<router-link class="item" :exact="true" :to="{name: 'library.index'}"><i class="music icon"></i><translate translate-context="Sidebar/Navigation/List item.Link/Verb">Browse</translate></router-link>
|
<modal
|
||||||
<router-link class="item" :to="{name: 'library.podcasts.browse'}"><i class="podcast icon"></i><translate translate-context="*/*/*">Podcasts</translate></router-link>
|
ref="languageModal"
|
||||||
<router-link class="item" :to="{name: 'library.albums.browse'}"><i class="compact disc icon"></i><translate translate-context="*/*/*">Albums</translate></router-link>
|
:fullscreen="false"
|
||||||
<router-link class="item" :to="{name: 'library.artists.browse'}"><i class="user icon"></i><translate translate-context="*/*/*">Artists</translate></router-link>
|
:show="showLanguageModal"
|
||||||
<router-link class="item" :to="{name: 'library.playlists.browse'}"><i class="list icon"></i><translate translate-context="*/*/*">Playlists</translate></router-link>
|
@update:show="showLanguageModal = $event"
|
||||||
<router-link class="item" :to="{name: 'library.radios.browse'}"><i class="feed icon"></i><translate translate-context="*/*/*">Radios</translate></router-link>
|
>
|
||||||
|
<i
|
||||||
|
role="button"
|
||||||
|
class="left chevron back inside icon"
|
||||||
|
@click.prevent.exact="showUserModal = !showUserModal"
|
||||||
|
/>
|
||||||
|
<div class="header">
|
||||||
|
<h3 class="title">
|
||||||
|
{{ labels.language }}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="content">
|
||||||
<div :class="[{collapsed: !myLibraryExpanded}, 'collapsible item']" v-if="$store.state.auth.authenticated">
|
<fieldset
|
||||||
<h3 class="header" role="button" @click="myLibraryExpanded = true" tabindex="0" @focus="myLibraryExpanded = true">
|
v-for="(language, key) in $language.available"
|
||||||
<translate translate-context="*/*/*/Noun">My Library</translate>
|
:key="key"
|
||||||
<i class="angle right icon" v-if="!myLibraryExpanded"></i>
|
>
|
||||||
</h3>
|
<input
|
||||||
<div class="menu">
|
:id="key"
|
||||||
<router-link class="item" :exact="true" :to="{name: 'library.me'}"><i class="music icon"></i><translate translate-context="Sidebar/Navigation/List item.Link/Verb">Browse</translate></router-link>
|
v-model="languageSelection"
|
||||||
<router-link class="item" :to="{name: 'library.albums.me'}"><i class="compact disc icon"></i><translate translate-context="*/*/*">Albums</translate></router-link>
|
type="radio"
|
||||||
<router-link class="item" :to="{name: 'library.artists.me'}"><i class="user icon"></i><translate translate-context="*/*/*">Artists</translate></router-link>
|
name="language"
|
||||||
<router-link class="item" :to="{name: 'library.playlists.me'}"><i class="list icon"></i><translate translate-context="*/*/*">Playlists</translate></router-link>
|
:value="key"
|
||||||
<router-link class="item" :to="{name: 'library.radios.me'}"><i class="feed icon"></i><translate translate-context="*/*/*">Radios</translate></router-link>
|
>
|
||||||
<router-link class="item" :to="{name: 'favorites'}"><i class="heart icon"></i><translate translate-context="Sidebar/Favorites/List item.Link/Noun">Favorites</translate></router-link>
|
<label :for="key">{{ language }}</label>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal>
|
||||||
<router-link class="header item" :to="{name: 'subscriptions'}" v-if="$store.state.auth.authenticated">
|
<modal
|
||||||
<translate translate-context="*/*/*">Channels</translate>
|
ref="themeModal"
|
||||||
</router-link>
|
:fullscreen="false"
|
||||||
<div class="item">
|
:show="showThemeModal"
|
||||||
<h3 class="header">
|
@update:show="showThemeModal = $event"
|
||||||
<translate translate-context="Footer/About/List item.Link">More</translate>
|
>
|
||||||
</h3>
|
<i
|
||||||
<div class="menu">
|
role="button"
|
||||||
<router-link class="item" to="/about">
|
class="left chevron back inside icon"
|
||||||
<i class="info icon"></i><translate translate-context="Sidebar/*/List item.Link">About this pod</translate>
|
@click.prevent.exact="showUserModal = !showUserModal"
|
||||||
</router-link>
|
/>
|
||||||
|
<div class="header">
|
||||||
|
<h3 class="title">
|
||||||
|
{{ labels.theme }}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<fieldset
|
||||||
|
v-for="theme in themes"
|
||||||
|
:key="theme.key"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="theme.key"
|
||||||
|
v-model="themeSelection"
|
||||||
|
type="radio"
|
||||||
|
name="theme"
|
||||||
|
:value="theme.key"
|
||||||
|
>
|
||||||
|
<label :for="theme.key">{{ theme.name }}</label>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
<div class="item collapse-button-wrapper">
|
||||||
|
<button
|
||||||
|
:class="['ui', 'basic', 'big', {'vibrant': !isCollapsed}, 'inverted icon', 'collapse', 'button']"
|
||||||
|
@click="isCollapsed = !isCollapsed"
|
||||||
|
>
|
||||||
|
<i class="sidebar icon" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</section>
|
</header>
|
||||||
</nav>
|
<div class="ui basic search-wrapper segment">
|
||||||
</aside>
|
<search-bar @search="isCollapsed = false" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!$store.state.auth.authenticated"
|
||||||
|
class="ui basic signup segment"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
class="ui fluid tiny primary button"
|
||||||
|
:to="{name: 'login'}"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/Login/*/Verb">
|
||||||
|
Login
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<div class="ui small hidden divider" />
|
||||||
|
<router-link
|
||||||
|
class="ui fluid tiny button"
|
||||||
|
:to="{path: '/signup'}"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/Signup/Link/Verb">
|
||||||
|
Create an account
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<nav
|
||||||
|
class="secondary"
|
||||||
|
role="navigation"
|
||||||
|
aria-labelledby="navigation-label"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
id="navigation-label"
|
||||||
|
class="visually-hidden"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/*">
|
||||||
|
Main navigation
|
||||||
|
</translate>
|
||||||
|
</h1>
|
||||||
|
<div class="ui small hidden divider" />
|
||||||
|
<section
|
||||||
|
:class="['ui', 'bottom', 'attached', {active: selectedTab === 'library'}, 'tab']"
|
||||||
|
:aria-label="labels.mainMenu"
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
class="ui vertical large fluid inverted menu"
|
||||||
|
role="navigation"
|
||||||
|
:aria-label="labels.mainMenu"
|
||||||
|
>
|
||||||
|
<div :class="[{collapsed: !exploreExpanded}, 'collapsible item']">
|
||||||
|
<h2
|
||||||
|
class="header"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="exploreExpanded = true"
|
||||||
|
@focus="exploreExpanded = true"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/*/Verb">
|
||||||
|
Explore
|
||||||
|
</translate>
|
||||||
|
<i
|
||||||
|
v-if="!exploreExpanded"
|
||||||
|
class="angle right icon"
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<div class="menu">
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:to="{name: 'search'}"
|
||||||
|
>
|
||||||
|
<i class="search icon" /><translate translate-context="Sidebar/Navigation/List item.Link/Verb">
|
||||||
|
Search
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:exact="true"
|
||||||
|
:to="{name: 'library.index'}"
|
||||||
|
>
|
||||||
|
<i class="music icon" /><translate translate-context="Sidebar/Navigation/List item.Link/Verb">
|
||||||
|
Browse
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:to="{name: 'library.podcasts.browse'}"
|
||||||
|
>
|
||||||
|
<i class="podcast icon" /><translate translate-context="*/*/*">
|
||||||
|
Podcasts
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:to="{name: 'library.albums.browse'}"
|
||||||
|
>
|
||||||
|
<i class="compact disc icon" /><translate translate-context="*/*/*">
|
||||||
|
Albums
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:to="{name: 'library.artists.browse'}"
|
||||||
|
>
|
||||||
|
<i class="user icon" /><translate translate-context="*/*/*">
|
||||||
|
Artists
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:to="{name: 'library.playlists.browse'}"
|
||||||
|
>
|
||||||
|
<i class="list icon" /><translate translate-context="*/*/*">
|
||||||
|
Playlists
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:to="{name: 'library.radios.browse'}"
|
||||||
|
>
|
||||||
|
<i class="feed icon" /><translate translate-context="*/*/*">
|
||||||
|
Radios
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="$store.state.auth.authenticated"
|
||||||
|
:class="[{collapsed: !myLibraryExpanded}, 'collapsible item']"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="header"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="myLibraryExpanded = true"
|
||||||
|
@focus="myLibraryExpanded = true"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/*/Noun">
|
||||||
|
My Library
|
||||||
|
</translate>
|
||||||
|
<i
|
||||||
|
v-if="!myLibraryExpanded"
|
||||||
|
class="angle right icon"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<div class="menu">
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:exact="true"
|
||||||
|
:to="{name: 'library.me'}"
|
||||||
|
>
|
||||||
|
<i class="music icon" /><translate translate-context="Sidebar/Navigation/List item.Link/Verb">
|
||||||
|
Browse
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:to="{name: 'library.albums.me'}"
|
||||||
|
>
|
||||||
|
<i class="compact disc icon" /><translate translate-context="*/*/*">
|
||||||
|
Albums
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:to="{name: 'library.artists.me'}"
|
||||||
|
>
|
||||||
|
<i class="user icon" /><translate translate-context="*/*/*">
|
||||||
|
Artists
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:to="{name: 'library.playlists.me'}"
|
||||||
|
>
|
||||||
|
<i class="list icon" /><translate translate-context="*/*/*">
|
||||||
|
Playlists
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:to="{name: 'library.radios.me'}"
|
||||||
|
>
|
||||||
|
<i class="feed icon" /><translate translate-context="*/*/*">
|
||||||
|
Radios
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
:to="{name: 'favorites'}"
|
||||||
|
>
|
||||||
|
<i class="heart icon" /><translate translate-context="Sidebar/Favorites/List item.Link/Noun">
|
||||||
|
Favorites
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<router-link
|
||||||
|
v-if="$store.state.auth.authenticated"
|
||||||
|
class="header item"
|
||||||
|
:to="{name: 'subscriptions'}"
|
||||||
|
>
|
||||||
|
<translate translate-context="*/*/*">
|
||||||
|
Channels
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
<div class="item">
|
||||||
|
<h3 class="header">
|
||||||
|
<translate translate-context="Footer/About/List item.Link">
|
||||||
|
More
|
||||||
|
</translate>
|
||||||
|
</h3>
|
||||||
|
<div class="menu">
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
to="/about"
|
||||||
|
>
|
||||||
|
<i class="info icon" /><translate translate-context="Sidebar/*/List item.Link">
|
||||||
|
About this pod
|
||||||
|
</translate>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!production"
|
||||||
|
class="item"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
href=""
|
||||||
|
class="link item"
|
||||||
|
@click.prevent="$emit('show:set-instance-modal')"
|
||||||
|
>Switch instance</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</section>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapActions, mapGetters } from "vuex"
|
import { mapState, mapActions, mapGetters } from 'vuex'
|
||||||
|
import UserModal from '@/components/common/UserModal'
|
||||||
|
import Logo from '@/components/Logo'
|
||||||
|
import SearchBar from '@/components/audio/SearchBar'
|
||||||
|
import UserMenu from '@/components/common/UserMenu'
|
||||||
|
import Modal from '@/components/semantic/Modal'
|
||||||
|
|
||||||
import Logo from "@/components/Logo"
|
import $ from 'jquery'
|
||||||
import SearchBar from "@/components/audio/SearchBar"
|
|
||||||
|
|
||||||
import $ from "jquery"
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "sidebar",
|
name: 'Sidebar',
|
||||||
components: {
|
components: {
|
||||||
SearchBar,
|
SearchBar,
|
||||||
Logo
|
Logo,
|
||||||
|
UserMenu,
|
||||||
|
UserModal,
|
||||||
|
Modal
|
||||||
},
|
},
|
||||||
data() {
|
props: {
|
||||||
|
width: { type: Number, required: true }
|
||||||
|
},
|
||||||
|
data () {
|
||||||
return {
|
return {
|
||||||
selectedTab: "library",
|
selectedTab: 'library',
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
fetchInterval: null,
|
fetchInterval: null,
|
||||||
exploreExpanded: false,
|
exploreExpanded: false,
|
||||||
myLibraryExpanded: false,
|
myLibraryExpanded: false,
|
||||||
|
showUserModal: false,
|
||||||
|
showLanguageModal: false,
|
||||||
|
showThemeModal: false,
|
||||||
|
languageSelection: this.$language.current,
|
||||||
|
themeSelection: this.$store.state.ui.theme
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy () {
|
||||||
if (this.fetchInterval) {
|
if (this.fetchInterval) {
|
||||||
clearInterval(this.fetchInterval)
|
clearInterval(this.fetchInterval)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
document.getElementById('fake-sidebar').classList.add('loaded')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
|
||||||
additionalNotifications: "ui/additionalNotifications",
|
|
||||||
}),
|
|
||||||
...mapState({
|
...mapState({
|
||||||
queue: state => state.queue,
|
queue: state => state.queue,
|
||||||
url: state => state.route.path
|
url: state => state.route.path
|
||||||
}),
|
}),
|
||||||
labels() {
|
...mapGetters({
|
||||||
let mainMenu = this.$pgettext('Sidebar/*/Hidden text', "Main menu")
|
additionalNotifications: 'ui/additionalNotifications'
|
||||||
let selectTrack = this.$pgettext('Sidebar/Player/Hidden text', "Play this track")
|
}),
|
||||||
let pendingFollows = this.$pgettext('Sidebar/Notifications/Hidden text', "Pending follow requests")
|
labels () {
|
||||||
let pendingReviewEdits = this.$pgettext('Sidebar/Moderation/Hidden text', "Pending review edits")
|
const mainMenu = this.$pgettext('Sidebar/*/Hidden text', 'Main menu')
|
||||||
|
const selectTrack = this.$pgettext('Sidebar/Player/Hidden text', 'Play this track')
|
||||||
|
const pendingFollows = this.$pgettext('Sidebar/Notifications/Hidden text', 'Pending follow requests')
|
||||||
|
const pendingReviewEdits = this.$pgettext('Sidebar/Moderation/Hidden text', 'Pending review edits')
|
||||||
|
const language = this.$pgettext(
|
||||||
|
'Sidebar/Settings/Dropdown.Label/Short, Verb',
|
||||||
|
'Language')
|
||||||
|
const theme = this.$pgettext(
|
||||||
|
'Sidebar/Settings/Dropdown.Label/Short, Verb',
|
||||||
|
'Theme')
|
||||||
return {
|
return {
|
||||||
pendingFollows,
|
pendingFollows,
|
||||||
mainMenu,
|
mainMenu,
|
||||||
selectTrack,
|
selectTrack,
|
||||||
pendingReviewEdits,
|
pendingReviewEdits,
|
||||||
addContent: this.$pgettext("*/Library/*/Verb", 'Add content'),
|
language,
|
||||||
notifications: this.$pgettext("*/Notifications/*", 'Notifications'),
|
theme,
|
||||||
administration: this.$pgettext("Sidebar/Admin/Title/Noun", 'Administration'),
|
addContent: this.$pgettext('*/Library/*/Verb', 'Add content'),
|
||||||
|
administration: this.$pgettext('Sidebar/Admin/Title/Noun', 'Administration')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
logoUrl() {
|
logoUrl () {
|
||||||
if (this.$store.state.auth.authenticated) {
|
if (this.$store.state.auth.authenticated) {
|
||||||
return "library.index"
|
return 'library.index'
|
||||||
} else {
|
} else {
|
||||||
return "index"
|
return 'index'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
focusedMenu () {
|
focusedMenu () {
|
||||||
let mapping = {
|
const mapping = {
|
||||||
"search": 'exploreExpanded',
|
search: 'exploreExpanded',
|
||||||
"library.index": 'exploreExpanded',
|
'library.index': 'exploreExpanded',
|
||||||
"library.podcasts.browse": 'exploreExpanded',
|
'library.podcasts.browse': 'exploreExpanded',
|
||||||
"library.albums.browse": 'exploreExpanded',
|
'library.albums.browse': 'exploreExpanded',
|
||||||
"library.albums.detail": 'exploreExpanded',
|
'library.albums.detail': 'exploreExpanded',
|
||||||
"library.artists.browse": 'exploreExpanded',
|
'library.artists.browse': 'exploreExpanded',
|
||||||
"library.artists.detail": 'exploreExpanded',
|
'library.artists.detail': 'exploreExpanded',
|
||||||
"library.tracks.detail": 'exploreExpanded',
|
'library.tracks.detail': 'exploreExpanded',
|
||||||
"library.playlists.browse": 'exploreExpanded',
|
'library.playlists.browse': 'exploreExpanded',
|
||||||
"library.playlists.detail": 'exploreExpanded',
|
'library.playlists.detail': 'exploreExpanded',
|
||||||
"library.radios.browse": 'exploreExpanded',
|
'library.radios.browse': 'exploreExpanded',
|
||||||
"library.radios.detail": 'exploreExpanded',
|
'library.radios.detail': 'exploreExpanded',
|
||||||
'library.me': "myLibraryExpanded",
|
'library.me': 'myLibraryExpanded',
|
||||||
'library.albums.me': "myLibraryExpanded",
|
'library.albums.me': 'myLibraryExpanded',
|
||||||
'library.artists.me': "myLibraryExpanded",
|
'library.artists.me': 'myLibraryExpanded',
|
||||||
'library.playlists.me': "myLibraryExpanded",
|
'library.playlists.me': 'myLibraryExpanded',
|
||||||
'library.radios.me': "myLibraryExpanded",
|
'library.radios.me': 'myLibraryExpanded',
|
||||||
'favorites': "myLibraryExpanded",
|
favorites: 'myLibraryExpanded'
|
||||||
}
|
}
|
||||||
let m = mapping[this.$route.name]
|
const m = mapping[this.$route.name]
|
||||||
if (m) {
|
if (m) {
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
@ -263,57 +586,31 @@ export default {
|
||||||
this.$store.state.ui.notifications.pendingReviewReports +
|
this.$store.state.ui.notifications.pendingReviewReports +
|
||||||
this.$store.state.ui.notifications.pendingReviewRequests
|
this.$store.state.ui.notifications.pendingReviewRequests
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions({
|
|
||||||
cleanTrack: "queue/cleanTrack"
|
|
||||||
}),
|
|
||||||
applyContentFilters () {
|
|
||||||
let artistIds = this.$store.getters['moderation/artistFilters']().map((f) => {
|
|
||||||
return f.target.id
|
|
||||||
})
|
|
||||||
|
|
||||||
if (artistIds.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let self = this
|
|
||||||
let tracks = this.tracks.slice().reverse()
|
|
||||||
tracks.forEach(async (t, i) => {
|
|
||||||
// we loop from the end because removing index from the start can lead to removing the wrong tracks
|
|
||||||
let realIndex = tracks.length - i - 1
|
|
||||||
let matchArtist = artistIds.indexOf(t.artist.id) > -1
|
|
||||||
if (matchArtist) {
|
|
||||||
return await self.cleanTrack(realIndex)
|
|
||||||
}
|
|
||||||
if (t.album && artistIds.indexOf(t.album.artist.id) > -1) {
|
|
||||||
return await self.cleanTrack(realIndex)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
setupDropdown (selector) {
|
production () {
|
||||||
let self = this
|
return process.env.NODE_ENV === 'production'
|
||||||
$(self.$el).find(selector).dropdown({
|
},
|
||||||
selectOnKeydown: false,
|
themes () {
|
||||||
action: function (text, value, $el) {
|
return [
|
||||||
// used ton ensure focusing the dropdown and clicking via keyboard
|
{
|
||||||
// works as expected
|
name: this.$pgettext('Sidebar/Settings/Dropdown.Label/Theme name', 'Light'),
|
||||||
let link = $($el).closest('a')
|
key: 'light'
|
||||||
let url = link.attr('href')
|
},
|
||||||
self.$router.push(url)
|
{
|
||||||
$(self.$el).find(selector).dropdown('hide')
|
name: this.$pgettext('Sidebar/Settings/Dropdown.Label/Theme name', 'Dark'),
|
||||||
|
key: 'dark'
|
||||||
}
|
}
|
||||||
})
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
url: function() {
|
url: function () {
|
||||||
this.isCollapsed = true
|
this.isCollapsed = true
|
||||||
},
|
},
|
||||||
"$store.state.moderation.lastUpdate": function () {
|
'$store.state.moderation.lastUpdate': function () {
|
||||||
this.applyContentFilters()
|
this.applyContentFilters()
|
||||||
},
|
},
|
||||||
"$store.state.auth.authenticated": {
|
'$store.state.auth.authenticated': {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler (v) {
|
handler (v) {
|
||||||
if (v) {
|
if (v) {
|
||||||
|
@ -321,17 +618,21 @@ export default {
|
||||||
this.setupDropdown('.user-dropdown')
|
this.setupDropdown('.user-dropdown')
|
||||||
this.setupDropdown('.admin-dropdown')
|
this.setupDropdown('.admin-dropdown')
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.setupDropdown('.user-dropdown')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"$store.state.auth.availablePermissions": {
|
'$store.state.auth.availablePermissions': {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler (v) {
|
handler (v) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.setupDropdown('.admin-dropdown')
|
this.setupDropdown('.admin-dropdown')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true
|
||||||
},
|
},
|
||||||
focusedMenu: {
|
focusedMenu: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
|
@ -351,6 +652,93 @@ export default {
|
||||||
this.myLibraryExpanded = false
|
this.myLibraryExpanded = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
languageSelection: function (v) {
|
||||||
|
this.$store.dispatch('ui/currentLanguage', v)
|
||||||
|
this.$refs.languageModal.closeModal()
|
||||||
|
},
|
||||||
|
themeSelection: function (v) {
|
||||||
|
this.$store.dispatch('ui/theme', v)
|
||||||
|
this.$refs.themeModal.closeModal()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
document.getElementById('fake-sidebar').classList.add('loaded')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions({
|
||||||
|
cleanTrack: 'queue/cleanTrack'
|
||||||
|
}),
|
||||||
|
applyContentFilters () {
|
||||||
|
const artistIds = this.$store.getters['moderation/artistFilters']().map((f) => {
|
||||||
|
return f.target.id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (artistIds.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const self = this
|
||||||
|
const tracks = this.tracks.slice().reverse()
|
||||||
|
tracks.forEach(async (t, i) => {
|
||||||
|
// we loop from the end because removing index from the start can lead to removing the wrong tracks
|
||||||
|
const realIndex = tracks.length - i - 1
|
||||||
|
const matchArtist = artistIds.indexOf(t.artist.id) > -1
|
||||||
|
if (matchArtist) {
|
||||||
|
return await self.cleanTrack(realIndex)
|
||||||
|
}
|
||||||
|
if (t.album && artistIds.indexOf(t.album.artist.id) > -1) {
|
||||||
|
return await self.cleanTrack(realIndex)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setupDropdown (selector) {
|
||||||
|
const self = this
|
||||||
|
$(self.$el).find(selector).dropdown({
|
||||||
|
selectOnKeydown: false,
|
||||||
|
action: function (text, value, $el) {
|
||||||
|
// used ton ensure focusing the dropdown and clicking via keyboard
|
||||||
|
// works as expected
|
||||||
|
const link = $($el).closest('a')
|
||||||
|
const url = link.attr('href')
|
||||||
|
self.$router.push(url)
|
||||||
|
$(self.$el).find(selector).dropdown('hide')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
[type="radio"] {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
[type="radio"] + label::after {
|
||||||
|
content: "";
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
[type="radio"]:checked + label::after {
|
||||||
|
margin-left: 10px;
|
||||||
|
content: "\2713"; /* Checkmark */
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
[type="radio"]:checked + label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
fieldset {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.back {
|
||||||
|
font-size: 1.25em !important;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
left: 0.5rem;
|
||||||
|
width: 2.25rem !important;
|
||||||
|
height: 2.25rem !important;
|
||||||
|
padding: 0.625rem 0 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<modal
|
||||||
@update:show="$emit('update:show', $event)"
|
ref="modal"
|
||||||
:show="show"
|
:show="show"
|
||||||
:scrolling="true"
|
:scrolling="true"
|
||||||
:additionalClasses="['scrolling-track-options']"
|
:additional-classes="['scrolling-track-options']"
|
||||||
|
@update:show="$emit('update:show', $event)"
|
||||||
>
|
>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="ui large centered rounded image">
|
<div class="ui large centered rounded image">
|
||||||
<img
|
<img
|
||||||
alt=""
|
|
||||||
class="ui centered image"
|
|
||||||
v-if="
|
v-if="
|
||||||
track.album && track.album.cover && track.album.cover.urls.original
|
track.album && track.album.cover && track.album.cover.urls.original
|
||||||
"
|
"
|
||||||
|
@ -18,43 +17,50 @@
|
||||||
track.album.cover.urls.medium_square_crop
|
track.album.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui centered image"
|
class="ui centered image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else-if="track.cover"
|
v-else-if="track.cover"
|
||||||
v-lazy="
|
v-lazy="
|
||||||
$store.getters['instance/absoluteUrl'](
|
$store.getters['instance/absoluteUrl'](
|
||||||
track.cover.urls.medium_square_crop
|
track.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui centered image"
|
class="ui centered image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else-if="track.artist.cover"
|
v-else-if="track.artist.cover"
|
||||||
v-lazy="
|
v-lazy="
|
||||||
$store.getters['instance/absoluteUrl'](
|
$store.getters['instance/absoluteUrl'](
|
||||||
track.artist.cover.urls.medium_square_crop
|
track.artist.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui centered image"
|
class="ui centered image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else
|
v-else
|
||||||
|
alt=""
|
||||||
|
class="ui centered image"
|
||||||
src="../../../assets/audio/default-cover.png"
|
src="../../../assets/audio/default-cover.png"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="track-modal-title">{{ track.title }}</h3>
|
<h3 class="track-modal-title">
|
||||||
<h4 class="track-modal-subtitle">{{ track.artist.name }}</h4>
|
{{ track.title }}
|
||||||
|
</h3>
|
||||||
|
<h4 class="track-modal-subtitle">
|
||||||
|
{{ track.artist.name }}
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="ui one column unstackable grid">
|
<div class="ui one column unstackable grid">
|
||||||
<div
|
<div
|
||||||
|
v-if="$store.state.auth.authenticated && track.artist.content_category !== 'podcast'"
|
||||||
class="row"
|
class="row"
|
||||||
v-if="$store.state.auth.authenticated && this.track.artist.content_category !== 'podcast'">
|
>
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="column"
|
class="column"
|
||||||
|
@ -80,11 +86,11 @@
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
|
:aria-label="labels.addToQueue"
|
||||||
@click.stop.prevent="
|
@click.stop.prevent="
|
||||||
add();
|
add();
|
||||||
closeModal();
|
$refs.modal.closeModal();
|
||||||
"
|
"
|
||||||
:aria-label="labels.addToQueue"
|
|
||||||
>
|
>
|
||||||
<i class="plus icon track-modal list-icon" />
|
<i class="plus icon track-modal list-icon" />
|
||||||
<span class="track-modal list-item">{{ labels.addToQueue }}</span>
|
<span class="track-modal list-item">{{ labels.addToQueue }}</span>
|
||||||
|
@ -94,11 +100,11 @@
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
|
:aria-label="labels.playNext"
|
||||||
@click.stop.prevent="
|
@click.stop.prevent="
|
||||||
addNext(true);
|
addNext(true);
|
||||||
closeModal();
|
$refs.modal.closeModal();
|
||||||
"
|
"
|
||||||
:aria-label="labels.playNext"
|
|
||||||
>
|
>
|
||||||
<i class="step forward icon track-modal list-icon" />
|
<i class="step forward icon track-modal list-icon" />
|
||||||
<span class="track-modal list-item">{{ labels.playNext }}</span>
|
<span class="track-modal list-item">{{ labels.playNext }}</span>
|
||||||
|
@ -108,14 +114,14 @@
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
|
:aria-label="labels.startRadio"
|
||||||
@click.stop.prevent="
|
@click.stop.prevent="
|
||||||
$store.dispatch('radios/start', {
|
$store.dispatch('radios/start', {
|
||||||
type: 'similar',
|
type: 'similar',
|
||||||
objectId: track.id,
|
objectId: track.id,
|
||||||
});
|
});
|
||||||
closeModal();
|
$refs.modal.closeModal();
|
||||||
"
|
"
|
||||||
:aria-label="labels.startRadio"
|
|
||||||
>
|
>
|
||||||
<i class="rss icon track-modal list-icon" />
|
<i class="rss icon track-modal list-icon" />
|
||||||
<span class="track-modal list-item">{{ labels.startRadio }}</span>
|
<span class="track-modal list-item">{{ labels.startRadio }}</span>
|
||||||
|
@ -125,8 +131,8 @@
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
@click.stop="$store.commit('playlists/chooseTrack', track)"
|
|
||||||
:aria-label="labels.addToPlaylist"
|
:aria-label="labels.addToPlaylist"
|
||||||
|
@click.stop="$store.commit('playlists/chooseTrack', track)"
|
||||||
>
|
>
|
||||||
<i class="list icon track-modal list-icon" />
|
<i class="list icon track-modal list-icon" />
|
||||||
<span class="track-modal list-item">{{
|
<span class="track-modal list-item">{{
|
||||||
|
@ -134,8 +140,11 @@
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider" />
|
||||||
<div v-if="!isAlbum && track.album" class="row">
|
<div
|
||||||
|
v-if="!isAlbum && track.album"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -153,7 +162,10 @@
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isArtist" class="row">
|
<div
|
||||||
|
v-if="!isArtist"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -189,7 +201,7 @@
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider" />
|
||||||
<div
|
<div
|
||||||
v-for="obj in getReportableObjs({
|
v-for="obj in getReportableObjs({
|
||||||
track,
|
track,
|
||||||
|
@ -197,16 +209,15 @@
|
||||||
artist,
|
artist,
|
||||||
})"
|
})"
|
||||||
:key="obj.target.type + obj.target.id"
|
:key="obj.target.type + obj.target.id"
|
||||||
class="row"
|
|
||||||
:ref="`report${obj.target.type}${obj.target.id}`"
|
:ref="`report${obj.target.type}${obj.target.id}`"
|
||||||
|
class="row"
|
||||||
:data-ref="`report${obj.target.type}${obj.target.id}`"
|
:data-ref="`report${obj.target.type}${obj.target.id}`"
|
||||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
|
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
|
||||||
>
|
>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<i class="share icon track-modal list-icon" /><span
|
<i class="share icon track-modal list-icon" /><span
|
||||||
class="track-modal list-item"
|
class="track-modal list-item"
|
||||||
>{{ obj.label }}</span
|
>{{ obj.label }}</span>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -215,90 +226,83 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Modal from "@/components/semantic/Modal";
|
import Modal from '@/components/semantic/Modal'
|
||||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
|
||||||
import ReportMixin from '@/components/mixins/Report'
|
import ReportMixin from '@/components/mixins/Report'
|
||||||
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
Modal
|
||||||
|
},
|
||||||
mixins: [ReportMixin, PlayOptionsMixin],
|
mixins: [ReportMixin, PlayOptionsMixin],
|
||||||
props: {
|
props: {
|
||||||
show: { type: Boolean, required: true, default: false },
|
show: { type: Boolean, required: true, default: false },
|
||||||
track: { type: Object, required: true },
|
track: { type: Object, required: true },
|
||||||
index: { type: Number, required: true },
|
index: { type: Number, required: true },
|
||||||
isArtist: { type: Boolean, required: false, default: false },
|
isArtist: { type: Boolean, required: false, default: false },
|
||||||
isAlbum: { type: Boolean, required: false, default: false },
|
isAlbum: { type: Boolean, required: false, default: false }
|
||||||
},
|
},
|
||||||
components: {
|
data () {
|
||||||
Modal,
|
|
||||||
TrackFavoriteIcon,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
return {
|
||||||
isShowing: this.show,
|
isShowing: this.show,
|
||||||
tracks: [this.track],
|
tracks: [this.track],
|
||||||
album: this.track.album,
|
album: this.track.album,
|
||||||
artist: this.track.artist,
|
artist: this.track.artist
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isFavorite() {
|
isFavorite () {
|
||||||
return this.$store.getters["favorites/isFavorite"](this.track.id);
|
return this.$store.getters['favorites/isFavorite'](this.track.id)
|
||||||
},
|
},
|
||||||
favoriteButton() {
|
favoriteButton () {
|
||||||
if (this.isFavorite) {
|
if (this.isFavorite) {
|
||||||
return this.$pgettext(
|
return this.$pgettext(
|
||||||
"Content/Track/Icon.Tooltip/Verb",
|
'Content/Track/Icon.Tooltip/Verb',
|
||||||
"Remove from favorites"
|
'Remove from favorites'
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
return this.$pgettext("Content/Track/*/Verb", "Add to favorites");
|
return this.$pgettext('Content/Track/*/Verb', 'Add to favorites')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trackDetailsButton() {
|
trackDetailsButton () {
|
||||||
if (this.track.artist.content_category === 'podcast') {
|
if (this.track.artist.content_category === 'podcast') {
|
||||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Episode details")
|
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'Episode details')
|
||||||
} else {
|
} else {
|
||||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Track details")
|
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'Track details')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
albumDetailsButton() {
|
albumDetailsButton () {
|
||||||
if (this.track.artist.content_category === 'podcast') {
|
if (this.track.artist.content_category === 'podcast') {
|
||||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View series")
|
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View series')
|
||||||
} else {
|
} else {
|
||||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View album")
|
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View album')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
artistDetailsButton() {
|
artistDetailsButton () {
|
||||||
if (this.track.artist.content_category === 'podcast') {
|
if (this.track.artist.content_category === 'podcast') {
|
||||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View channel")
|
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View channel')
|
||||||
} else {
|
} else {
|
||||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View artist")
|
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View artist')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
startRadio: this.$pgettext(
|
startRadio: this.$pgettext(
|
||||||
"*/Queue/Dropdown/Button/Title",
|
'*/Queue/Dropdown/Button/Title',
|
||||||
"Play radio"
|
'Play radio'
|
||||||
),
|
),
|
||||||
playNow: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play now"),
|
playNow: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play now'),
|
||||||
addToQueue: this.$pgettext(
|
addToQueue: this.$pgettext(
|
||||||
"*/Queue/Dropdown/Button/Title",
|
'*/Queue/Dropdown/Button/Title',
|
||||||
"Add to queue"
|
'Add to queue'
|
||||||
),
|
),
|
||||||
playNext: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play next"),
|
playNext: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play next'),
|
||||||
addToPlaylist: this.$pgettext(
|
addToPlaylist: this.$pgettext(
|
||||||
"Sidebar/Player/Icon.Tooltip/Verb",
|
'Sidebar/Player/Icon.Tooltip/Verb',
|
||||||
"Add to playlist…"
|
'Add to playlist…'
|
||||||
),
|
)
|
||||||
};
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
methods: {
|
}
|
||||||
closeModal() {
|
|
||||||
this.$emit("update:show", false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<modal
|
||||||
@update:show="$emit('update:show', $event)"
|
ref="modal"
|
||||||
:show="show"
|
:show="show"
|
||||||
:scrolling="true"
|
:scrolling="true"
|
||||||
:additionalClasses="['scrolling-track-options']"
|
:additional-classes="['scrolling-track-options']"
|
||||||
|
@update:show="$emit('update:show', $event)"
|
||||||
>
|
>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="ui large centered rounded image">
|
<div class="ui large centered rounded image">
|
||||||
<img
|
<img
|
||||||
alt=""
|
|
||||||
class="ui centered image"
|
|
||||||
v-if="
|
v-if="
|
||||||
track.album && track.album.cover && track.album.cover.urls.original
|
track.album && track.album.cover && track.album.cover.urls.original
|
||||||
"
|
"
|
||||||
|
@ -18,43 +17,50 @@
|
||||||
track.album.cover.urls.medium_square_crop
|
track.album.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui centered image"
|
class="ui centered image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else-if="track.cover"
|
v-else-if="track.cover"
|
||||||
v-lazy="
|
v-lazy="
|
||||||
$store.getters['instance/absoluteUrl'](
|
$store.getters['instance/absoluteUrl'](
|
||||||
track.cover.urls.medium_square_crop
|
track.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui centered image"
|
class="ui centered image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else-if="track.artist.cover"
|
v-else-if="track.artist.cover"
|
||||||
v-lazy="
|
v-lazy="
|
||||||
$store.getters['instance/absoluteUrl'](
|
$store.getters['instance/absoluteUrl'](
|
||||||
track.artist.cover.urls.medium_square_crop
|
track.artist.cover.urls.medium_square_crop
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt=""
|
alt=""
|
||||||
class="ui centered image"
|
class="ui centered image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
v-else
|
v-else
|
||||||
|
alt=""
|
||||||
|
class="ui centered image"
|
||||||
src="../../../assets/audio/default-cover.png"
|
src="../../../assets/audio/default-cover.png"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="track-modal-title">{{ track.title }}</h3>
|
<h3 class="track-modal-title">
|
||||||
<h4 class="track-modal-subtitle">{{ track.artist.name }}</h4>
|
{{ track.title }}
|
||||||
|
</h3>
|
||||||
|
<h4 class="track-modal-subtitle">
|
||||||
|
{{ track.artist.name }}
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="ui one column unstackable grid">
|
<div class="ui one column unstackable grid">
|
||||||
<div
|
<div
|
||||||
|
v-if="$store.state.auth.authenticated && track.artist.content_category !== 'podcast'"
|
||||||
class="row"
|
class="row"
|
||||||
v-if="$store.state.auth.authenticated && this.track.artist.content_category !== 'podcast'">
|
>
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="column"
|
class="column"
|
||||||
|
@ -80,11 +86,11 @@
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
|
:aria-label="labels.addToQueue"
|
||||||
@click.stop.prevent="
|
@click.stop.prevent="
|
||||||
add();
|
add();
|
||||||
closeModal();
|
$refs.modal.closeModal();
|
||||||
"
|
"
|
||||||
:aria-label="labels.addToQueue"
|
|
||||||
>
|
>
|
||||||
<i class="plus icon track-modal list-icon" />
|
<i class="plus icon track-modal list-icon" />
|
||||||
<span class="track-modal list-item">{{ labels.addToQueue }}</span>
|
<span class="track-modal list-item">{{ labels.addToQueue }}</span>
|
||||||
|
@ -94,11 +100,11 @@
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
|
:aria-label="labels.playNext"
|
||||||
@click.stop.prevent="
|
@click.stop.prevent="
|
||||||
addNext(true);
|
addNext(true);
|
||||||
closeModal();
|
$refs.modal.closeModal();
|
||||||
"
|
"
|
||||||
:aria-label="labels.playNext"
|
|
||||||
>
|
>
|
||||||
<i class="step forward icon track-modal list-icon" />
|
<i class="step forward icon track-modal list-icon" />
|
||||||
<span class="track-modal list-item">{{ labels.playNext }}</span>
|
<span class="track-modal list-item">{{ labels.playNext }}</span>
|
||||||
|
@ -108,14 +114,14 @@
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
|
:aria-label="labels.startRadio"
|
||||||
@click.stop.prevent="
|
@click.stop.prevent="
|
||||||
$store.dispatch('radios/start', {
|
$store.dispatch('radios/start', {
|
||||||
type: 'similar',
|
type: 'similar',
|
||||||
objectId: track.id,
|
objectId: track.id,
|
||||||
});
|
});
|
||||||
closeModal();
|
$refs.modal.closeModal();
|
||||||
"
|
"
|
||||||
:aria-label="labels.startRadio"
|
|
||||||
>
|
>
|
||||||
<i class="rss icon track-modal list-icon" />
|
<i class="rss icon track-modal list-icon" />
|
||||||
<span class="track-modal list-item">{{ labels.startRadio }}</span>
|
<span class="track-modal list-item">{{ labels.startRadio }}</span>
|
||||||
|
@ -125,8 +131,8 @@
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
@click.stop="$store.commit('playlists/chooseTrack', track)"
|
|
||||||
:aria-label="labels.addToPlaylist"
|
:aria-label="labels.addToPlaylist"
|
||||||
|
@click.stop="$store.commit('playlists/chooseTrack', track)"
|
||||||
>
|
>
|
||||||
<i class="list icon track-modal list-icon" />
|
<i class="list icon track-modal list-icon" />
|
||||||
<span class="track-modal list-item">{{
|
<span class="track-modal list-item">{{
|
||||||
|
@ -134,8 +140,11 @@
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider" />
|
||||||
<div v-if="!isAlbum && track.album" class="row">
|
<div
|
||||||
|
v-if="!isAlbum && track.album"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -153,7 +162,10 @@
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isArtist" class="row">
|
<div
|
||||||
|
v-if="!isArtist"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -189,7 +201,7 @@
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider" />
|
||||||
<div
|
<div
|
||||||
v-for="obj in getReportableObjs({
|
v-for="obj in getReportableObjs({
|
||||||
track,
|
track,
|
||||||
|
@ -197,16 +209,15 @@
|
||||||
artist,
|
artist,
|
||||||
})"
|
})"
|
||||||
:key="obj.target.type + obj.target.id"
|
:key="obj.target.type + obj.target.id"
|
||||||
class="row"
|
|
||||||
:ref="`report${obj.target.type}${obj.target.id}`"
|
:ref="`report${obj.target.type}${obj.target.id}`"
|
||||||
|
class="row"
|
||||||
:data-ref="`report${obj.target.type}${obj.target.id}`"
|
:data-ref="`report${obj.target.type}${obj.target.id}`"
|
||||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
|
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
|
||||||
>
|
>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<i class="share icon track-modal list-icon" /><span
|
<i class="share icon track-modal list-icon" /><span
|
||||||
class="track-modal list-item"
|
class="track-modal list-item"
|
||||||
>{{ obj.label }}</span
|
>{{ obj.label }}</span>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -215,90 +226,83 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Modal from "@/components/semantic/Modal";
|
import Modal from '@/components/semantic/Modal'
|
||||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
|
||||||
import ReportMixin from '@/components/mixins/Report'
|
import ReportMixin from '@/components/mixins/Report'
|
||||||
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
Modal
|
||||||
|
},
|
||||||
mixins: [ReportMixin, PlayOptionsMixin],
|
mixins: [ReportMixin, PlayOptionsMixin],
|
||||||
props: {
|
props: {
|
||||||
show: { type: Boolean, required: true, default: false },
|
show: { type: Boolean, required: true, default: false },
|
||||||
track: { type: Object, required: true },
|
track: { type: Object, required: true },
|
||||||
index: { type: Number, required: true },
|
index: { type: Number, required: true },
|
||||||
isArtist: { type: Boolean, required: false, default: false },
|
isArtist: { type: Boolean, required: false, default: false },
|
||||||
isAlbum: { type: Boolean, required: false, default: false },
|
isAlbum: { type: Boolean, required: false, default: false }
|
||||||
},
|
},
|
||||||
components: {
|
data () {
|
||||||
Modal,
|
|
||||||
TrackFavoriteIcon,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
return {
|
||||||
isShowing: this.show,
|
isShowing: this.show,
|
||||||
tracks: [this.track],
|
tracks: [this.track],
|
||||||
album: this.track.album,
|
album: this.track.album,
|
||||||
artist: this.track.artist,
|
artist: this.track.artist
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isFavorite() {
|
isFavorite () {
|
||||||
return this.$store.getters["favorites/isFavorite"](this.track.id);
|
return this.$store.getters['favorites/isFavorite'](this.track.id)
|
||||||
},
|
},
|
||||||
favoriteButton() {
|
favoriteButton () {
|
||||||
if (this.isFavorite) {
|
if (this.isFavorite) {
|
||||||
return this.$pgettext(
|
return this.$pgettext(
|
||||||
"Content/Track/Icon.Tooltip/Verb",
|
'Content/Track/Icon.Tooltip/Verb',
|
||||||
"Remove from favorites"
|
'Remove from favorites'
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
return this.$pgettext("Content/Track/*/Verb", "Add to favorites");
|
return this.$pgettext('Content/Track/*/Verb', 'Add to favorites')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trackDetailsButton() {
|
trackDetailsButton () {
|
||||||
if (this.track.artist.content_category === 'podcast') {
|
if (this.track.artist.content_category === 'podcast') {
|
||||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Episode details")
|
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'Episode details')
|
||||||
} else {
|
} else {
|
||||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Track details")
|
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'Track details')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
albumDetailsButton() {
|
albumDetailsButton () {
|
||||||
if (this.track.artist.content_category === 'podcast') {
|
if (this.track.artist.content_category === 'podcast') {
|
||||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View series")
|
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View series')
|
||||||
} else {
|
} else {
|
||||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View album")
|
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View album')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
artistDetailsButton() {
|
artistDetailsButton () {
|
||||||
if (this.track.artist.content_category === 'podcast') {
|
if (this.track.artist.content_category === 'podcast') {
|
||||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View channel")
|
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View channel')
|
||||||
} else {
|
} else {
|
||||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View artist")
|
return this.$pgettext('*/Queue/Dropdown/Button/Label/Short', 'View artist')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
labels() {
|
labels () {
|
||||||
return {
|
return {
|
||||||
startRadio: this.$pgettext(
|
startRadio: this.$pgettext(
|
||||||
"*/Queue/Dropdown/Button/Title",
|
'*/Queue/Dropdown/Button/Title',
|
||||||
"Play radio"
|
'Play radio'
|
||||||
),
|
),
|
||||||
playNow: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play now"),
|
playNow: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play now'),
|
||||||
addToQueue: this.$pgettext(
|
addToQueue: this.$pgettext(
|
||||||
"*/Queue/Dropdown/Button/Title",
|
'*/Queue/Dropdown/Button/Title',
|
||||||
"Add to queue"
|
'Add to queue'
|
||||||
),
|
),
|
||||||
playNext: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play next"),
|
playNext: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play next'),
|
||||||
addToPlaylist: this.$pgettext(
|
addToPlaylist: this.$pgettext(
|
||||||
"Sidebar/Player/Icon.Tooltip/Verb",
|
'Sidebar/Player/Icon.Tooltip/Verb',
|
||||||
"Add to playlist…"
|
'Add to playlist…'
|
||||||
),
|
)
|
||||||
};
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
methods: {
|
}
|
||||||
closeModal() {
|
|
||||||
this.$emit("update:show", false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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>
|
<template>
|
||||||
<div :class="additionalClasses.concat(['ui', {'active': show}, {'scrolling': scrolling} ,{'overlay fullscreen': fullscreen && ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal'])">
|
<div :class="additionalClasses.concat(['ui', {'active': show}, {'scrolling': scrolling} ,{'overlay fullscreen': fullscreen && ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal'])">
|
||||||
<i tabindex=0 class="close inside icon"></i>
|
<i
|
||||||
<slot v-if="show">
|
tabindex="0"
|
||||||
|
class="close inside icon"
|
||||||
</slot>
|
/>
|
||||||
|
<slot v-if="show" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -13,15 +14,41 @@ import createFocusTrap from 'focus-trap'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
show: {type: Boolean, required: true},
|
show: { type: Boolean, required: true },
|
||||||
fullscreen: {type: Boolean, default: true},
|
fullscreen: { type: Boolean, default: true },
|
||||||
scrolling: {type: Boolean, required: false, default: false},
|
scrolling: { type: Boolean, required: false, default: false },
|
||||||
additionalClasses: {type: Array, required: false, default: () => []}
|
additionalClasses: { type: Array, required: false, default: () => [] }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
control: null,
|
control: null,
|
||||||
focusTrap: null,
|
focusTrap: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.initModal()
|
||||||
|
this.$emit('show')
|
||||||
|
this.control.modal('show')
|
||||||
|
this.focusTrap.activate()
|
||||||
|
this.focusTrap.unpause()
|
||||||
|
document.body.classList.add('scrolling')
|
||||||
|
} else {
|
||||||
|
if (this.control) {
|
||||||
|
this.$emit('hide')
|
||||||
|
this.control.modal('hide')
|
||||||
|
this.control.remove()
|
||||||
|
this.focusTrap.deactivate()
|
||||||
|
this.focusTrap.pause()
|
||||||
|
document.body.classList.remove('scrolling')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
$route (to, from) {
|
||||||
|
this.closeModal()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
@ -52,29 +79,9 @@ export default {
|
||||||
this.focusTrap.unpause()
|
this.focusTrap.unpause()
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
},
|
closeModal () {
|
||||||
watch: {
|
this.$emit('update:show', false)
|
||||||
show: {
|
|
||||||
handler (newValue) {
|
|
||||||
if (newValue) {
|
|
||||||
this.initModal()
|
|
||||||
this.$emit('show')
|
|
||||||
this.control.modal('show')
|
|
||||||
this.focusTrap.activate()
|
|
||||||
this.focusTrap.unpause()
|
|
||||||
document.body.classList.add('scrolling')
|
|
||||||
} else {
|
|
||||||
if (this.control) {
|
|
||||||
this.$emit('hide')
|
|
||||||
this.control.modal('hide')
|
|
||||||
this.control.remove()
|
|
||||||
this.focusTrap.deactivate()
|
|
||||||
this.focusTrap.pause()
|
|
||||||
document.body.classList.remove('scrolling')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -224,7 +224,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'activity',
|
path: '/activity',
|
||||||
name: `profile${route.suffix}.activity`,
|
name: `profile${route.suffix}.activity`,
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -318,7 +318,7 @@ export default new Router({
|
||||||
import(/* webpackChunkName: "admin" */ '@/views/admin/library/Base'),
|
import(/* webpackChunkName: "admin" */ '@/views/admin/library/Base'),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'edits',
|
path: '/edits',
|
||||||
name: 'manage.library.edits',
|
name: 'manage.library.edits',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -331,7 +331,7 @@ export default new Router({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'artists',
|
path: '/artists',
|
||||||
name: 'manage.library.artists',
|
name: 'manage.library.artists',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -344,7 +344,7 @@ export default new Router({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'artists/:id',
|
path: '/artists/:id',
|
||||||
name: 'manage.library.artists.detail',
|
name: 'manage.library.artists.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -353,7 +353,7 @@ export default new Router({
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'channels',
|
path: '/channels',
|
||||||
name: 'manage.channels',
|
name: 'manage.channels',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -366,7 +366,7 @@ export default new Router({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'channels/:id',
|
path: '/channels/:id',
|
||||||
name: 'manage.channels.detail',
|
name: 'manage.channels.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -375,7 +375,7 @@ export default new Router({
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'albums',
|
path: '/albums',
|
||||||
name: 'manage.library.albums',
|
name: 'manage.library.albums',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -388,7 +388,7 @@ export default new Router({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'albums/:id',
|
path: '/albums/:id',
|
||||||
name: 'manage.library.albums.detail',
|
name: 'manage.library.albums.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -397,7 +397,7 @@ export default new Router({
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tracks',
|
path: '/tracks',
|
||||||
name: 'manage.library.tracks',
|
name: 'manage.library.tracks',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -410,7 +410,7 @@ export default new Router({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tracks/:id',
|
path: '/tracks/:id',
|
||||||
name: 'manage.library.tracks.detail',
|
name: 'manage.library.tracks.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -419,7 +419,7 @@ export default new Router({
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'libraries',
|
path: '/libraries',
|
||||||
name: 'manage.library.libraries',
|
name: 'manage.library.libraries',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -432,7 +432,7 @@ export default new Router({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'libraries/:id',
|
path: '/libraries/:id',
|
||||||
name: 'manage.library.libraries.detail',
|
name: 'manage.library.libraries.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -441,7 +441,7 @@ export default new Router({
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'uploads',
|
path: '/uploads',
|
||||||
name: 'manage.library.uploads',
|
name: 'manage.library.uploads',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -454,7 +454,7 @@ export default new Router({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'uploads/:id',
|
path: '/uploads/:id',
|
||||||
name: 'manage.library.uploads.detail',
|
name: 'manage.library.uploads.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -463,7 +463,7 @@ export default new Router({
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tags',
|
path: '/tags',
|
||||||
name: 'manage.library.tags',
|
name: 'manage.library.tags',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -476,7 +476,7 @@ export default new Router({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tags/:id',
|
path: '/tags/:id',
|
||||||
name: 'manage.library.tags.detail',
|
name: 'manage.library.tags.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -493,7 +493,7 @@ export default new Router({
|
||||||
import(/* webpackChunkName: "admin" */ '@/views/admin/users/Base'),
|
import(/* webpackChunkName: "admin" */ '@/views/admin/users/Base'),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'users',
|
path: '/users',
|
||||||
name: 'manage.users.users.list',
|
name: 'manage.users.users.list',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -501,7 +501,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'invitations',
|
path: '/invitations',
|
||||||
name: 'manage.users.invitations.list',
|
name: 'manage.users.invitations.list',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -517,7 +517,7 @@ export default new Router({
|
||||||
import(/* webpackChunkName: "admin" */ '@/views/admin/moderation/Base'),
|
import(/* webpackChunkName: "admin" */ '@/views/admin/moderation/Base'),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'domains',
|
path: '/domains',
|
||||||
name: 'manage.moderation.domains.list',
|
name: 'manage.moderation.domains.list',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -525,7 +525,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'domains/:id',
|
path: '/domains/:id',
|
||||||
name: 'manage.moderation.domains.detail',
|
name: 'manage.moderation.domains.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -534,7 +534,7 @@ export default new Router({
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'accounts',
|
path: '/accounts',
|
||||||
name: 'manage.moderation.accounts.list',
|
name: 'manage.moderation.accounts.list',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -547,7 +547,7 @@ export default new Router({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'accounts/:id',
|
path: '/accounts/:id',
|
||||||
name: 'manage.moderation.accounts.detail',
|
name: 'manage.moderation.accounts.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -556,7 +556,7 @@ export default new Router({
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'reports',
|
path: '/reports',
|
||||||
name: 'manage.moderation.reports.list',
|
name: 'manage.moderation.reports.list',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -570,7 +570,7 @@ export default new Router({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'reports/:id',
|
path: '/reports/:id',
|
||||||
name: 'manage.moderation.reports.detail',
|
name: 'manage.moderation.reports.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -579,7 +579,7 @@ export default new Router({
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'requests',
|
path: '/requests',
|
||||||
name: 'manage.moderation.requests.list',
|
name: 'manage.moderation.requests.list',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -593,7 +593,7 @@ export default new Router({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'requests/:id',
|
path: '/requests/:id',
|
||||||
name: 'manage.moderation.requests.detail',
|
name: 'manage.moderation.requests.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -609,13 +609,13 @@ export default new Router({
|
||||||
import(/* webpackChunkName: "core" */ '@/components/library/Library'),
|
import(/* webpackChunkName: "core" */ '@/components/library/Library'),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '/',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "core" */ '@/components/library/Home'),
|
import(/* webpackChunkName: "core" */ '@/components/library/Home'),
|
||||||
name: 'library.index'
|
name: 'library.index'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'me',
|
path: '/me',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "core" */ '@/components/library/Home'),
|
import(/* webpackChunkName: "core" */ '@/components/library/Home'),
|
||||||
name: 'library.me',
|
name: 'library.me',
|
||||||
|
@ -624,7 +624,7 @@ export default new Router({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'artists/',
|
path: '/artists/',
|
||||||
name: 'library.artists.browse',
|
name: 'library.artists.browse',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -641,7 +641,7 @@ export default new Router({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'me/artists',
|
path: '/me/artists',
|
||||||
name: 'library.artists.me',
|
name: 'library.artists.me',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -659,7 +659,7 @@ export default new Router({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'albums/',
|
path: '/albums/',
|
||||||
name: 'library.albums.browse',
|
name: 'library.albums.browse',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -676,7 +676,7 @@ export default new Router({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'podcasts/',
|
path: '/podcasts/',
|
||||||
name: 'library.podcasts.browse',
|
name: 'library.podcasts.browse',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -693,7 +693,7 @@ export default new Router({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'me/albums',
|
path: '/me/albums',
|
||||||
name: 'library.albums.me',
|
name: 'library.albums.me',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -711,7 +711,7 @@ export default new Router({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'radios/',
|
path: '/radios/',
|
||||||
name: 'library.radios.browse',
|
name: 'library.radios.browse',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -725,7 +725,7 @@ export default new Router({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'me/radios/',
|
path: '/me/radios/',
|
||||||
name: 'library.radios.me',
|
name: 'library.radios.me',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -740,7 +740,7 @@ export default new Router({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'radios/build',
|
path: '/radios/build',
|
||||||
name: 'library.radios.build',
|
name: 'library.radios.build',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -749,7 +749,7 @@ export default new Router({
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'radios/build/:id',
|
path: '/radios/build/:id',
|
||||||
name: 'library.radios.edit',
|
name: 'library.radios.edit',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -758,14 +758,14 @@ export default new Router({
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'radios/:id',
|
path: '/radios/:id',
|
||||||
name: 'library.radios.detail',
|
name: 'library.radios.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "radios" */ '@/views/radios/Detail'),
|
import(/* webpackChunkName: "radios" */ '@/views/radios/Detail'),
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'playlists/',
|
path: '/playlists/',
|
||||||
name: 'library.playlists.browse',
|
name: 'library.playlists.browse',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "playlists" */ '@/views/playlists/List'),
|
import(/* webpackChunkName: "playlists" */ '@/views/playlists/List'),
|
||||||
|
@ -777,7 +777,7 @@ export default new Router({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'me/playlists/',
|
path: '/me/playlists/',
|
||||||
name: 'library.playlists.me',
|
name: 'library.playlists.me',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "playlists" */ '@/views/playlists/List'),
|
import(/* webpackChunkName: "playlists" */ '@/views/playlists/List'),
|
||||||
|
@ -790,7 +790,7 @@ export default new Router({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'playlists/:id',
|
path: '/playlists/:id',
|
||||||
name: 'library.playlists.detail',
|
name: 'library.playlists.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "playlists" */ '@/views/playlists/Detail'),
|
import(/* webpackChunkName: "playlists" */ '@/views/playlists/Detail'),
|
||||||
|
@ -800,7 +800,7 @@ export default new Router({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tags/:id',
|
path: '/tags/:id',
|
||||||
name: 'library.tags.detail',
|
name: 'library.tags.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -809,7 +809,7 @@ export default new Router({
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'artists/:id',
|
path: '/artists/:id',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "artists" */ '@/components/library/ArtistBase'
|
/* webpackChunkName: "artists" */ '@/components/library/ArtistBase'
|
||||||
|
@ -817,7 +817,7 @@ export default new Router({
|
||||||
props: true,
|
props: true,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '/',
|
||||||
name: 'library.artists.detail',
|
name: 'library.artists.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -825,7 +825,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'edit',
|
path: '/edit',
|
||||||
name: 'library.artists.edit',
|
name: 'library.artists.edit',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -833,7 +833,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'edit/:editId',
|
path: '/edit/:editId',
|
||||||
name: 'library.artists.edit.detail',
|
name: 'library.artists.edit.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -844,7 +844,7 @@ export default new Router({
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'albums/:id',
|
path: '/albums/:id',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "albums" */ '@/components/library/AlbumBase'
|
/* webpackChunkName: "albums" */ '@/components/library/AlbumBase'
|
||||||
|
@ -852,7 +852,7 @@ export default new Router({
|
||||||
props: true,
|
props: true,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '/',
|
||||||
name: 'library.albums.detail',
|
name: 'library.albums.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -860,7 +860,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'edit',
|
path: '/edit',
|
||||||
name: 'library.albums.edit',
|
name: 'library.albums.edit',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -868,7 +868,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'edit/:editId',
|
path: '/edit/:editId',
|
||||||
name: 'library.albums.edit.detail',
|
name: 'library.albums.edit.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -879,7 +879,7 @@ export default new Router({
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tracks/:id',
|
path: '/tracks/:id',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "tracks" */ '@/components/library/TrackBase'
|
/* webpackChunkName: "tracks" */ '@/components/library/TrackBase'
|
||||||
|
@ -887,7 +887,7 @@ export default new Router({
|
||||||
props: true,
|
props: true,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '/',
|
||||||
name: 'library.tracks.detail',
|
name: 'library.tracks.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -895,7 +895,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'edit',
|
path: '/edit',
|
||||||
name: 'library.tracks.edit',
|
name: 'library.tracks.edit',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -903,7 +903,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'edit/:editId',
|
path: '/edit/:editId',
|
||||||
name: 'library.tracks.edit.detail',
|
name: 'library.tracks.edit.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -914,7 +914,7 @@ export default new Router({
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'uploads/:id',
|
path: '/uploads/:id',
|
||||||
name: 'library.uploads.detail',
|
name: 'library.uploads.detail',
|
||||||
props: true,
|
props: true,
|
||||||
component: () =>
|
component: () =>
|
||||||
|
@ -924,7 +924,7 @@ export default new Router({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// browse a single library via it's uuid
|
// browse a single library via it's uuid
|
||||||
path: ':id([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})',
|
path: '/:id([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})',
|
||||||
props: true,
|
props: true,
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -932,7 +932,7 @@ export default new Router({
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '/',
|
||||||
name: 'library.detail',
|
name: 'library.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -940,7 +940,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'albums',
|
path: '/albums',
|
||||||
name: 'library.detail.albums',
|
name: 'library.detail.albums',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -948,7 +948,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tracks',
|
path: '/tracks',
|
||||||
name: 'library.detail.tracks',
|
name: 'library.detail.tracks',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -956,7 +956,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'edit',
|
path: '/edit',
|
||||||
name: 'library.detail.edit',
|
name: 'library.detail.edit',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -964,7 +964,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'upload',
|
path: '/upload',
|
||||||
name: 'library.detail.upload',
|
name: 'library.detail.upload',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -995,7 +995,7 @@ export default new Router({
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '/',
|
||||||
name: 'channels.detail',
|
name: 'channels.detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
@ -1003,7 +1003,7 @@ export default new Router({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'episodes',
|
path: '/episodes',
|
||||||
name: 'channels.detail.episodes',
|
name: 'channels.detail.episodes',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import(
|
||||||
|
|
|
@ -48,6 +48,7 @@ $bottom-player-height: 4rem;
|
||||||
@import "./components/_track_widget.scss";
|
@import "./components/_track_widget.scss";
|
||||||
@import "./components/_track_table.scss";
|
@import "./components/_track_table.scss";
|
||||||
@import "./components/_user_link.scss";
|
@import "./components/_user_link.scss";
|
||||||
|
@import "./components/user_modal.scss";
|
||||||
@import "./components/_volume_control.scss";
|
@import "./components/_volume_control.scss";
|
||||||
@import "./components/_loaders.scss";
|
@import "./components/_loaders.scss";
|
||||||
|
|
||||||
|
|
|
@ -214,6 +214,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ui.user-dropdown .ui.menu {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
.ui.user-dropdown>.text>.label {
|
.ui.user-dropdown>.text>.label {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
@ -234,4 +238,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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