Newer
Older
Eliot Berriot
committed
<template>
<aside :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar',]">
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<header class="ui basic segment header-wrapper">
<router-link :title="'Funkwhale'" :to="{name: logoUrl}">
<i class="logo bordered inverted orange big icon">
<logo class="logo"></logo>
</i>
</router-link>
<router-link v-if="!$store.state.auth.authenticated" class="logo-wrapper" :to="{name: logoUrl}">
<img src="../assets/logo/text-white.svg" />
</router-link>
<nav class="top ui compact right aligned inverted text menu">
<template v-if="$store.state.auth.authenticated">
<div class="right menu">
<div class="item" :title="labels.administration" v-if="$store.state.auth.availablePermissions['settings'] || $store.state.auth.availablePermissions['moderation']">
<div class="item ui inline admin-dropdown dropdown">
<i class="wrench icon"></i>
<div
v-if="$store.state.ui.notifications.pendingReviewEdits + $store.state.ui.notifications.pendingReviewReports > 0"
:class="['ui', 'teal', 'mini', 'bottom floating', 'circular', 'label']">{{ $store.state.ui.notifications.pendingReviewEdits + $store.state.ui.notifications.pendingReviewReports }}</div>
<div class="menu">
<div class="header">
<translate translate-context="Sidebar/Admin/Title/Noun">Administration</translate>
</div>
<div class="divider"></div>
<router-link
v-if="$store.state.auth.availablePermissions['library']"
class="item"
:to="{name: 'manage.library.edits', query: {q: 'is_approved:null'}}">
<div
v-if="$store.state.ui.notifications.pendingReviewEdits > 0"
:title="labels.pendingReviewEdits"
:class="['ui', 'circular', 'mini', 'right floated', 'teal', 'label']">
{{ $store.state.ui.notifications.pendingReviewEdits }}</div>
<translate translate-context="*/*/*/Noun">Library</translate>
</router-link>
<router-link
v-if="$store.state.auth.availablePermissions['moderation']"
class="item"
:to="{name: 'manage.moderation.reports.list', query: {q: 'resolved:no'}}">
<div
v-if="$store.state.ui.notifications.pendingReviewReports > 0"
:title="labels.pendingReviewReports"
:class="['ui', 'circular', 'mini', 'right floated', 'teal', 'label']">{{ $store.state.ui.notifications.pendingReviewReports }}</div>
<translate translate-context="*/Moderation/*">Moderation</translate>
</router-link>
<router-link
v-if="$store.state.auth.availablePermissions['settings']"
class="item"
:to="{name: 'manage.users.users.list'}">
<translate translate-context="*/*/*/Noun">Users</translate>
</router-link>
<router-link
v-if="$store.state.auth.availablePermissions['settings']"
class="item"
:to="{path: '/manage/settings'}">
<translate translate-context="*/*/*/Noun">Settings</translate>
</router-link>
</div>
</div>
</div>
</div>
<router-link
class="item"
v-if="$store.state.auth.authenticated"
:title="labels.addContent"
:to="{name: 'content.index'}"><i class="upload icon"></i></router-link>
Eliot Berriot
committed
<router-link class="item" v-if="$store.state.auth.authenticated" :title="labels.notifications" :to="{name: 'notifications'}">
<i class="bell icon"></i><div
v-if="$store.state.ui.notifications.inbox + additionalNotifications > 0"
:class="['ui', 'teal', 'mini', 'bottom floating', 'circular', 'label']">{{ $store.state.ui.notifications.inbox + additionalNotifications }}</div>
</router-link>
<div class="item">
<div class="ui user-dropdown dropdown" >
<img class="ui avatar image" v-if="$store.state.auth.profile.avatar.square_crop" :src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.square_crop)" />
<actor-avatar v-else :actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username}" />
<div class="menu">
<router-link class="item" :to="{name: 'profile', params: {username: $store.state.auth.username}}"><translate translate-context="*/*/*/Noun">Profile</translate></router-link>
<router-link class="item" :to="{path: '/settings'}"></i><translate translate-context="*/*/*/Noun">Settings</translate></router-link>
<router-link class="item" :to="{name: 'logout'}"></i><translate translate-context="Sidebar/Login/List item.Link/Verb">Logout</translate></router-link>
</div>
</div>
</div>
</template>
<div class="item collapse-button-wrapper">
<span
@click="isCollapsed = !isCollapsed"
:class="['ui', 'basic', 'big', {'orange': !isCollapsed}, 'inverted icon', 'collapse', 'button']">
<i class="sidebar icon"></i></span>
</div>
</nav>
</header>
<div class="ui basic search-wrapper segment">
<search-bar @search="isCollapsed = false"></search-bar>
</div>
<div v-if="!$store.state.auth.authenticated" class="ui basic signup segment">
<router-link class="ui fluid tiny primary button" :to="{name: 'login'}"><translate translate-context="*/Login/*/Verb">Login</translate></router-link>
<div class="ui small hidden divider"></div>
<router-link class="ui fluid tiny button" :to="{path: '/signup'}">
<translate translate-context="*/Signup/Link/Verb">Create an account</translate>
</router-link>
Eliot Berriot
committed
</div>
<nav class="secondary" role="navigation">
<div class="ui small hidden divider"></div>
<section :class="['ui', 'bottom', 'attached', {active: selectedTab === 'library'}, 'tab']" :aria-label="labels.mainMenu">
<nav class="ui vertical large fluid inverted menu" role="navigation" :aria-label="labels.mainMenu">
<div :class="[{collapsed: !exploreExpanded}, 'collaspable item']">
<header class="header" @click="exploreExpanded = true" tabindex="0" @focus="exploreExpanded = true">
<translate translate-context="*/*/*/Verb">Explore</translate>
<i class="angle right icon" v-if="!exploreExpanded"></i>
</header>
<router-link class="item" :exact="true" :to="{name: 'library.index'}"><i class="music icon"></i><translate translate-context="Sidebar/Navigation/List item.Link/Verb">Browse</translate></router-link>
<router-link class="item" :to="{name: 'library.albums.browse'}"><i class="compact disc icon"></i><translate translate-context="*/*/*">Albums</translate></router-link>
<router-link class="item" :to="{name: 'library.artists.browse'}"><i class="user icon"></i><translate translate-context="*/*/*">Artists</translate></router-link>
<router-link class="item" :to="{name: 'library.playlists.browse'}"><i class="list icon"></i><translate translate-context="*/*/*">Playlists</translate></router-link>
<router-link class="item" :to="{name: 'library.radios.browse'}"><i class="feed icon"></i><translate translate-context="*/*/*">Radios</translate></router-link>
<div :class="[{collapsed: !myLibraryExpanded}, 'collaspable item']" v-if="$store.state.auth.authenticated">
<header class="header" @click="myLibraryExpanded = true" tabindex="0" @focus="myLibraryExpanded = true">
<translate translate-context="*/*/*/Noun">My Library</translate>
<i class="angle right icon" v-if="!myLibraryExpanded"></i>
</header>
<router-link class="item" :exact="true" :to="{name: 'library.me'}"><i class="music icon"></i><translate translate-context="Sidebar/Navigation/List item.Link/Verb">Browse</translate></router-link>
<router-link class="item" :to="{name: 'library.albums.me'}"><i class="compact disc icon"></i><translate translate-context="*/*/*">Albums</translate></router-link>
<router-link class="item" :to="{name: 'library.artists.me'}"><i class="user icon"></i><translate translate-context="*/*/*">Artists</translate></router-link>
<router-link class="item" :to="{name: 'library.playlists.me'}"><i class="list icon"></i><translate translate-context="*/*/*">Playlists</translate></router-link>
<router-link class="item" :to="{name: 'library.radios.me'}"><i class="feed icon"></i><translate translate-context="*/*/*">Radios</translate></router-link>
<router-link class="item" :to="{name: 'favorites'}"><i class="heart icon"></i><translate translate-context="Sidebar/Favorites/List item.Link/Noun">Favorites</translate></router-link>
<div class="item">
<header class="header">
<translate translate-context="Footer/About/List item.Link">More</translate>
</header>
<router-link class="item" to="/about">
<i class="info icon"></i><translate translate-context="Sidebar/*/List item.Link">About this pod</translate>
</nav>
</section>
</aside>
Eliot Berriot
committed
</template>
<script>
import { mapState, mapActions, mapGetters } from "vuex"
import Logo from "@/components/Logo"
import SearchBar from "@/components/audio/SearchBar"
import backend from "@/audio/backend"
Eliot Berriot
committed
import $ from "jquery"
Eliot Berriot
committed
export default {
name: "sidebar",
Eliot Berriot
committed
components: {
SearchBar,
Eliot Berriot
committed
},
data() {
Eliot Berriot
committed
return {
selectedTab: "library",
backend: backend,
isCollapsed: true,
fetchInterval: null,
exploreExpanded: false,
myLibraryExpanded: false,
Eliot Berriot
committed
}
},
destroy() {
if (this.fetchInterval) {
clearInterval(this.fetchInterval)
}
},
mounted () {
this.$nextTick(() => {
document.getElementById('fake-sidebar').classList.add('loaded')
})
},
...mapGetters({
additionalNotifications: "ui/additionalNotifications",
}),
queue: state => state.queue,
url: state => state.route.path
labels() {
let mainMenu = this.$pgettext('Sidebar/*/Hidden text', "Main menu")
let selectTrack = this.$pgettext('Sidebar/Player/Hidden text', "Play this track")
let pendingFollows = this.$pgettext('Sidebar/Notifications/Hidden text', "Pending follow requests")
let pendingReviewEdits = this.$pgettext('Sidebar/Moderation/Hidden text', "Pending review edits")
pendingFollows,
mainMenu,
pendingReviewEdits,
addContent: this.$pgettext("*/Library/*/Verb", 'Add content'),
notifications: this.$pgettext("*/Notifications/*", 'Notifications'),
administration: this.$pgettext("Sidebar/Admin/Title/Noun", 'Administration'),
Eliot Berriot
committed
}
logoUrl() {
if (this.$store.state.auth.authenticated) {
return "library.index"
return "index"
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
},
focusedMenu () {
let mapping = {
"library.index": 'exploreExpanded',
"library.albums.browse": 'exploreExpanded',
"library.albums.detail": 'exploreExpanded',
"library.artists.browse": 'exploreExpanded',
"library.artists.detail": 'exploreExpanded',
"library.tracks.detail": 'exploreExpanded',
"library.playlists.browse": 'exploreExpanded',
"library.playlists.detail": 'exploreExpanded',
"library.radios.browse": 'exploreExpanded',
"library.radios.detail": 'exploreExpanded',
'library.me': "myLibraryExpanded",
'library.albums.me': "myLibraryExpanded",
'library.artists.me': "myLibraryExpanded",
'library.playlists.me': "myLibraryExpanded",
'library.radios.me': "myLibraryExpanded",
'favorites': "myLibraryExpanded",
}
let m = mapping[this.$route.name]
if (m) {
return m
}
if (this.$store.state.auth.authenticated) {
return 'myLibraryExpanded'
} else {
return 'exploreExpanded'
}
methods: {
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) {
let self = this
$(self.$el).find(selector).dropdown({
selectOnKeydown: false,
action: function (text, value, $el) {
// used ton ensure focusing the dropdown and clicking via keyboard
// works as expected
let link = $($el).closest('a')
let url = link.attr('href')
self.$router.push(url)
$(self.$el).find(selector).dropdown('hide')
}
})
},
watch: {
url: function() {
this.isCollapsed = true
Eliot Berriot
committed
},
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
"$store.state.moderation.lastUpdate": function () {
this.applyContentFilters()
},
"$store.state.auth.authenticated": {
immediate: true,
handler (v) {
if (v) {
this.$nextTick(() => {
this.setupDropdown('.user-dropdown')
})
}
}
},
"$store.state.auth.availablePermissions": {
immediate: true,
handler (v) {
this.$nextTick(() => {
this.setupDropdown('.admin-dropdown')
})
},
deep: true,
},
focusedMenu: {
immediate: true,
handler (n) {
if (n) {
this[n] = true
}
Eliot Berriot
committed
}
},
myLibraryExpanded (v) {
if (v) {
this.exploreExpanded = false
}
},
exploreExpanded (v) {
if (v) {
this.myLibraryExpanded = false
Eliot Berriot
committed
}
Eliot Berriot
committed
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
@import "../style/vendor/media";
Eliot Berriot
committed
$sidebar-color: #2D2F33;
Eliot Berriot
committed
.sidebar {
background: $sidebar-color;
@include media(">desktop") {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-bottom: 4em;
}
> nav {
flex-grow: 1;
overflow-y: auto;
}
@include media(">desktop") {
.menu .item.collapse-button-wrapper {
padding: 0;
}
.collapse.button {
display: none !important;
}
}
@include media("<=desktop") {
position: static !important;
width: 100% !important;
&.collapsed {
.player-wrapper,
.search,
.signup.segment,
nav.secondary {
display: none;
}
}
}
Eliot Berriot
committed
> div {
margin: 0;
background-color: $sidebar-color;
}
.menu.vertical {
background: $sidebar-color;
Eliot Berriot
committed
}
}
.ui.vertical.menu {
> i.icon {
float: none;
margin: 0 0.5em 0 0;
}
&:not(.active) {
// color: rgba(255, 255, 255, 0.75);
.item.active {
border-right: 5px solid #F2711C;
border-radius: 0 !important;
background-color: rgba(255, 255, 255, 0.15) !important;
}
.item.collapsed {
&:not(:focus) > .menu {
display: none;
}
.header {
margin-bottom: 0;
}
}
.collaspable.item .header {
cursor: pointer;
}
}
.ui.secondary.menu {
margin-left: 0;
margin-right: 0;
Eliot Berriot
committed
.tabs {
Eliot Berriot
committed
flex: 1;
display: flex;
flex-direction: column;
Eliot Berriot
committed
overflow-y: auto;
Eliot Berriot
committed
justify-content: space-between;
@include media("<=desktop") {
Eliot Berriot
committed
max-height: 500px;
Eliot Berriot
committed
}
Eliot Berriot
committed
.ui.tab.active {
display: flex;
}
Eliot Berriot
committed
.tab[data-tab="queue"] {
Eliot Berriot
committed
flex-direction: column;
Eliot Berriot
committed
tr {
cursor: pointer;
}
Eliot Berriot
committed
}
.item .header .angle.icon {
float: right;
margin: 0;
}
Eliot Berriot
committed
.tab[data-tab="library"] {
flex-direction: column;
flex: 1 1 auto;
> .menu {
flex: 1;
flex-grow: 1;
}
> .player-wrapper {
width: 100%;
}
}
Eliot Berriot
committed
.sidebar .segment {
margin: 0;
border-radius: 0;
}
.ui.menu .item.inline.admin-dropdown.dropdown > .menu {
left: 0;
right: auto;
}
.ui.segment.header-wrapper {
Eliot Berriot
committed
padding: 0;
display: flex;
justify-content: space-between;
align-items: center;
height: 4em;
nav {
> .item, > .menu > .item > .item {
&:hover {
background-color: transparent;
}
}
}
}
nav.top.title-menu {
flex-grow: 1;
.item {
font-size: 1.5em;
}
Eliot Berriot
committed
}
.logo {
cursor: pointer;
display: inline-block;
Eliot Berriot
committed
}
.collapsed .search-wrapper {
@include media("<desktop") {
padding: 0;
}
}
Eliot Berriot
committed
.ui.search {
Eliot Berriot
committed
}
.ui.message.black {
background: $sidebar-color;
}
.ui.mini.image {
width: 100%;
}
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
nav.top {
align-items: self-end;
padding: 0.5em 0;
> .item, > .right.menu > .item {
// color: rgba(255, 255, 255, 0.9) !important;
font-size: 1.2em;
&:hover, > .dropdown > .icon {
// color: rgba(255, 255, 255, 0.9) !important;
}
> .label, > .dropdown > .label {
font-size: 0.5em;
right: 1.7em;
bottom: -0.5em;
z-index: 0 !important;
}
}
}
.ui.user-dropdown > .text > .label {
margin-right: 0;
}
.logo-wrapper {
display: inline-block;
margin: 0 auto;
@include media("<desktop") {
margin: 0;
}
img {
height: 1em;
display: inline-block;
margin: 0 auto;
}
@include media(">tablet") {
img {
height: 1.5em;
}
}
}
aside.ui.sidebar {
overflow-y: visible !important;
.ui.search .input {
flex: 1;
.prompt {
border-radius: 0;
}
}
.ui.search .results {
vertical-align: middle;
}
.ui.search .name {
vertical-align: middle;
}
Eliot Berriot
committed
:not(.active) button.title {
outline-color: white;
}