diff --git a/front/scripts/fix-fomantic-css.py b/front/scripts/fix-fomantic-css.py index 80b864416..e2a9b6b0a 100755 --- a/front/scripts/fix-fomantic-css.py +++ b/front/scripts/fix-fomantic-css.py @@ -165,6 +165,16 @@ def discard_unused_icons(rule): ".wrench", ".x", ".key", + ".cog", + ".life.ring", + ".language", + ".palette", + ".sun", + ".moon", + ".gitlab", + ".chevron", + ".right", + ".left" ] if ":before" not in rule["lines"][0]: return False diff --git a/front/src/App.vue b/front/src/App.vue index 5ac09cb8e..2db0fbc26 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -1,35 +1,47 @@ @@ -37,28 +49,26 @@ import Vue from 'vue' import axios from 'axios' import _ from '@/lodash' -import {mapState, mapGetters, mapActions} from 'vuex' +import { mapState, mapGetters } from 'vuex' import { WebSocketBridge } from 'django-channels' import GlobalEvents from '@/components/utils/global-events' -import moment from 'moment' import locales from './locales' -import {getClientOnlyRadio} from '@/radios' +import { getClientOnlyRadio } from '@/radios' export default { - name: 'app', + name: 'App', components: { - Player: () => import(/* webpackChunkName: "audio" */ "@/components/audio/Player"), - Queue: () => import(/* webpackChunkName: "audio" */ "@/components/Queue"), - PlaylistModal: () => import(/* webpackChunkName: "auth-audio" */ "@/components/playlists/PlaylistModal"), - ChannelUploadModal: () => import(/* webpackChunkName: "auth-audio" */ "@/components/channels/UploadModal"), - Sidebar: () => import(/* webpackChunkName: "core" */ "@/components/Sidebar"), - AppFooter: () => import(/* webpackChunkName: "core" */ "@/components/Footer"), - ServiceMessages: () => import(/* webpackChunkName: "core" */ "@/components/ServiceMessages"), - SetInstanceModal: () => import(/* webpackChunkName: "core" */ "@/components/SetInstanceModal"), - ShortcutsModal: () => import(/* webpackChunkName: "core" */ "@/components/ShortcutsModal"), - FilterModal: () => import(/* webpackChunkName: "moderation" */ "@/components/moderation/FilterModal"), - ReportModal: () => import(/* webpackChunkName: "moderation" */ "@/components/moderation/ReportModal"), - GlobalEvents, + Player: () => import(/* webpackChunkName: "audio" */ '@/components/audio/Player'), + Queue: () => import(/* webpackChunkName: "audio" */ '@/components/Queue'), + PlaylistModal: () => import(/* webpackChunkName: "auth-audio" */ '@/components/playlists/PlaylistModal'), + ChannelUploadModal: () => import(/* webpackChunkName: "auth-audio" */ '@/components/channels/UploadModal'), + Sidebar: () => import(/* webpackChunkName: "core" */ '@/components/Sidebar'), + ServiceMessages: () => import(/* webpackChunkName: "core" */ '@/components/ServiceMessages'), + SetInstanceModal: () => import(/* webpackChunkName: "core" */ '@/components/SetInstanceModal'), + ShortcutsModal: () => import(/* webpackChunkName: "core" */ '@/components/ShortcutsModal'), + FilterModal: () => import(/* webpackChunkName: "moderation" */ '@/components/moderation/FilterModal'), + ReportModal: () => import(/* webpackChunkName: "moderation" */ '@/components/moderation/ReportModal'), + GlobalEvents }, data () { return { @@ -70,23 +80,168 @@ export default { width: window.innerWidth } }, + computed: { + ...mapState({ + messages: state => state.ui.messages, + nodeinfo: state => state.instance.nodeinfo, + playing: state => state.player.playing, + bufferProgress: state => state.player.bufferProgress, + isLoadingAudio: state => state.player.isLoadingAudio, + serviceWorker: state => state.ui.serviceWorker + }), + ...mapGetters({ + hasNext: 'queue/hasNext', + currentTrack: 'queue/currentTrack', + progress: 'player/progress' + }), + labels () { + const play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Play track') + const pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Pause track') + const next = this.$pgettext('Sidebar/Player/Icon.Tooltip', 'Next track') + const expandQueue = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Expand queue') + return { + play, + pause, + next, + expandQueue + } + }, + suggestedInstances () { + const instances = this.$store.state.instance.knownInstances.slice(0) + if (this.$store.state.instance.frontSettings.defaultServerUrl) { + let serverUrl = this.$store.state.instance.frontSettings.defaultServerUrl + if (!serverUrl.endsWith('/')) { + serverUrl = serverUrl + '/' + } + instances.push(serverUrl) + } + instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/') + return _.uniq(instances.filter((e) => { return e })) + }, + version () { + if (!this.nodeinfo) { + return null + } + return _.get(this.nodeinfo, 'software.version') + }, + customStylesheets () { + if (this.$store.state.instance.frontSettings) { + return this.$store.state.instance.frontSettings.additionalStylesheets || [] + } + return null + } + }, + watch: { + '$store.state.instance.instanceUrl' (v) { + this.$store.dispatch('instance/fetchSettings') + this.fetchNodeInfo() + }, + '$store.state.ui.theme': { + immediate: true, + handler (newValue, oldValue) { + const oldTheme = oldValue || 'light' + document.body.classList.remove(`theme-${oldTheme}`) + document.body.classList.add(`theme-${newValue}`) + } + }, + '$store.state.auth.authenticated' (newValue) { + if (!newValue) { + this.disconnect() + } else { + this.openWebsocket() + } + }, + '$store.state.ui.currentLanguage': { + immediate: true, + handler (newValue) { + const self = this + const htmlLocale = newValue.toLowerCase().replace('_', '-') + document.documentElement.setAttribute('lang', htmlLocale) + if (newValue === 'en_US') { + self.$language.current = 'noop' + self.$language.current = newValue + return self.$store.commit('ui/momentLocale', 'en') + } + import(/* webpackChunkName: "locale-[request]" */ `./translations/${newValue}.json`).then((response) => { + Vue.$translations[newValue] = response.default[newValue] + }).finally(() => { + // set current language twice, otherwise we seem to have a cache somewhere + // and rendering does not happen + self.$language.current = 'noop' + self.$language.current = newValue + }) + const momentLocale = newValue.replace('_', '-').toLowerCase() + import(/* webpackChunkName: "moment-locale-[request]" */ `moment/locale/${momentLocale}.js`).then(() => { + self.$store.commit('ui/momentLocale', momentLocale) + }).catch(() => { + console.log('No momentjs locale available for', momentLocale) + const shortLocale = momentLocale.split('-')[0] + import(/* webpackChunkName: "moment-locale-[request]" */ `moment/locale/${shortLocale}.js`).then(() => { + self.$store.commit('ui/momentLocale', shortLocale) + }).catch(() => { + console.log('No momentjs locale available for', shortLocale) + }) + }) + } + }, + currentTrack: { + immediate: true, + handler (newValue) { + this.updateDocumentTitle() + } + }, + '$store.state.ui.pageTitle': { + immediate: true, + handler (newValue) { + this.updateDocumentTitle() + } + }, + 'serviceWorker.updateAvailable': { + handler (v) { + if (!v) { + return + } + const self = this + this.$store.commit('ui/addMessage', { + content: this.$pgettext('App/Message/Paragraph', 'A new version of the app is available.'), + date: new Date(), + key: 'refreshApp', + displayTime: 0, + classActions: 'bottom attached opaque', + actions: [ + { + text: this.$pgettext('App/Message/Paragraph', 'Update'), + class: 'primary', + click: function () { + self.updateApp() + } + }, + { + text: this.$pgettext('App/Message/Paragraph', 'Later'), + class: 'basic' + } + ] + }) + }, + immediate: true + } + }, async created () { - if (navigator.serviceWorker) { navigator.serviceWorker.addEventListener( 'controllerchange', () => { - if (this.serviceWorker.refreshing) return; + if (this.serviceWorker.refreshing) return this.$store.commit('ui/serviceWorker', { refreshing: true }) - window.location.reload(); + window.location.reload() } - ); + ) } - window.addEventListener('resize', this.handleResize); - this.handleResize(); + window.addEventListener('resize', this.handleResize) + this.handleResize() this.openWebsocket() - let self = this + const self = this if (!this.$store.state.ui.selectedLanguage) { this.autodetectLanguage() } @@ -94,7 +249,7 @@ export default { // used to redraw ago dates every minute self.$store.commit('ui/computeLastDate') }, 1000 * 60) - const urlParams = new URLSearchParams(window.location.search); + const urlParams = new URLSearchParams(window.location.search) const serverUrl = urlParams.get('_server') if (serverUrl) { this.$store.commit('instance/instanceUrl', serverUrl) @@ -102,13 +257,12 @@ export default { const url = urlParams.get('_url') if (url) { this.$router.replace(url) - } - else if (!this.$store.state.instance.instanceUrl) { + } else if (!this.$store.state.instance.instanceUrl) { // we have several way to guess the API server url. By order of precedence: // 1. use the url provided in settings.json, if any // 2. use the url specified when building via VUE_APP_INSTANCE_URL // 3. use the current url - let defaultInstanceUrl = this.$store.state.instance.frontSettings.defaultServerUrl || process.env.VUE_APP_INSTANCE_URL || this.$store.getters['instance/defaultUrl']() + const defaultInstanceUrl = this.$store.state.instance.frontSettings.defaultServerUrl || process.env.VUE_APP_INSTANCE_URL || this.$store.getters['instance/defaultUrl']() this.$store.commit('instance/instanceUrl', defaultInstanceUrl) } else { // needed to trigger initialization of axios / service worker @@ -153,80 +307,78 @@ export default { }) }, mounted () { - let self = this + const self = this // slight hack to allow use to have internal links in tags // while preserving router behaviour document.documentElement.addEventListener('click', function (event) { - if (!event.target.matches('a.internal')) return; + if (!event.target.matches('a.internal')) return self.$router.push(event.target.getAttribute('href')) - event.preventDefault(); - }, false); + event.preventDefault() + }, false) this.$nextTick(() => { document.getElementById('fake-content').classList.add('loaded') }) - }, destroyed () { this.$store.commit('ui/removeWebsocketEventHandler', { eventName: 'inbox.item_added', - id: 'sidebarCount', + id: 'sidebarCount' }) this.$store.commit('ui/removeWebsocketEventHandler', { eventName: 'mutation.created', - id: 'sidebarReviewEditCount', + id: 'sidebarReviewEditCount' }) this.$store.commit('ui/removeWebsocketEventHandler', { eventName: 'mutation.updated', - id: 'sidebarReviewEditCount', + id: 'sidebarReviewEditCount' }) this.$store.commit('ui/removeWebsocketEventHandler', { eventName: 'mutation.updated', - id: 'sidebarPendingReviewReportCount', + id: 'sidebarPendingReviewReportCount' }) this.$store.commit('ui/removeWebsocketEventHandler', { eventName: 'user_request.created', - id: 'sidebarPendingReviewRequestCount', + id: 'sidebarPendingReviewRequestCount' }) this.$store.commit('ui/removeWebsocketEventHandler', { eventName: 'Listen', - id: 'handleListen', + id: 'handleListen' }) this.disconnect() }, methods: { incrementNotificationCountInSidebar (event) { - this.$store.commit('ui/incrementNotifications', {type: 'inbox', count: 1}) + this.$store.commit('ui/incrementNotifications', { type: 'inbox', count: 1 }) }, 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) { - this.$store.commit('ui/incrementNotifications', {type: 'pendingReviewReports', value: event.unresolved_count}) + this.$store.commit('ui/incrementNotifications', { type: 'pendingReviewReports', value: event.unresolved_count }) }, 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) { if (this.$store.state.radios.current && this.$store.state.radios.running) { - let current = this.$store.state.radios.current + const current = this.$store.state.radios.current if (current.clientOnly && current.type === 'account') { getClientOnlyRadio(current).handleListen(current, event, this.$store) } } }, async fetchNodeInfo () { - let response = await axios.get('instance/nodeinfo/2.0/') + const response = await axios.get('instance/nodeinfo/2.0/') this.$store.commit('instance/nodeinfo', response.data) }, autodetectLanguage () { - let userLanguage = navigator.language || navigator.userLanguage - let available = locales.locales.map(e => { return e.code }) - let self = this + const userLanguage = navigator.language || navigator.userLanguage + const available = locales.locales.map(e => { return e.code }) let candidate - let matching = available.filter((a) => { + const matching = available.filter((a) => { return userLanguage.replace('-', '_') === a }) - let almostMatching = available.filter((a) => { + const almostMatching = available.filter((a) => { return userLanguage.replace('-', '_').split('_')[0] === a.split('_')[0] }) if (matching.length > 0) { @@ -242,15 +394,15 @@ export default { if (!this.bridge) { return } - this.bridge.socket.close(1000, 'goodbye', {keepClosed: true}) + this.bridge.socket.close(1000, 'goodbye', { keepClosed: true }) }, openWebsocket () { if (!this.$store.state.auth.authenticated) { return } this.disconnect() - let self = this - let token = this.$store.state.auth.token + const self = this + const token = this.$store.state.auth.token // let token = 'test' const bridge = new WebSocketBridge() this.bridge = bridge @@ -260,7 +412,7 @@ export default { bridge.connect( url, [], - {reconnectInterval: 1000 * 60}) + { reconnectInterval: 1000 * 60 }) bridge.listen(function (event) { self.$store.dispatch('ui/websocketEvent', event) }) @@ -268,7 +420,7 @@ export default { console.log('Connected to WebSocket') }) }, - getTrackInformationText(track) { + getTrackInformationText (track) { const trackTitle = track.title const albumArtist = (track.album) ? track.album.artist.name : null const artistName = ( @@ -276,11 +428,12 @@ export default { const text = `♫ ${trackTitle} – ${artistName} ♫` return text }, - updateDocumentTitle() { - let parts = [] + updateDocumentTitle () { + const parts = [] const currentTrackPart = ( - (this.currentTrack) ? this.getTrackInformationText(this.currentTrack) - : null) + (this.currentTrack) + ? this.getTrackInformationText(this.currentTrack) + : null) if (currentTrackPart) { parts.push(currentTrackPart) } @@ -292,158 +445,13 @@ export default { }, updateApp () { - this.$store.commit('ui/serviceWorker', {updateAvailable: false}) - if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return; } - this.serviceWorker.registration.waiting.postMessage({command: 'skipWaiting'}) + this.$store.commit('ui/serviceWorker', { updateAvailable: false }) + if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return } + this.serviceWorker.registration.waiting.postMessage({ command: 'skipWaiting' }) }, - handleResize() { + handleResize () { this.width = window.innerWidth } - }, - computed: { - ...mapState({ - messages: state => state.ui.messages, - nodeinfo: state => state.instance.nodeinfo, - playing: state => state.player.playing, - bufferProgress: state => state.player.bufferProgress, - isLoadingAudio: state => state.player.isLoadingAudio, - serviceWorker: state => state.ui.serviceWorker, - }), - ...mapGetters({ - hasNext: "queue/hasNext", - currentTrack: 'queue/currentTrack', - progress: "player/progress", - }), - labels() { - let play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Play track") - let pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Pause track") - let next = this.$pgettext('Sidebar/Player/Icon.Tooltip', "Next track") - let expandQueue = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Expand queue") - return { - play, - pause, - next, - expandQueue, - } - }, - suggestedInstances () { - let instances = this.$store.state.instance.knownInstances.slice(0) - if (this.$store.state.instance.frontSettings.defaultServerUrl) { - let serverUrl = this.$store.state.instance.frontSettings.defaultServerUrl - if (!serverUrl.endsWith('/')) { - serverUrl = serverUrl + '/' - } - instances.push(serverUrl) - } - instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/') - return _.uniq(instances.filter((e) => {return e})) - }, - version () { - if (!this.nodeinfo) { - return null - } - return _.get(this.nodeinfo, 'software.version') - }, - customStylesheets () { - if (this.$store.state.instance.frontSettings) { - return this.$store.state.instance.frontSettings.additionalStylesheets || [] - } - }, - }, - watch: { - '$store.state.instance.instanceUrl' (v) { - this.$store.dispatch('instance/fetchSettings') - this.fetchNodeInfo() - }, - '$store.state.ui.theme': { - immediate: true, - handler (newValue, oldValue) { - let oldTheme = oldValue || 'light' - document.body.classList.remove(`theme-${oldTheme}`) - document.body.classList.add(`theme-${newValue}`) - }, - }, - '$store.state.auth.authenticated' (newValue) { - if (!newValue) { - this.disconnect() - } else { - this.openWebsocket() - } - }, - '$store.state.ui.currentLanguage': { - immediate: true, - handler(newValue) { - let self = this - let htmlLocale = newValue.toLowerCase().replace('_', '-') - document.documentElement.setAttribute('lang', htmlLocale); - if (newValue === 'en_US') { - self.$language.current = 'noop' - self.$language.current = newValue - return self.$store.commit('ui/momentLocale', 'en') - } - import(/* webpackChunkName: "locale-[request]" */ `./translations/${newValue}.json`).then((response) =>{ - Vue.$translations[newValue] = response.default[newValue] - }).finally(() => { - // set current language twice, otherwise we seem to have a cache somewhere - // and rendering does not happen - self.$language.current = 'noop' - self.$language.current = newValue - }) - let momentLocale = newValue.replace('_', '-').toLowerCase() - import(/* webpackChunkName: "moment-locale-[request]" */ `moment/locale/${momentLocale}.js`).then(() => { - self.$store.commit('ui/momentLocale', momentLocale) - }).catch(() => { - console.log('No momentjs locale available for', momentLocale) - let shortLocale = momentLocale.split('-')[0] - import(/* webpackChunkName: "moment-locale-[request]" */ `moment/locale/${shortLocale}.js`).then(() => { - self.$store.commit('ui/momentLocale', shortLocale) - }).catch(() => { - console.log('No momentjs locale available for', shortLocale) - }) - }) - } - }, - 'currentTrack': { - immediate: true, - handler(newValue) { - this.updateDocumentTitle() - }, - }, - '$store.state.ui.pageTitle': { - immediate: true, - handler(newValue) { - this.updateDocumentTitle() - }, - }, - 'serviceWorker.updateAvailable': { - handler (v) { - if (!v) { - return - } - let self = this - this.$store.commit('ui/addMessage', { - content: this.$pgettext("App/Message/Paragraph", "A new version of the app is available."), - date: new Date(), - key: 'refreshApp', - displayTime: 0, - classActions: 'bottom attached opaque', - actions: [ - { - text: this.$pgettext("App/Message/Paragraph", "Update"), - class: "primary", - click: function () { - self.updateApp() - }, - }, - { - text: this.$pgettext("App/Message/Paragraph", "Later"), - class: "basic", - } - ] - }) - }, - immediate: true, - } } } diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index 9713c3cdb..b3b2fa6fe 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -1,252 +1,575 @@ + diff --git a/front/src/components/audio/podcast/Modal.vue b/front/src/components/audio/podcast/Modal.vue index dae3275cc..e1de0cdb1 100644 --- a/front/src/components/audio/podcast/Modal.vue +++ b/front/src/components/audio/podcast/Modal.vue @@ -1,15 +1,14 @@ diff --git a/front/src/components/audio/track/Modal.vue b/front/src/components/audio/track/Modal.vue index dae3275cc..e1de0cdb1 100644 --- a/front/src/components/audio/track/Modal.vue +++ b/front/src/components/audio/track/Modal.vue @@ -1,15 +1,14 @@ diff --git a/front/src/components/auth/Settings.vue b/front/src/components/auth/Settings.vue index 046492fd7..d614cf4b3 100644 --- a/front/src/components/auth/Settings.vue +++ b/front/src/components/auth/Settings.vue @@ -1,174 +1,353 @@ diff --git a/front/src/components/common/UserModal.vue b/front/src/components/common/UserModal.vue new file mode 100644 index 000000000..7c0b27e4b --- /dev/null +++ b/front/src/components/common/UserModal.vue @@ -0,0 +1,239 @@ + + + + + diff --git a/front/src/components/semantic/Modal.vue b/front/src/components/semantic/Modal.vue index 223c0e730..a40511f64 100644 --- a/front/src/components/semantic/Modal.vue +++ b/front/src/components/semantic/Modal.vue @@ -1,9 +1,10 @@ @@ -13,15 +14,41 @@ import createFocusTrap from 'focus-trap' export default { props: { - show: {type: Boolean, required: true}, - fullscreen: {type: Boolean, default: true}, - scrolling: {type: Boolean, required: false, default: false}, - additionalClasses: {type: Array, required: false, default: () => []} + show: { type: Boolean, required: true }, + fullscreen: { type: Boolean, default: true }, + scrolling: { type: Boolean, required: false, default: false }, + additionalClasses: { type: Array, required: false, default: () => [] } }, data () { return { control: null, - focusTrap: null, + focusTrap: null + } + }, + watch: { + show: { + handler (newValue) { + if (newValue) { + this.initModal() + this.$emit('show') + this.control.modal('show') + this.focusTrap.activate() + this.focusTrap.unpause() + document.body.classList.add('scrolling') + } else { + if (this.control) { + this.$emit('hide') + this.control.modal('hide') + this.control.remove() + this.focusTrap.deactivate() + this.focusTrap.pause() + document.body.classList.remove('scrolling') + } + } + } + }, + $route (to, from) { + this.closeModal() } }, mounted () { @@ -52,29 +79,9 @@ export default { this.focusTrap.unpause() }.bind(this) }) - } - }, - watch: { - show: { - handler (newValue) { - if (newValue) { - this.initModal() - this.$emit('show') - this.control.modal('show') - this.focusTrap.activate() - this.focusTrap.unpause() - document.body.classList.add('scrolling') - } else { - if (this.control) { - this.$emit('hide') - this.control.modal('hide') - this.control.remove() - this.focusTrap.deactivate() - this.focusTrap.pause() - document.body.classList.remove('scrolling') - } - } - } + }, + closeModal () { + this.$emit('update:show', false) } } diff --git a/front/src/router/index.js b/front/src/router/index.js index 35ce8be5d..210f0aa43 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -224,7 +224,7 @@ export default new Router({ ) }, { - path: 'activity', + path: '/activity', name: `profile${route.suffix}.activity`, component: () => import( @@ -318,7 +318,7 @@ export default new Router({ import(/* webpackChunkName: "admin" */ '@/views/admin/library/Base'), children: [ { - path: 'edits', + path: '/edits', name: 'manage.library.edits', component: () => import( @@ -331,7 +331,7 @@ export default new Router({ } }, { - path: 'artists', + path: '/artists', name: 'manage.library.artists', component: () => import( @@ -344,7 +344,7 @@ export default new Router({ } }, { - path: 'artists/:id', + path: '/artists/:id', name: 'manage.library.artists.detail', component: () => import( @@ -353,7 +353,7 @@ export default new Router({ props: true }, { - path: 'channels', + path: '/channels', name: 'manage.channels', component: () => import( @@ -366,7 +366,7 @@ export default new Router({ } }, { - path: 'channels/:id', + path: '/channels/:id', name: 'manage.channels.detail', component: () => import( @@ -375,7 +375,7 @@ export default new Router({ props: true }, { - path: 'albums', + path: '/albums', name: 'manage.library.albums', component: () => import( @@ -388,7 +388,7 @@ export default new Router({ } }, { - path: 'albums/:id', + path: '/albums/:id', name: 'manage.library.albums.detail', component: () => import( @@ -397,7 +397,7 @@ export default new Router({ props: true }, { - path: 'tracks', + path: '/tracks', name: 'manage.library.tracks', component: () => import( @@ -410,7 +410,7 @@ export default new Router({ } }, { - path: 'tracks/:id', + path: '/tracks/:id', name: 'manage.library.tracks.detail', component: () => import( @@ -419,7 +419,7 @@ export default new Router({ props: true }, { - path: 'libraries', + path: '/libraries', name: 'manage.library.libraries', component: () => import( @@ -432,7 +432,7 @@ export default new Router({ } }, { - path: 'libraries/:id', + path: '/libraries/:id', name: 'manage.library.libraries.detail', component: () => import( @@ -441,7 +441,7 @@ export default new Router({ props: true }, { - path: 'uploads', + path: '/uploads', name: 'manage.library.uploads', component: () => import( @@ -454,7 +454,7 @@ export default new Router({ } }, { - path: 'uploads/:id', + path: '/uploads/:id', name: 'manage.library.uploads.detail', component: () => import( @@ -463,7 +463,7 @@ export default new Router({ props: true }, { - path: 'tags', + path: '/tags', name: 'manage.library.tags', component: () => import( @@ -476,7 +476,7 @@ export default new Router({ } }, { - path: 'tags/:id', + path: '/tags/:id', name: 'manage.library.tags.detail', component: () => import( @@ -493,7 +493,7 @@ export default new Router({ import(/* webpackChunkName: "admin" */ '@/views/admin/users/Base'), children: [ { - path: 'users', + path: '/users', name: 'manage.users.users.list', component: () => import( @@ -501,7 +501,7 @@ export default new Router({ ) }, { - path: 'invitations', + path: '/invitations', name: 'manage.users.invitations.list', component: () => import( @@ -517,7 +517,7 @@ export default new Router({ import(/* webpackChunkName: "admin" */ '@/views/admin/moderation/Base'), children: [ { - path: 'domains', + path: '/domains', name: 'manage.moderation.domains.list', component: () => import( @@ -525,7 +525,7 @@ export default new Router({ ) }, { - path: 'domains/:id', + path: '/domains/:id', name: 'manage.moderation.domains.detail', component: () => import( @@ -534,7 +534,7 @@ export default new Router({ props: true }, { - path: 'accounts', + path: '/accounts', name: 'manage.moderation.accounts.list', component: () => import( @@ -547,7 +547,7 @@ export default new Router({ } }, { - path: 'accounts/:id', + path: '/accounts/:id', name: 'manage.moderation.accounts.detail', component: () => import( @@ -556,7 +556,7 @@ export default new Router({ props: true }, { - path: 'reports', + path: '/reports', name: 'manage.moderation.reports.list', component: () => import( @@ -570,7 +570,7 @@ export default new Router({ } }, { - path: 'reports/:id', + path: '/reports/:id', name: 'manage.moderation.reports.detail', component: () => import( @@ -579,7 +579,7 @@ export default new Router({ props: true }, { - path: 'requests', + path: '/requests', name: 'manage.moderation.requests.list', component: () => import( @@ -593,7 +593,7 @@ export default new Router({ } }, { - path: 'requests/:id', + path: '/requests/:id', name: 'manage.moderation.requests.detail', component: () => import( @@ -609,13 +609,13 @@ export default new Router({ import(/* webpackChunkName: "core" */ '@/components/library/Library'), children: [ { - path: '', + path: '/', component: () => import(/* webpackChunkName: "core" */ '@/components/library/Home'), name: 'library.index' }, { - path: 'me', + path: '/me', component: () => import(/* webpackChunkName: "core" */ '@/components/library/Home'), name: 'library.me', @@ -624,7 +624,7 @@ export default new Router({ }) }, { - path: 'artists/', + path: '/artists/', name: 'library.artists.browse', component: () => import( @@ -641,7 +641,7 @@ export default new Router({ }) }, { - path: 'me/artists', + path: '/me/artists', name: 'library.artists.me', component: () => import( @@ -659,7 +659,7 @@ export default new Router({ }) }, { - path: 'albums/', + path: '/albums/', name: 'library.albums.browse', component: () => import( @@ -676,7 +676,7 @@ export default new Router({ }) }, { - path: 'podcasts/', + path: '/podcasts/', name: 'library.podcasts.browse', component: () => import( @@ -693,7 +693,7 @@ export default new Router({ }) }, { - path: 'me/albums', + path: '/me/albums', name: 'library.albums.me', component: () => import( @@ -711,7 +711,7 @@ export default new Router({ }) }, { - path: 'radios/', + path: '/radios/', name: 'library.radios.browse', component: () => import( @@ -725,7 +725,7 @@ export default new Router({ }) }, { - path: 'me/radios/', + path: '/me/radios/', name: 'library.radios.me', component: () => import( @@ -740,7 +740,7 @@ export default new Router({ }) }, { - path: 'radios/build', + path: '/radios/build', name: 'library.radios.build', component: () => import( @@ -749,7 +749,7 @@ export default new Router({ props: true }, { - path: 'radios/build/:id', + path: '/radios/build/:id', name: 'library.radios.edit', component: () => import( @@ -758,14 +758,14 @@ export default new Router({ props: true }, { - path: 'radios/:id', + path: '/radios/:id', name: 'library.radios.detail', component: () => import(/* webpackChunkName: "radios" */ '@/views/radios/Detail'), props: true }, { - path: 'playlists/', + path: '/playlists/', name: 'library.playlists.browse', component: () => import(/* webpackChunkName: "playlists" */ '@/views/playlists/List'), @@ -777,7 +777,7 @@ export default new Router({ }) }, { - path: 'me/playlists/', + path: '/me/playlists/', name: 'library.playlists.me', component: () => import(/* webpackChunkName: "playlists" */ '@/views/playlists/List'), @@ -790,7 +790,7 @@ export default new Router({ }) }, { - path: 'playlists/:id', + path: '/playlists/:id', name: 'library.playlists.detail', component: () => import(/* webpackChunkName: "playlists" */ '@/views/playlists/Detail'), @@ -800,7 +800,7 @@ export default new Router({ }) }, { - path: 'tags/:id', + path: '/tags/:id', name: 'library.tags.detail', component: () => import( @@ -809,7 +809,7 @@ export default new Router({ props: true }, { - path: 'artists/:id', + path: '/artists/:id', component: () => import( /* webpackChunkName: "artists" */ '@/components/library/ArtistBase' @@ -817,7 +817,7 @@ export default new Router({ props: true, children: [ { - path: '', + path: '/', name: 'library.artists.detail', component: () => import( @@ -825,7 +825,7 @@ export default new Router({ ) }, { - path: 'edit', + path: '/edit', name: 'library.artists.edit', component: () => import( @@ -833,7 +833,7 @@ export default new Router({ ) }, { - path: 'edit/:editId', + path: '/edit/:editId', name: 'library.artists.edit.detail', component: () => import( @@ -844,7 +844,7 @@ export default new Router({ ] }, { - path: 'albums/:id', + path: '/albums/:id', component: () => import( /* webpackChunkName: "albums" */ '@/components/library/AlbumBase' @@ -852,7 +852,7 @@ export default new Router({ props: true, children: [ { - path: '', + path: '/', name: 'library.albums.detail', component: () => import( @@ -860,7 +860,7 @@ export default new Router({ ) }, { - path: 'edit', + path: '/edit', name: 'library.albums.edit', component: () => import( @@ -868,7 +868,7 @@ export default new Router({ ) }, { - path: 'edit/:editId', + path: '/edit/:editId', name: 'library.albums.edit.detail', component: () => import( @@ -879,7 +879,7 @@ export default new Router({ ] }, { - path: 'tracks/:id', + path: '/tracks/:id', component: () => import( /* webpackChunkName: "tracks" */ '@/components/library/TrackBase' @@ -887,7 +887,7 @@ export default new Router({ props: true, children: [ { - path: '', + path: '/', name: 'library.tracks.detail', component: () => import( @@ -895,7 +895,7 @@ export default new Router({ ) }, { - path: 'edit', + path: '/edit', name: 'library.tracks.edit', component: () => import( @@ -903,7 +903,7 @@ export default new Router({ ) }, { - path: 'edit/:editId', + path: '/edit/:editId', name: 'library.tracks.edit.detail', component: () => import( @@ -914,7 +914,7 @@ export default new Router({ ] }, { - path: 'uploads/:id', + path: '/uploads/:id', name: 'library.uploads.detail', props: true, component: () => @@ -924,7 +924,7 @@ export default new Router({ }, { // browse a single library via it's uuid - path: ':id([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})', + path: '/:id([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})', props: true, component: () => import( @@ -932,7 +932,7 @@ export default new Router({ ), children: [ { - path: '', + path: '/', name: 'library.detail', component: () => import( @@ -940,7 +940,7 @@ export default new Router({ ) }, { - path: 'albums', + path: '/albums', name: 'library.detail.albums', component: () => import( @@ -948,7 +948,7 @@ export default new Router({ ) }, { - path: 'tracks', + path: '/tracks', name: 'library.detail.tracks', component: () => import( @@ -956,7 +956,7 @@ export default new Router({ ) }, { - path: 'edit', + path: '/edit', name: 'library.detail.edit', component: () => import( @@ -964,7 +964,7 @@ export default new Router({ ) }, { - path: 'upload', + path: '/upload', name: 'library.detail.upload', component: () => import( @@ -995,7 +995,7 @@ export default new Router({ ), children: [ { - path: '', + path: '/', name: 'channels.detail', component: () => import( @@ -1003,7 +1003,7 @@ export default new Router({ ) }, { - path: 'episodes', + path: '/episodes', name: 'channels.detail.episodes', component: () => import( diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss index b867d2741..217ee30ce 100644 --- a/front/src/style/_main.scss +++ b/front/src/style/_main.scss @@ -48,6 +48,7 @@ $bottom-player-height: 4rem; @import "./components/_track_widget.scss"; @import "./components/_track_table.scss"; @import "./components/_user_link.scss"; +@import "./components/user_modal.scss"; @import "./components/_volume_control.scss"; @import "./components/_loaders.scss"; diff --git a/front/src/style/components/_sidebar.scss b/front/src/style/components/_sidebar.scss index 99867533e..ada61d833 100644 --- a/front/src/style/components/_sidebar.scss +++ b/front/src/style/components/_sidebar.scss @@ -214,6 +214,10 @@ } } } + .ui.user-dropdown .ui.menu { + left: auto; + right: 0; + } .ui.user-dropdown>.text>.label { margin-right: 0; } @@ -234,4 +238,4 @@ } } } -} \ No newline at end of file +} diff --git a/front/src/style/components/_user_modal.scss b/front/src/style/components/_user_modal.scss new file mode 100644 index 000000000..9fc87aa0e --- /dev/null +++ b/front/src/style/components/_user_modal.scss @@ -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; + } +} \ No newline at end of file