Merge branch '612-semantic-elements' into 'develop'

Fix #612: Improved accessibility by using main/section/nav tags and aria-labels…

Closes #612

See merge request funkwhale/funkwhale!471
This commit is contained in:
Eliot Berriot 2018-11-19 22:39:16 +00:00
commit c0904ca8b8
42 changed files with 1266 additions and 1122 deletions

View File

@ -0,0 +1 @@
Improved accessibility by using main/section/nav tags and aria-labels in most critical places (#612)

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="main pusher" v-title="labels.title"> <main class="main pusher" v-title="labels.title">
<div class="ui vertical center aligned stripe segment"> <section class="ui vertical center aligned stripe segment">
<div class="ui text container"> <div class="ui text container">
<h1 class="ui huge header"> <h1 class="ui huge header">
<translate v-if="instance.name.value" :translate-params="{instance: instance.name.value}"> <translate v-if="instance.name.value" :translate-params="{instance: instance.name.value}">
@ -10,8 +10,8 @@
</h1> </h1>
<stats></stats> <stats></stats>
</div> </div>
</div> </section>
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<p v-if="!instance.short_description.value && !instance.long_description.value"> <p v-if="!instance.short_description.value && !instance.long_description.value">
<translate>Unfortunately, owners of this instance did not yet take the time to complete this page.</translate> <translate>Unfortunately, owners of this instance did not yet take the time to complete this page.</translate>
</p> </p>
@ -31,20 +31,20 @@
class="ui middle aligned stackable text container" class="ui middle aligned stackable text container"
v-html="$options.filters.markdown(instance.long_description.value)"> v-html="$options.filters.markdown(instance.long_description.value)">
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import {mapState} from 'vuex' import { mapState } from "vuex"
import Stats from '@/components/instance/Stats' import Stats from "@/components/instance/Stats"
export default { export default {
components: { components: {
Stats Stats
}, },
created() { created() {
this.$store.dispatch('instance/fetchSettings') this.$store.dispatch("instance/fetchSettings")
}, },
computed: { computed: {
...mapState({ ...mapState({
@ -52,7 +52,7 @@ export default {
}), }),
labels() { labels() {
return { return {
title: this.$gettext('About this instance') title: this.$gettext("About this instance")
} }
} }
} }

View File

@ -1,8 +1,8 @@
<template> <template>
<footer id="footer" class="ui vertical footer segment"> <footer id="footer" role="contentinfo" class="ui vertical footer segment">
<div class="ui container"> <div class="ui container">
<div class="ui stackable equal height stackable grid"> <div class="ui stackable equal height stackable grid">
<div class="four wide column"> <section class="four wide column">
<h4 v-translate class="ui header"> <h4 v-translate class="ui header">
<translate :translate-params="{instanceName: instanceHostname}" >About %{instanceName}</translate> <translate :translate-params="{instanceName: instanceHostname}" >About %{instanceName}</translate>
</h4> </h4>
@ -25,24 +25,24 @@
</select> </select>
</div> </div>
</div> </div>
</div> </section>
<div class="four wide column"> <section class="four wide column">
<h4 v-translate class="ui header">Using Funkwhale</h4> <h4 v-translate class="ui header">Using Funkwhale</h4>
<div class="ui link list"> <div class="ui link list">
<a href="https://docs.funkwhale.audio" class="item" target="_blank"><translate>Documentation</translate></a> <a href="https://docs.funkwhale.audio" class="item" target="_blank"><translate>Documentation</translate></a>
<a href="https://docs.funkwhale.audio/users/apps.html" class="item" target="_blank"><translate>Mobile and desktop apps</translate></a> <a href="https://docs.funkwhale.audio/users/apps.html" class="item" target="_blank"><translate>Mobile and desktop apps</translate></a>
<div role="button" class="item" @click="$emit('show:shortcuts-modal')"><translate>Keyboard shortcuts</translate></div> <div role="button" class="item" @click="$emit('show:shortcuts-modal')"><translate>Keyboard shortcuts</translate></div>
</div> </div>
</div> </section>
<div class="four wide column"> <section class="four wide column">
<h4 v-translate class="ui header">Getting help</h4> <h4 v-translate class="ui header">Getting help</h4>
<div class="ui link list"> <div class="ui link list">
<a href="https://socialhub.network/c/projects/funkwhale" class="item" target="_blank"><translate>Support forum</translate></a> <a href="https://socialhub.network/c/projects/funkwhale" class="item" target="_blank"><translate>Support forum</translate></a>
<a href="https://riot.im/app/#/room/#funkwhale-troubleshooting:matrix.org" class="item" target="_blank"><translate>Chat room</translate></a> <a href="https://riot.im/app/#/room/#funkwhale-troubleshooting:matrix.org" class="item" target="_blank"><translate>Chat room</translate></a>
<a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank"><translate>Issue tracker</translate></a> <a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank"><translate>Issue tracker</translate></a>
</div> </div>
</div> </section>
<div class="four wide column"> <section class="four wide column">
<h4 v-translate class="ui header">About Funkwhale</h4> <h4 v-translate class="ui header">About Funkwhale</h4>
<div class="ui link list"> <div class="ui link list">
<a href="https://funkwhale.audio" class="item" target="_blank"><translate>Official website</translate></a> <a href="https://funkwhale.audio" class="item" target="_blank"><translate>Official website</translate></a>
@ -53,25 +53,28 @@
<p> <p>
<translate>The funkwhale logo was kindly designed and provided by Francis Gading.</translate> <translate>The funkwhale logo was kindly designed and provided by Francis Gading.</translate>
</p> </p>
</div> </section>
</div> </div>
</div> </div>
</footer> </footer>
</template> </template>
<script> <script>
import {mapState} from 'vuex' import { mapState } from "vuex"
export default { export default {
props: ['version'], props: ["version"],
methods: { methods: {
switchInstance() { switchInstance() {
let confirm = window.confirm(this.$gettext('This will erase your local data and disconnect you, do you want to continue?')) let confirm = window.confirm(
this.$gettext(
"This will erase your local data and disconnect you, do you want to continue?"
)
)
if (confirm) { if (confirm) {
this.$store.commit('instance/instanceUrl', null) this.$store.commit("instance/instanceUrl", null)
}
} }
},
}, },
computed: { computed: {
...mapState({ ...mapState({
@ -79,14 +82,17 @@ export default {
}), }),
instanceHostname() { instanceHostname() {
let url = this.$store.state.instance.instanceUrl let url = this.$store.state.instance.instanceUrl
let parser = document.createElement('a'); let parser = document.createElement("a")
parser.href = url parser.href = url
return parser.hostname return parser.hostname
}, },
suggestedInstances() { suggestedInstances() {
let instances = [this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio'] let instances = [
this.$store.getters["instance/defaultUrl"](),
"https://demo.funkwhale.audio"
]
return instances return instances
}, }
} }
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="main pusher" v-title="labels.title"> <main class="main pusher" v-title="labels.title">
<div class="ui vertical center aligned stripe segment"> <section class="ui vertical center aligned stripe segment">
<div class="ui text container"> <div class="ui text container">
<h1 class="ui huge header"> <h1 class="ui huge header">
<translate>Welcome on Funkwhale</translate> <translate>Welcome on Funkwhale</translate>
@ -15,8 +15,8 @@
<i class="right arrow icon"></i> <i class="right arrow icon"></i>
</router-link> </router-link>
</div> </div>
</div> </section>
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div class="ui middle aligned stackable text container"> <div class="ui middle aligned stackable text container">
<div class="ui grid"> <div class="ui grid">
<div class="row"> <div class="row">
@ -136,22 +136,21 @@
</div> </div>
</div> </div>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
musicbrainzUrl: 'https://musicbrainz.org/' musicbrainzUrl: "https://musicbrainz.org/"
} }
}, },
computed: { computed: {
labels() { labels() {
return { return {
title: this.$gettext('Welcome') title: this.$gettext("Welcome")
} }
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="main pusher" :v-title="labels.title"> <main class="main pusher" :v-title="labels.title">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div class="ui text container"> <div class="ui text container">
<h1 class="ui huge header"> <h1 class="ui huge header">
<i class="warning icon"></i> <i class="warning icon"></i>
@ -16,8 +16,8 @@
<i class="right arrow icon"></i> <i class="right arrow icon"></i>
</router-link> </router-link>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
@ -30,7 +30,7 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
title: this.$gettext('Page Not Found') title: this.$gettext("Page Not Found")
} }
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="ui pagination menu"> <div class="ui pagination menu" role="navigation" :aria-label="labels.pagination">
<a href <a href
:disabled="current - 1 < 1" :disabled="current - 1 < 1"
@click.prevent.stop="selectPage(current - 1)" @click.prevent.stop="selectPage(current - 1)"
@ -24,7 +24,7 @@
</template> </template>
<script> <script>
import _ from 'lodash' import _ from "lodash"
export default { export default {
props: { props: {
@ -34,20 +34,32 @@ export default {
compact: { type: Boolean, default: false } compact: { type: Boolean, default: false }
}, },
computed: { computed: {
labels() {
return {
pagination: this.$gettext("Pagination")
}
},
pages: function() { pages: function() {
let range = 2 let range = 2
let current = this.current let current = this.current
let beginning = _.range(1, Math.min(this.maxPage, 1 + range)) let beginning = _.range(1, Math.min(this.maxPage, 1 + range))
let middle = _.range(Math.max(1, current - range + 1), Math.min(this.maxPage, current + range)) let middle = _.range(
Math.max(1, current - range + 1),
Math.min(this.maxPage, current + range)
)
let end = _.range(this.maxPage, Math.max(1, this.maxPage - range)) let end = _.range(this.maxPage, Math.max(1, this.maxPage - range))
let allowed = beginning.concat(middle, end) let allowed = beginning.concat(middle, end)
allowed = _.uniq(allowed) allowed = _.uniq(allowed)
allowed = _.sortBy(allowed, [(e) => { return e }]) allowed = _.sortBy(allowed, [
e => {
return e
}
])
let final = [] let final = []
allowed.forEach(p => { allowed.forEach(p => {
let last = final.slice(-1)[0] let last = final.slice(-1)[0]
let consecutive = true let consecutive = true
if (last === 'skip') { if (last === "skip") {
consecutive = false consecutive = false
} else { } else {
if (!last) { if (!last) {
@ -59,8 +71,8 @@ export default {
if (consecutive) { if (consecutive) {
final.push(p) final.push(p)
} else { } else {
if (p !== 'skip') { if (p !== "skip") {
final.push('skip') final.push("skip")
final.push(p) final.push(p)
} }
} }
@ -77,7 +89,7 @@ export default {
return return
} }
if (this.current !== page) { if (this.current !== page) {
this.$emit('page-changed', page) this.$emit("page-changed", page)
} }
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar',]"> <aside :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar',]">
<div class="ui inverted segment header-wrapper"> <header class="ui inverted segment header-wrapper">
<search-bar @search="isCollapsed = false"> <search-bar @search="isCollapsed = false">
<router-link :title="'Funkwhale'" :to="{name: logoUrl}"> <router-link :title="'Funkwhale'" :to="{name: logoUrl}">
<i class="logo bordered inverted orange big icon"> <i class="logo bordered inverted orange big icon">
@ -12,12 +12,12 @@
:class="['ui', 'basic', 'big', {'inverted': isCollapsed}, 'orange', 'icon', 'collapse', 'button']"> :class="['ui', 'basic', 'big', {'inverted': isCollapsed}, 'orange', 'icon', 'collapse', 'button']">
<i class="sidebar icon"></i></span> <i class="sidebar icon"></i></span>
</search-bar> </search-bar>
</div> </header>
<div class="menu-area"> <div class="menu-area">
<div class="ui compact fluid two item inverted menu"> <div class="ui compact fluid two item inverted menu">
<a class="active item" href @click.prevent.stop="selectedTab = 'library'" data-tab="library"><translate>Browse</translate></a> <a class="active item" role="button" @click.prevent.stop="selectedTab = 'library'" data-tab="library"><translate>Browse</translate></a>
<a class="item" href @click.prevent.stop="selectedTab = 'queue'" data-tab="queue"> <a class="item" role="button" @click.prevent.stop="selectedTab = 'queue'" data-tab="queue">
<translate>Queue</translate>&nbsp; <translate>Queue</translate>&nbsp;
<template v-if="queue.tracks.length === 0"> <template v-if="queue.tracks.length === 0">
<translate>(empty)</translate> <translate>(empty)</translate>
@ -29,10 +29,10 @@
</div> </div>
</div> </div>
<div class="tabs"> <div class="tabs">
<div class="ui bottom attached active tab" data-tab="library"> <section class="ui bottom attached active tab" data-tab="library" :aria-label="labels.mainMenu">
<div class="ui inverted vertical large fluid menu"> <nav class="ui inverted vertical large fluid menu" role="navigation" :aria-label="labels.mainMenu">
<div class="item"> <div class="item">
<div class="header"><translate>My account</translate></div> <header class="header"><translate>My account</translate></header>
<div class="menu"> <div class="menu">
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'profile', params: {username: $store.state.auth.username}}"> <router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'profile', params: {username: $store.state.auth.username}}">
<i class="user icon"></i> <i class="user icon"></i>
@ -61,7 +61,7 @@
</div> </div>
</div> </div>
<div class="item"> <div class="item">
<div class="header"><translate>Music</translate></div> <header class="header"><translate>Music</translate></header>
<div class="menu"> <div class="menu">
<router-link class="item" :to="{path: '/library'}"><i class="sound icon"></i><translate>Browse library</translate></router-link> <router-link class="item" :to="{path: '/library'}"><i class="sound icon"></i><translate>Browse library</translate></router-link>
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{path: '/favorites'}"><i class="heart icon"></i><translate>Favorites</translate></router-link> <router-link class="item" v-if="$store.state.auth.authenticated" :to="{path: '/favorites'}"><i class="heart icon"></i><translate>Favorites</translate></router-link>
@ -77,7 +77,7 @@
</div> </div>
</div> </div>
<div class="item" v-if="$store.state.auth.availablePermissions['settings']"> <div class="item" v-if="$store.state.auth.availablePermissions['settings']">
<div class="header"><translate>Administration</translate></div> <header class="header"><translate>Administration</translate></header>
<div class="menu"> <div class="menu">
<router-link <router-link
class="item" class="item"
@ -91,8 +91,8 @@
</router-link> </router-link>
</div> </div>
</div> </div>
</div> </nav>
</div> </section>
<div v-if="queue.previousQueue " class="ui black icon message"> <div v-if="queue.previousQueue " class="ui black icon message">
<i class="history icon"></i> <i class="history icon"></i>
<div class="content"> <div class="content">
@ -113,17 +113,21 @@
</div> </div>
</div> </div>
</div> </div>
<div class="ui bottom attached tab" data-tab="queue"> <section class="ui bottom attached tab" data-tab="queue">
<table class="ui compact inverted very basic fixed single line unstackable table"> <table class="ui compact inverted very basic fixed single line unstackable table">
<draggable v-model="tracks" element="tbody" @update="reorder"> <draggable v-model="tracks" element="tbody" @update="reorder">
<tr @click="$store.dispatch('queue/currentIndex', index)" v-for="(track, index) in tracks" :key="index" :class="[{'active': index === queue.currentIndex}]"> <tr
@click="$store.dispatch('queue/currentIndex', index)"
v-for="(track, index) in tracks"
:key="index"
:class="[{'active': index === queue.currentIndex}]">
<td class="right aligned">{{ index + 1}}</td> <td class="right aligned">{{ index + 1}}</td>
<td class="center aligned"> <td class="center aligned">
<img class="ui mini image" v-if="track.album.cover && track.album.cover.original" :src="$store.getters['instance/absoluteUrl'](track.album.cover.small_square_crop)"> <img class="ui mini image" v-if="track.album.cover && track.album.cover.original" :src="$store.getters['instance/absoluteUrl'](track.album.cover.small_square_crop)">
<img class="ui mini image" v-else src="../assets/audio/default-cover.png"> <img class="ui mini image" v-else src="../assets/audio/default-cover.png">
</td> </td>
<td colspan="4"> <td colspan="4">
<button class="title reset ellipsis"> <button class="title reset ellipsis" :aria-label="labels.selectTrack">
<strong>{{ track.title }}</strong><br /> <strong>{{ track.title }}</strong><br />
{{ track.artist.name }} {{ track.artist.name }}
</button> </button>
@ -134,7 +138,7 @@
</template> </template>
</td> </td>
<td> <td>
<button @click.stop="cleanTrack(index)" :class="['ui', {'inverted': index != queue.currentIndex}, 'really', 'tiny', 'basic', 'circular', 'icon', 'button']"> <button :title="labels.removeFromQueue" @click.stop="cleanTrack(index)" :class="['ui', {'inverted': index != queue.currentIndex}, 'really', 'tiny', 'basic', 'circular', 'icon', 'button']">
<i class="trash icon"></i> <i class="trash icon"></i>
</button> </button>
</td> </td>
@ -150,25 +154,25 @@
<div @click="$store.dispatch('radios/stop')" class="ui basic inverted red button"><translate>Stop radio</translate></div> <div @click="$store.dispatch('radios/stop')" class="ui basic inverted red button"><translate>Stop radio</translate></div>
</div> </div>
</div> </div>
</div> </section>
</div> </div>
<player @next="scrollToCurrent" @previous="scrollToCurrent"></player> <player @next="scrollToCurrent" @previous="scrollToCurrent"></player>
</div> </aside>
</template> </template>
<script> <script>
import {mapState, mapActions} from 'vuex' import { mapState, mapActions } from "vuex"
import Player from '@/components/audio/Player' import Player from "@/components/audio/Player"
import Logo from '@/components/Logo' import Logo from "@/components/Logo"
import SearchBar from '@/components/audio/SearchBar' import SearchBar from "@/components/audio/SearchBar"
import backend from '@/audio/backend' import backend from "@/audio/backend"
import draggable from 'vuedraggable' import draggable from "vuedraggable"
import $ from 'jquery' import $ from "jquery"
export default { export default {
name: 'sidebar', name: "sidebar",
components: { components: {
Player, Player,
SearchBar, SearchBar,
@ -177,15 +181,17 @@ export default {
}, },
data() { data() {
return { return {
selectedTab: 'library', selectedTab: "library",
backend: backend, backend: backend,
tracksChangeBuffer: null, tracksChangeBuffer: null,
isCollapsed: true, isCollapsed: true,
fetchInterval: null, fetchInterval: null
} }
}, },
mounted() { mounted() {
$(this.$el).find('.menu .item').tab() $(this.$el)
.find(".menu .item")
.tab()
}, },
destroy() { destroy() {
if (this.fetchInterval) { if (this.fetchInterval) {
@ -198,11 +204,15 @@ export default {
url: state => state.route.path url: state => state.route.path
}), }),
labels() { labels() {
let pendingRequests = this.$gettext('Pending import requests') let mainMenu = this.$gettext("Main menu")
let pendingFollows = this.$gettext('Pending follow requests') let selectTrack = this.$gettext("Play this track")
let pendingRequests = this.$gettext("Pending import requests")
let pendingFollows = this.$gettext("Pending follow requests")
return { return {
pendingRequests, pendingRequests,
pendingFollows pendingFollows,
mainMenu,
selectTrack
} }
}, },
tracks: { tracks: {
@ -215,32 +225,38 @@ export default {
}, },
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"
} }
} }
}, },
methods: { methods: {
...mapActions({ ...mapActions({
cleanTrack: 'queue/cleanTrack' cleanTrack: "queue/cleanTrack"
}), }),
reorder: function(event) { reorder: function(event) {
this.$store.commit('queue/reorder', { this.$store.commit("queue/reorder", {
tracks: this.tracksChangeBuffer, oldIndex: event.oldIndex, newIndex: event.newIndex}) tracks: this.tracksChangeBuffer,
oldIndex: event.oldIndex,
newIndex: event.newIndex
})
}, },
scrollToCurrent() { scrollToCurrent() {
let current = $(this.$el).find('[data-tab="queue"] .active')[0] let current = $(this.$el).find('[data-tab="queue"] .active')[0]
if (!current) { if (!current) {
return return
} }
let container = $(this.$el).find('.tabs')[0] let container = $(this.$el).find(".tabs")[0]
// Position container at the top line then scroll current into view // Position container at the top line then scroll current into view
container.scrollTop = 0 container.scrollTop = 0
current.scrollIntoView(true) current.scrollIntoView(true)
// Scroll back nothing if element is at bottom of container else do it // Scroll back nothing if element is at bottom of container else do it
// for half the height of the containers display area // for half the height of the containers display area
var scrollBack = (container.scrollHeight - container.scrollTop <= container.clientHeight) ? 0 : container.clientHeight / 2 var scrollBack =
container.scrollHeight - container.scrollTop <= container.clientHeight
? 0
: container.clientHeight / 2
container.scrollTop = container.scrollTop - scrollBack container.scrollTop = container.scrollTop - scrollBack
} }
}, },
@ -249,22 +265,22 @@ export default {
this.isCollapsed = true this.isCollapsed = true
}, },
selectedTab: function(newValue) { selectedTab: function(newValue) {
if (newValue === 'queue') { if (newValue === "queue") {
this.scrollToCurrent() this.scrollToCurrent()
} }
}, },
'$store.state.queue.currentIndex': function () { "$store.state.queue.currentIndex": function() {
if (this.selectedTab !== 'queue') { if (this.selectedTab !== "queue") {
this.scrollToCurrent() this.scrollToCurrent()
} }
}, }
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss"> <style scoped lang="scss">
@import '../style/vendor/media'; @import "../style/vendor/media";
$sidebar-color: #3d3e3f; $sidebar-color: #3d3e3f;
@ -284,7 +300,9 @@ $sidebar-color: #3d3e3f;
position: static !important; position: static !important;
width: 100% !important; width: 100% !important;
&.collapsed { &.collapsed {
.menu-area, .player-wrapper, .tabs { .menu-area,
.player-wrapper,
.tabs {
display: none; display: none;
} }
} }
@ -378,7 +396,9 @@ $sidebar-color: #3d3e3f;
.ui.search { .ui.search {
display: flex; display: flex;
.collapse.button, .collapse.button:hover, .collapse.button:active { .collapse.button,
.collapse.button:hover,
.collapse.button:active {
box-shadow: none !important; box-shadow: none !important;
margin: 0px; margin: 0px;
display: flex; display: flex;

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="ui inverted segment player-wrapper" :style="style"> <section class="ui inverted segment player-wrapper" :aria-label="labels.audioPlayer" :style="style">
<div class="player"> <div class="player">
<audio-track <audio-track
ref="currentAudio" ref="currentAudio"
@ -213,18 +213,18 @@
@keydown.s.prevent.exact="shuffle" @keydown.s.prevent.exact="shuffle"
/> />
</div> </div>
</div> </section>
</template> </template>
<script> <script>
import {mapState, mapGetters, mapActions} from 'vuex' import { mapState, mapGetters, mapActions } from "vuex"
import GlobalEvents from '@/components/utils/global-events' import GlobalEvents from "@/components/utils/global-events"
import ColorThief from '@/vendor/color-thief' import ColorThief from "@/vendor/color-thief"
import {Howl} from 'howler' import { Howl } from "howler"
import AudioTrack from '@/components/audio/Track' import AudioTrack from "@/components/audio/Track"
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon"
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon' import TrackPlaylistIcon from "@/components/playlists/TrackPlaylistIcon"
export default { export default {
components: { components: {
@ -234,7 +234,12 @@ export default {
AudioTrack AudioTrack
}, },
data() { data() {
let defaultAmbiantColors = [[46, 46, 46], [46, 46, 46], [46, 46, 46], [46, 46, 46]] let defaultAmbiantColors = [
[46, 46, 46],
[46, 46, 46],
[46, 46, 46],
[46, 46, 46]
]
return { return {
isShuffling: false, isShuffling: false,
sliderVolume: this.volume, sliderVolume: this.volume,
@ -254,7 +259,7 @@ export default {
this.dummyAudio = new Howl({ this.dummyAudio = new Howl({
preload: false, preload: false,
autoplay: false, autoplay: false,
src: ['noop.webm', 'noop.mp3'] src: ["noop.webm", "noop.mp3"]
}) })
}, },
destroyed() { destroyed() {
@ -262,11 +267,11 @@ export default {
}, },
methods: { methods: {
...mapActions({ ...mapActions({
togglePlay: 'player/togglePlay', togglePlay: "player/togglePlay",
mute: 'player/mute', mute: "player/mute",
unmute: 'player/unmute', unmute: "player/unmute",
clean: 'queue/clean', clean: "queue/clean",
updateProgress: 'player/updateProgress' updateProgress: "player/updateProgress"
}), }),
shuffle() { shuffle() {
let disabled = this.queue.tracks.length === 0 let disabled = this.queue.tracks.length === 0
@ -274,12 +279,12 @@ export default {
return return
} }
let self = this let self = this
let msg = this.$gettext('Queue shuffled!') let msg = this.$gettext("Queue shuffled!")
this.isShuffling = true this.isShuffling = true
setTimeout(() => { setTimeout(() => {
self.$store.dispatch('queue/shuffle', () => { self.$store.dispatch("queue/shuffle", () => {
self.isShuffling = false self.isShuffling = false
self.$store.commit('ui/addMessage', { self.$store.commit("ui/addMessage", {
content: msg, content: msg,
date: new Date() date: new Date()
}) })
@ -288,20 +293,20 @@ export default {
}, },
next() { next() {
let self = this let self = this
this.$store.dispatch('queue/next').then(() => { this.$store.dispatch("queue/next").then(() => {
self.$emit('next') self.$emit("next")
}) })
}, },
previous() { previous() {
let self = this let self = this
this.$store.dispatch('queue/previous').then(() => { this.$store.dispatch("queue/previous").then(() => {
self.$emit('previous') self.$emit("previous")
}) })
}, },
touchProgress(e) { touchProgress(e) {
let time let time
let target = this.$refs.progress let target = this.$refs.progress
time = e.layerX / target.offsetWidth * this.duration time = (e.layerX / target.offsetWidth) * this.duration
this.$refs.currentAudio.setCurrentTime(time) this.$refs.currentAudio.setCurrentTime(time)
}, },
updateBackground() { updateBackground() {
@ -313,8 +318,8 @@ export default {
this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4) this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4)
}, },
handleError({ sound, error }) { handleError({ sound, error }) {
this.$store.commit('player/isLoadingAudio', false) this.$store.commit("player/isLoadingAudio", false)
this.$store.dispatch('player/trackErrored') this.$store.dispatch("player/trackErrored")
} }
}, },
computed: { computed: {
@ -330,26 +335,34 @@ export default {
queue: state => state.queue queue: state => state.queue
}), }),
...mapGetters({ ...mapGetters({
currentTrack: 'queue/currentTrack', currentTrack: "queue/currentTrack",
hasNext: 'queue/hasNext', hasNext: "queue/hasNext",
emptyQueue: 'queue/isEmpty', emptyQueue: "queue/isEmpty",
durationFormatted: 'player/durationFormatted', durationFormatted: "player/durationFormatted",
currentTimeFormatted: 'player/currentTimeFormatted', currentTimeFormatted: "player/currentTimeFormatted",
progress: 'player/progress' progress: "player/progress"
}), }),
labels() { labels() {
let previousTrack = this.$gettext('Previous track') let audioPlayer = this.$gettext("Media player")
let play = this.$gettext('Play track') let previousTrack = this.$gettext("Previous track")
let pause = this.$gettext('Pause track') let play = this.$gettext("Play track")
let next = this.$gettext('Next track') let pause = this.$gettext("Pause track")
let unmute = this.$gettext('Unmute') let next = this.$gettext("Next track")
let mute = this.$gettext('Mute') let unmute = this.$gettext("Unmute")
let loopingDisabled = this.$gettext('Looping disabled. Click to switch to single-track looping.') let mute = this.$gettext("Mute")
let loopingSingle = this.$gettext('Looping on a single track. Click to switch to whole queue looping.') let loopingDisabled = this.$gettext(
let loopingWhole = this.$gettext('Looping on whole queue. Click to disable looping.') "Looping disabled. Click to switch to single-track looping."
let shuffle = this.$gettext('Shuffle your queue') )
let clear = this.$gettext('Clear your queue') let loopingSingle = this.$gettext(
"Looping on a single track. Click to switch to whole queue looping."
)
let loopingWhole = this.$gettext(
"Looping on whole queue. Click to disable looping."
)
let shuffle = this.$gettext("Shuffle your queue")
let clear = this.$gettext("Clear your queue")
return { return {
audioPlayer,
previousTrack, previousTrack,
play, play,
pause, pause,
@ -365,7 +378,7 @@ export default {
}, },
style: function() { style: function() {
let style = { let style = {
'background': this.ambiantGradiant background: this.ambiantGradiant
} }
return style return style
}, },
@ -376,11 +389,17 @@ export default {
{ orientation: 150, percent: 80, opacity: 0.7 }, { orientation: 150, percent: 80, opacity: 0.7 },
{ orientation: 60, percent: 70, opacity: 0.7 } { orientation: 60, percent: 70, opacity: 0.7 }
] ]
let gradients = this.ambiantColors.map((e, i) => { let gradients = this.ambiantColors
.map((e, i) => {
let [r, g, b] = e let [r, g, b] = e
let conf = indexConf[i] let conf = indexConf[i]
return `linear-gradient(${conf.orientation}deg, rgba(${r}, ${g}, ${b}, ${conf.opacity}) 10%, rgba(255, 255, 255, 0) ${conf.percent}%)` return `linear-gradient(${
}).join(', ') conf.orientation
}deg, rgba(${r}, ${g}, ${b}, ${
conf.opacity
}) 10%, rgba(255, 255, 255, 0) ${conf.percent}%)`
})
.join(", ")
return gradients return gradients
} }
}, },
@ -397,7 +416,7 @@ export default {
this.sliderVolume = newValue this.sliderVolume = newValue
}, },
sliderVolume(newValue) { sliderVolume(newValue) {
this.$store.commit('player/volume', newValue) this.$store.commit("player/volume", newValue)
} }
} }
} }
@ -405,7 +424,6 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss"> <style scoped lang="scss">
.ui.progress { .ui.progress {
margin: 0.5rem 0 1rem; margin: 0.5rem 0 1rem;
} }
@ -423,18 +441,21 @@ export default {
.ui.item { .ui.item {
.meta { .meta {
font-size: 90%; font-size: 90%;
line-height: 1.2 line-height: 1.2;
} }
} }
.timer.total { .timer.total {
text-align: right; text-align: right;
} }
.timer.start { .timer.start {
cursor: pointer cursor: pointer;
} }
.track-area { .track-area {
margin-top: 0; margin-top: 0;
.header, .meta, .artist, .album { .header,
.meta,
.artist,
.album {
color: white !important; color: white !important;
} }
} }
@ -468,57 +489,57 @@ export default {
left: 25%; left: 25%;
cursor: pointer; cursor: pointer;
} }
input[type=range]:focus { input[type="range"]:focus {
outline: none; outline: none;
} }
input[type=range]::-webkit-slider-runnable-track { input[type="range"]::-webkit-slider-runnable-track {
cursor: pointer; cursor: pointer;
} }
input[type=range]::-webkit-slider-thumb { input[type="range"]::-webkit-slider-thumb {
background: white; background: white;
cursor: pointer; cursor: pointer;
-webkit-appearance: none; -webkit-appearance: none;
border-radius: 3px; border-radius: 3px;
width: 10px; width: 10px;
} }
input[type=range]::-moz-range-track { input[type="range"]::-moz-range-track {
cursor: pointer; cursor: pointer;
background: white; background: white;
opacity: 0.3; opacity: 0.3;
} }
input[type=range]::-moz-focus-outer { input[type="range"]::-moz-focus-outer {
border: 0; border: 0;
} }
input[type=range]::-moz-range-thumb { input[type="range"]::-moz-range-thumb {
background: white; background: white;
cursor: pointer; cursor: pointer;
border-radius: 3px; border-radius: 3px;
width: 10px; width: 10px;
} }
input[type=range]::-ms-track { input[type="range"]::-ms-track {
cursor: pointer; cursor: pointer;
background: transparent; background: transparent;
border-color: transparent; border-color: transparent;
color: transparent; color: transparent;
} }
input[type=range]::-ms-fill-lower { input[type="range"]::-ms-fill-lower {
background: white; background: white;
opacity: 0.3; opacity: 0.3;
} }
input[type=range]::-ms-fill-upper { input[type="range"]::-ms-fill-upper {
background: white; background: white;
opacity: 0.3; opacity: 0.3;
} }
input[type=range]::-ms-thumb { input[type="range"]::-ms-thumb {
background: white; background: white;
cursor: pointer; cursor: pointer;
border-radius: 3px; border-radius: 3px;
width: 10px; width: 10px;
} }
input[type=range]:focus::-ms-fill-lower { input[type="range"]:focus::-ms-fill-lower {
background: white; background: white;
} }
input[type=range]:focus::-ms-fill-upper { input[type="range"]:focus::-ms-fill-upper {
background: white; background: white;
} }
} }
@ -545,7 +566,6 @@ export default {
margin: 0; margin: 0;
} }
@keyframes MOVE-BG { @keyframes MOVE-BG {
from { from {
transform: translateX(0px); transform: translateX(0px);
@ -576,7 +596,7 @@ export default {
grey 1px, grey 1px,
grey 10px, grey 10px,
transparent 10px, transparent 10px,
transparent 20px, transparent 20px
) !important; ) !important;
animation-name: MOVE-BG; animation-name: MOVE-BG;

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="main pusher" v-title="labels.title"> <main class="main pusher" v-title="labels.title">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div class="ui small text container"> <div class="ui small text container">
<h2><translate>Log in to your Funkwhale account</translate></h2> <h2><translate>Log in to your Funkwhale account</translate></h2>
<form class="ui form" @submit.prevent="submit()"> <form class="ui form" @submit.prevent="submit()">
@ -43,16 +43,16 @@
</button> </button>
</form> </form>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import PasswordInput from '@/components/forms/PasswordInput' import PasswordInput from "@/components/forms/PasswordInput"
export default { export default {
props: { props: {
next: {type: String, default: '/'} next: { type: String, default: "/" }
}, },
components: { components: {
PasswordInput PasswordInput
@ -62,10 +62,10 @@ export default {
// We need to initialize the component with any // We need to initialize the component with any
// properties that will be used in it // properties that will be used in it
credentials: { credentials: {
username: '', username: "",
password: '' password: ""
}, },
error: '', error: "",
isLoading: false isLoading: false
} }
}, },
@ -74,8 +74,8 @@ export default {
}, },
computed: { computed: {
labels() { labels() {
let usernamePlaceholder = this.$gettext('Enter your username or email') let usernamePlaceholder = this.$gettext("Enter your username or email")
let title = this.$gettext('Log In') let title = this.$gettext("Log In")
return { return {
usernamePlaceholder, usernamePlaceholder,
title title
@ -86,27 +86,28 @@ export default {
submit() { submit() {
var self = this var self = this
self.isLoading = true self.isLoading = true
this.error = '' this.error = ""
var credentials = { var credentials = {
username: this.credentials.username, username: this.credentials.username,
password: this.credentials.password password: this.credentials.password
} }
this.$store.dispatch('auth/login', { this.$store
.dispatch("auth/login", {
credentials, credentials,
next: '/library', next: "/library",
onError: error => { onError: error => {
if (error.response.status === 400) { if (error.response.status === 400) {
self.error = 'invalid_credentials' self.error = "invalid_credentials"
} else { } else {
self.error = 'unknown_error' self.error = "unknown_error"
} }
} }
}).then(e => { })
.then(e => {
self.isLoading = false self.isLoading = false
}) })
} }
} }
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="main pusher" v-title="labels.title"> <main class="main pusher" v-title="labels.title">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div class="ui small text container"> <div class="ui small text container">
<h2> <h2>
<translate>Are you sure you want to log out?</translate> <translate>Are you sure you want to log out?</translate>
@ -8,8 +8,8 @@
<p v-translate="{username: $store.state.auth.username}">You are currently logged in as %{ username }</p> <p v-translate="{username: $store.state.auth.username}">You are currently logged in as %{ username }</p>
<button class="ui button" @click="$store.dispatch('auth/logout')"><translate>Yes, log me out!</translate></button> <button class="ui button" @click="$store.dispatch('auth/logout')"><translate>Yes, log me out!</translate></button>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
@ -17,7 +17,7 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
title: this.$gettext('Log Out') title: this.$gettext("Log Out")
} }
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="main pusher" v-title="labels.usernameProfile"> <main class="main pusher" v-title="labels.usernameProfile">
<div v-if="isLoading" class="ui vertical segment"> <div v-if="isLoading" class="ui vertical segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div> </div>
@ -25,34 +25,35 @@
</a> </a>
</div> </div>
</template> </template>
</div> </main>
</template> </template>
<script> <script>
import {mapState} from 'vuex' import { mapState } from "vuex"
const dateFormat = require('dateformat') const dateFormat = require("dateformat")
export default { export default {
props: ['username'], props: ["username"],
created() { created() {
this.$store.dispatch('auth/fetchProfile') this.$store.dispatch("auth/fetchProfile")
}, },
computed: { computed: {
...mapState({ ...mapState({
profile: state => state.auth.profile profile: state => state.auth.profile
}), }),
labels() { labels() {
let msg = this.$gettext('%{ username }\'s profile') let msg = this.$gettext("%{ username }'s profile")
let usernameProfile = this.$gettextInterpolate(msg, {username: this.username}) let usernameProfile = this.$gettextInterpolate(msg, {
username: this.username
})
return { return {
usernameProfile usernameProfile
} }
}, },
signupDate() { signupDate() {
let d = new Date(this.profile.date_joined) let d = new Date(this.profile.date_joined)
return dateFormat(d, 'longDate') return dateFormat(d, "longDate")
}, },
isLoading() { isLoading() {
return !this.profile return !this.profile

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="main pusher" v-title="labels.title"> <main class="main pusher" v-title="labels.title">
<div class="ui vertical stripe segment"> <div class="ui vertical stripe segment">
<div class="ui small text container"> <section class="ui small text container">
<h2 class="ui header"> <h2 class="ui header">
<translate>Account settings</translate> <translate>Account settings</translate>
</h2> </h2>
@ -28,9 +28,9 @@
<translate>Update settings</translate> <translate>Update settings</translate>
</button> </button>
</form> </form>
</div> </section>
<div class="ui hidden divider"></div> <div class="ui hidden divider"></div>
<div class="ui small text container"> <section class="ui small text container">
<h2 class="ui header"> <h2 class="ui header">
<translate>Avatar</translate> <translate>Avatar</translate>
</h2> </h2>
@ -61,9 +61,9 @@
</div> </div>
</div> </div>
</div> </div>
</div> </section>
<div class="ui hidden divider"></div> <div class="ui hidden divider"></div>
<div class="ui small text container"> <section class="ui small text container">
<h2 class="ui header"> <h2 class="ui header">
<translate>Change my password</translate> <translate>Change my password</translate>
</h2> </h2>
@ -107,18 +107,18 @@
</form> </form>
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<subsonic-token-form /> <subsonic-token-form />
</section>
</div> </div>
</div> </main>
</div>
</template> </template>
<script> <script>
import $ from 'jquery' import $ from "jquery"
import axios from 'axios' import axios from "axios"
import logger from '@/logging' import logger from "@/logging"
import PasswordInput from '@/components/forms/PasswordInput' import PasswordInput from "@/components/forms/PasswordInput"
import SubsonicTokenForm from '@/components/auth/SubsonicTokenForm' import SubsonicTokenForm from "@/components/auth/SubsonicTokenForm"
import TranslationsMixin from '@/components/mixins/Translations' import TranslationsMixin from "@/components/mixins/Translations"
export default { export default {
mixins: [TranslationsMixin], mixins: [TranslationsMixin],
@ -130,10 +130,10 @@ export default {
let d = { let d = {
// We need to initialize the component with any // We need to initialize the component with any
// properties that will be used in it // properties that will be used in it
old_password: '', old_password: "",
new_password: '', new_password: "",
currentAvatar: this.$store.state.auth.profile.avatar, currentAvatar: this.$store.state.auth.profile.avatar,
passwordError: '', passwordError: "",
isLoading: false, isLoading: false,
isLoadingAvatar: false, isLoadingAvatar: false,
avatarErrors: [], avatarErrors: [],
@ -141,12 +141,12 @@ export default {
settings: { settings: {
success: false, success: false,
errors: [], errors: [],
order: ['privacy_level'], order: ["privacy_level"],
fields: { fields: {
'privacy_level': { privacy_level: {
type: 'dropdown', type: "dropdown",
initial: this.$store.state.auth.profile.privacy_level, initial: this.$store.state.auth.profile.privacy_level,
choices: ['me', 'instance'] choices: ["me", "instance"]
} }
} }
} }
@ -158,7 +158,7 @@ export default {
return d return d
}, },
mounted() { mounted() {
$('select.dropdown').dropdown() $("select.dropdown").dropdown()
}, },
methods: { methods: {
submitSettings() { submitSettings() {
@ -167,17 +167,20 @@ export default {
let self = this let self = this
let payload = this.settingsValues let payload = this.settingsValues
let url = `users/users/${this.$store.state.auth.username}/` let url = `users/users/${this.$store.state.auth.username}/`
return axios.patch(url, payload).then(response => { return axios.patch(url, payload).then(
logger.default.info('Updated settings successfully') response => {
logger.default.info("Updated settings successfully")
self.settings.success = true self.settings.success = true
return axios.get('users/users/me/').then((response) => { return axios.get("users/users/me/").then(response => {
self.$store.dispatch('auth/updateProfile', response.data) self.$store.dispatch("auth/updateProfile", response.data)
}) })
}, error => { },
logger.default.error('Error while updating settings') error => {
logger.default.error("Error while updating settings")
self.isLoading = false self.isLoading = false
self.settings.errors = error.backendErrors self.settings.errors = error.backendErrors
}) }
)
}, },
submitAvatar() { submitAvatar() {
this.isLoadingAvatar = true this.isLoadingAvatar = true
@ -185,71 +188,80 @@ export default {
let self = this let self = this
this.avatar = this.$refs.avatar.files[0] this.avatar = this.$refs.avatar.files[0]
let formData = new FormData() let formData = new FormData()
formData.append('avatar', this.avatar) formData.append("avatar", this.avatar)
axios.patch( axios
`users/users/${this.$store.state.auth.username}/`, .patch(`users/users/${this.$store.state.auth.username}/`, formData, {
formData,
{
headers: { headers: {
'Content-Type': 'multipart/form-data' "Content-Type": "multipart/form-data"
} }
} })
).then(response => { .then(
response => {
this.isLoadingAvatar = false this.isLoadingAvatar = false
self.currentAvatar = response.data.avatar self.currentAvatar = response.data.avatar
self.$store.commit('auth/avatar', self.currentAvatar) self.$store.commit("auth/avatar", self.currentAvatar)
}, error => { },
error => {
self.isLoadingAvatar = false self.isLoadingAvatar = false
self.avatarErrors = error.backendErrors self.avatarErrors = error.backendErrors
}) }
)
}, },
removeAvatar() { removeAvatar() {
this.isLoadingAvatar = true this.isLoadingAvatar = true
let self = this let self = this
this.avatar = null this.avatar = null
axios.patch( axios
`users/users/${this.$store.state.auth.username}/`, .patch(`users/users/${this.$store.state.auth.username}/`, {
{avatar: null} avatar: null
).then(response => { })
.then(
response => {
this.isLoadingAvatar = false this.isLoadingAvatar = false
self.currentAvatar = {} self.currentAvatar = {}
self.$store.commit('auth/avatar', self.currentAvatar) self.$store.commit("auth/avatar", self.currentAvatar)
}, error => { },
error => {
self.isLoadingAvatar = false self.isLoadingAvatar = false
self.avatarErrors = error.backendErrors self.avatarErrors = error.backendErrors
}) }
)
}, },
submitPassword() { submitPassword() {
var self = this var self = this
self.isLoading = true self.isLoading = true
this.error = '' this.error = ""
var credentials = { var credentials = {
old_password: this.old_password, old_password: this.old_password,
new_password1: this.new_password, new_password1: this.new_password,
new_password2: this.new_password new_password2: this.new_password
} }
let url = 'auth/registration/change-password/' let url = "auth/registration/change-password/"
return axios.post(url, credentials).then(response => { return axios.post(url, credentials).then(
logger.default.info('Password successfully changed') response => {
logger.default.info("Password successfully changed")
self.$router.push({ self.$router.push({
name: 'profile', name: "profile",
params: { params: {
username: self.$store.state.auth.username username: self.$store.state.auth.username
}}) }
}, error => { })
},
error => {
if (error.response.status === 400) { if (error.response.status === 400) {
self.passwordError = 'invalid_credentials' self.passwordError = "invalid_credentials"
} else { } else {
self.passwordError = 'unknown_error' self.passwordError = "unknown_error"
} }
self.isLoading = false self.isLoading = false
}) }
)
} }
}, },
computed: { computed: {
labels() { labels() {
return { return {
title: this.$gettext('Account Settings') title: this.$gettext("Account Settings")
} }
}, },
orderedSettingsFields() { orderedSettingsFields() {
@ -268,7 +280,6 @@ export default {
return s return s
} }
} }
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="main pusher" v-title="labels.title"> <main class="main pusher" v-title="labels.title">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div class="ui small text container"> <div class="ui small text container">
<h2><translate>Create a funkwhale account</translate></h2> <h2><translate>Create a funkwhale account</translate></h2>
<form <form
@ -53,29 +53,29 @@
</button> </button>
</form> </form>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import logger from '@/logging' import logger from "@/logging"
import PasswordInput from '@/components/forms/PasswordInput' import PasswordInput from "@/components/forms/PasswordInput"
export default { export default {
props: { props: {
defaultInvitation: { type: String, required: false, default: null }, defaultInvitation: { type: String, required: false, default: null },
next: {type: String, default: '/'} next: { type: String, default: "/" }
}, },
components: { components: {
PasswordInput PasswordInput
}, },
data() { data() {
return { return {
username: '', username: "",
email: '', email: "",
password: '', password: "",
isLoadingInstanceSetting: true, isLoadingInstanceSetting: true,
errors: [], errors: [],
isLoading: false, isLoading: false,
@ -84,7 +84,7 @@ export default {
}, },
created() { created() {
let self = this let self = this
this.$store.dispatch('instance/fetchSettings', { this.$store.dispatch("instance/fetchSettings", {
callback: function() { callback: function() {
self.isLoadingInstanceSetting = false self.isLoadingInstanceSetting = false
} }
@ -92,10 +92,12 @@ export default {
}, },
computed: { computed: {
labels() { labels() {
let title = this.$gettext('Sign Up') let title = this.$gettext("Sign Up")
let placeholder = this.$gettext('Enter your invitation code (case insensitive)') let placeholder = this.$gettext(
let usernamePlaceholder = this.$gettext('Enter your username') "Enter your invitation code (case insensitive)"
let emailPlaceholder = this.$gettext('Enter your email') )
let usernamePlaceholder = this.$gettext("Enter your username")
let emailPlaceholder = this.$gettext("Enter your email")
return { return {
title, title,
usernamePlaceholder, usernamePlaceholder,
@ -116,17 +118,21 @@ export default {
email: this.email, email: this.email,
invitation: this.invitation invitation: this.invitation
} }
return axios.post('auth/registration/', payload).then(response => { return axios.post("auth/registration/", payload).then(
logger.default.info('Successfully created account') response => {
logger.default.info("Successfully created account")
self.$router.push({ self.$router.push({
name: 'profile', name: "profile",
params: { params: {
username: this.username username: this.username
}}) }
}, error => { })
},
error => {
self.errors = error.backendErrors self.errors = error.backendErrors
self.isLoading = false self.isLoading = false
}) }
)
} }
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="main pusher" v-title="labels.title"> <main class="main pusher" v-title="labels.title">
<div class="ui vertical center aligned stripe segment"> <section class="ui vertical center aligned stripe segment">
<div :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']"> <div :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']">
<div class="ui text loader"> <div class="ui text loader">
<translate>Loading your favorites...</translate> <translate>Loading your favorites...</translate>
@ -16,8 +16,8 @@
</translate> </translate>
</h2> </h2>
<radio-button type="favorites"></radio-button> <radio-button type="favorites"></radio-button>
</div> </section>
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div :class="['ui', {'loading': isLoading}, 'form']"> <div :class="['ui', {'loading': isLoading}, 'form']">
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
@ -56,21 +56,21 @@
:total="results.count" :total="results.count"
></pagination> ></pagination>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import $ from 'jquery' import $ from "jquery"
import logger from '@/logging' import logger from "@/logging"
import TrackTable from '@/components/audio/track/Table' import TrackTable from "@/components/audio/track/Table"
import RadioButton from '@/components/radios/Button' import RadioButton from "@/components/radios/Button"
import Pagination from '@/components/Pagination' import Pagination from "@/components/Pagination"
import OrderingMixin from '@/components/mixins/Ordering' import OrderingMixin from "@/components/mixins/Ordering"
import PaginationMixin from '@/components/mixins/Pagination' import PaginationMixin from "@/components/mixins/Pagination"
import TranslationsMixin from '@/components/mixins/Translations' import TranslationsMixin from "@/components/mixins/Translations"
const FAVORITES_URL = 'tracks/' const FAVORITES_URL = "tracks/"
export default { export default {
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
@ -80,7 +80,9 @@ export default {
Pagination Pagination
}, },
data() { data() {
let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') let defaultOrdering = this.getOrderingFromString(
this.defaultOrdering || "-creation_date"
)
return { return {
results: null, results: null,
isLoading: false, isLoading: false,
@ -88,13 +90,13 @@ export default {
previousLink: null, previousLink: null,
page: parseInt(this.defaultPage), page: parseInt(this.defaultPage),
paginateBy: parseInt(this.defaultPaginateBy || 25), paginateBy: parseInt(this.defaultPaginateBy || 25),
orderingDirection: defaultOrdering.direction || '+', orderingDirection: defaultOrdering.direction || "+",
ordering: defaultOrdering.field, ordering: defaultOrdering.field,
orderingOptions: [ orderingOptions: [
['creation_date', 'creation_date'], ["creation_date", "creation_date"],
['title', 'track_title'], ["title", "track_title"],
['album__title', 'album_title'], ["album__title", "album_title"],
['artist__name', 'artist_name'] ["artist__name", "artist_name"]
] ]
} }
}, },
@ -102,12 +104,12 @@ export default {
this.fetchFavorites(FAVORITES_URL) this.fetchFavorites(FAVORITES_URL)
}, },
mounted() { mounted() {
$('.ui.dropdown').dropdown() $(".ui.dropdown").dropdown()
}, },
computed: { computed: {
labels() { labels() {
return { return {
title: this.$gettext('Your Favorites') title: this.$gettext("Your Favorites")
} }
} }
}, },
@ -125,20 +127,20 @@ export default {
var self = this var self = this
this.isLoading = true this.isLoading = true
let params = { let params = {
favorites: 'true', favorites: "true",
page: this.page, page: this.page,
page_size: this.paginateBy, page_size: this.paginateBy,
ordering: this.getOrderingAsString() ordering: this.getOrderingAsString()
} }
logger.default.time('Loading user favorites') logger.default.time("Loading user favorites")
axios.get(url, {params: params}).then((response) => { axios.get(url, { params: params }).then(response => {
self.results = response.data self.results = response.data
self.nextLink = response.data.next self.nextLink = response.data.next
self.previousLink = response.data.previous self.previousLink = response.data.previous
self.results.results.forEach((track) => { self.results.results.forEach(track => {
self.$store.commit('favorites/track', {id: track.id, value: true}) self.$store.commit("favorites/track", { id: track.id, value: true })
}) })
logger.default.timeEnd('Loading user favorites') logger.default.timeEnd("Loading user favorites")
self.isLoading = false self.isLoading = false
}) })
}, },

View File

@ -1,10 +1,10 @@
<template> <template>
<div> <main>
<div v-if="isLoading" class="ui vertical segment" v-title=""> <div v-if="isLoading" class="ui vertical segment" v-title="">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div> </div>
<template v-if="album"> <template v-if="album">
<div :class="['ui', 'head', {'with-background': album.cover.original}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="album.title"> <section :class="['ui', 'head', {'with-background': album.cover.original}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="album.title">
<div class="segment-content"> <div class="segment-content">
<h2 class="ui center aligned icon header"> <h2 class="ui center aligned icon header">
<i class="circular inverted sound yellow icon"></i> <i class="circular inverted sound yellow icon"></i>
@ -38,37 +38,37 @@
<translate>View on MusicBrainz</translate> <translate>View on MusicBrainz</translate>
</a> </a>
</div> </div>
</div> </section>
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2> <h2>
<translate>Tracks</translate> <translate>Tracks</translate>
</h2> </h2>
<track-table v-if="album" :artist="album.artist" :display-position="true" :tracks="album.tracks"></track-table> <track-table v-if="album" :artist="album.artist" :display-position="true" :tracks="album.tracks"></track-table>
</div> </section>
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2> <h2>
<translate>User libraries</translate> <translate>User libraries</translate>
</h2> </h2>
<library-widget :url="'albums/' + id + '/libraries/'"> <library-widget :url="'albums/' + id + '/libraries/'">
<translate slot="subtitle">This album is present in the following libraries:</translate> <translate slot="subtitle">This album is present in the following libraries:</translate>
</library-widget> </library-widget>
</div> </section>
</template> </template>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import logger from '@/logging' import logger from "@/logging"
import backend from '@/audio/backend' import backend from "@/audio/backend"
import PlayButton from '@/components/audio/PlayButton' import PlayButton from "@/components/audio/PlayButton"
import TrackTable from '@/components/audio/track/Table' import TrackTable from "@/components/audio/track/Table"
import LibraryWidget from '@/components/federation/LibraryWidget' import LibraryWidget from "@/components/federation/LibraryWidget"
const FETCH_URL = 'albums/' const FETCH_URL = "albums/"
export default { export default {
props: ['id'], props: ["id"],
components: { components: {
PlayButton, PlayButton,
TrackTable, TrackTable,
@ -87,9 +87,9 @@ export default {
fetchData() { fetchData() {
var self = this var self = this
this.isLoading = true this.isLoading = true
let url = FETCH_URL + this.id + '/' let url = FETCH_URL + this.id + "/"
logger.default.debug('Fetching album "' + this.id + '"') logger.default.debug('Fetching album "' + this.id + '"')
axios.get(url).then((response) => { axios.get(url).then(response => {
self.album = backend.Album.clean(response.data) self.album = backend.Album.clean(response.data)
self.isLoading = false self.isLoading = false
}) })
@ -98,22 +98,29 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
title: this.$gettext('Album') title: this.$gettext("Album")
} }
}, },
wikipediaUrl() { wikipediaUrl() {
return 'https://en.wikipedia.org/w/index.php?search=' + encodeURI(this.album.title + ' ' + this.album.artist.name) return (
"https://en.wikipedia.org/w/index.php?search=" +
encodeURI(this.album.title + " " + this.album.artist.name)
)
}, },
musicbrainzUrl() { musicbrainzUrl() {
if (this.album.mbid) { if (this.album.mbid) {
return 'https://musicbrainz.org/release/' + this.album.mbid return "https://musicbrainz.org/release/" + this.album.mbid
} }
}, },
headerStyle() { headerStyle() {
if (!this.album.cover.original) { if (!this.album.cover.original) {
return '' return ""
} }
return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.album.cover.original) + ')' return (
"background-image: url(" +
this.$store.getters["instance/absoluteUrl"](this.album.cover.original) +
")"
)
} }
}, },
watch: { watch: {
@ -126,5 +133,4 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss"> <style scoped lang="scss">
</style> </style>

View File

@ -1,10 +1,10 @@
<template> <template>
<div v-title="labels.title"> <main v-title="labels.title">
<div v-if="isLoading" class="ui vertical segment"> <div v-if="isLoading" class="ui vertical segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div> </div>
<template v-if="artist"> <template v-if="artist">
<div :class="['ui', 'head', {'with-background': cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="artist.name"> <section :class="['ui', 'head', {'with-background': cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="artist.name">
<div class="segment-content"> <div class="segment-content">
<h2 class="ui center aligned icon header"> <h2 class="ui center aligned icon header">
<i class="circular inverted users violet icon"></i> <i class="circular inverted users violet icon"></i>
@ -36,11 +36,11 @@
<translate>View on MusicBrainz</translate> <translate>View on MusicBrainz</translate>
</a> </a>
</div> </div>
</div> </section>
<div v-if="isLoadingAlbums" class="ui vertical stripe segment"> <section v-if="isLoadingAlbums" class="ui vertical stripe segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div> </section>
<div v-else-if="albums && albums.length > 0" class="ui vertical stripe segment"> <section v-else-if="albums && albums.length > 0" class="ui vertical stripe segment">
<h2> <h2>
<translate>Albums by this artist</translate> <translate>Albums by this artist</translate>
</h2> </h2>
@ -49,38 +49,38 @@
<album-card :mode="'rich'" class="fluid" :album="album"></album-card> <album-card :mode="'rich'" class="fluid" :album="album"></album-card>
</div> </div>
</div> </div>
</div> </section>
<div v-if="tracks.length > 0" class="ui vertical stripe segment"> <section v-if="tracks.length > 0" class="ui vertical stripe segment">
<h2> <h2>
<translate>Tracks by this artist</translate> <translate>Tracks by this artist</translate>
</h2> </h2>
<track-table :display-position="true" :tracks="tracks"></track-table> <track-table :display-position="true" :tracks="tracks"></track-table>
</div> </section>
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2> <h2>
<translate>User libraries</translate> <translate>User libraries</translate>
</h2> </h2>
<library-widget :url="'artists/' + id + '/libraries/'"> <library-widget :url="'artists/' + id + '/libraries/'">
<translate slot="subtitle">This artist is present in the following libraries:</translate> <translate slot="subtitle">This artist is present in the following libraries:</translate>
</library-widget> </library-widget>
</div> </section>
</template> </template>
</div> </main>
</template> </template>
<script> <script>
import _ from 'lodash' import _ from "lodash"
import axios from 'axios' import axios from "axios"
import logger from '@/logging' import logger from "@/logging"
import backend from '@/audio/backend' import backend from "@/audio/backend"
import AlbumCard from '@/components/audio/album/Card' import AlbumCard from "@/components/audio/album/Card"
import RadioButton from '@/components/radios/Button' import RadioButton from "@/components/radios/Button"
import PlayButton from '@/components/audio/PlayButton' import PlayButton from "@/components/audio/PlayButton"
import TrackTable from '@/components/audio/track/Table' import TrackTable from "@/components/audio/track/Table"
import LibraryWidget from '@/components/federation/LibraryWidget' import LibraryWidget from "@/components/federation/LibraryWidget"
export default { export default {
props: ['id'], props: ["id"],
components: { components: {
AlbumCard, AlbumCard,
RadioButton, RadioButton,
@ -107,18 +107,22 @@ export default {
var self = this var self = this
this.isLoading = true this.isLoading = true
logger.default.debug('Fetching artist "' + this.id + '"') logger.default.debug('Fetching artist "' + this.id + '"')
axios.get('tracks/', {params: {artist: this.id}}).then((response) => { axios.get("tracks/", { params: { artist: this.id } }).then(response => {
self.tracks = response.data.results self.tracks = response.data.results
self.totalTracks = response.data.count self.totalTracks = response.data.count
}) })
axios.get('artists/' + this.id + '/').then((response) => { axios.get("artists/" + this.id + "/").then(response => {
self.artist = response.data self.artist = response.data
self.isLoading = false self.isLoading = false
self.isLoadingAlbums = true self.isLoadingAlbums = true
axios.get('albums/', {params: {artist: self.id, ordering: '-release_date'}}).then((response) => { axios
.get("albums/", {
params: { artist: self.id, ordering: "-release_date" }
})
.then(response => {
self.totalAlbums = response.data.count self.totalAlbums = response.data.count
let parsed = JSON.parse(JSON.stringify(response.data.results)) let parsed = JSON.parse(JSON.stringify(response.data.results))
self.albums = parsed.map((album) => { self.albums = parsed.map(album => {
return backend.Album.clean(album) return backend.Album.clean(album)
}) })
@ -130,20 +134,25 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
title: this.$gettext('Artist') title: this.$gettext("Artist")
} }
}, },
isPlayable() { isPlayable() {
return this.artist.albums.filter((a) => { return (
this.artist.albums.filter(a => {
return a.is_playable return a.is_playable
}).length > 0 }).length > 0
)
}, },
wikipediaUrl() { wikipediaUrl() {
return 'https://en.wikipedia.org/w/index.php?search=' + encodeURI(this.artist.name) return (
"https://en.wikipedia.org/w/index.php?search=" +
encodeURI(this.artist.name)
)
}, },
musicbrainzUrl() { musicbrainzUrl() {
if (this.artist.mbid) { if (this.artist.mbid) {
return 'https://musicbrainz.org/artist/' + this.artist.mbid return "https://musicbrainz.org/artist/" + this.artist.mbid
} }
}, },
allTracks() { allTracks() {
@ -156,17 +165,23 @@ export default {
return tracks return tracks
}, },
cover() { cover() {
return this.artist.albums.filter(album => { return this.artist.albums
.filter(album => {
return album.cover return album.cover
}).map(album => { })
.map(album => {
return album.cover return album.cover
})[0] })[0]
}, },
headerStyle() { headerStyle() {
if (!this.cover || !this.cover.original) { if (!this.cover || !this.cover.original) {
return '' return ""
} }
return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.cover.original) + ')' return (
"background-image: url(" +
this.$store.getters["instance/absoluteUrl"](this.cover.original) +
")"
)
} }
}, },
watch: { watch: {

View File

@ -1,6 +1,6 @@
<template> <template>
<div v-title="labels.title"> <main v-title="labels.title">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2 class="ui header"> <h2 class="ui header">
<translate>Browsing artists</translate> <translate>Browsing artists</translate>
</h2> </h2>
@ -64,60 +64,59 @@
:total="result.count" :total="result.count"
></pagination> ></pagination>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import _ from 'lodash' import _ from "lodash"
import $ from 'jquery' import $ from "jquery"
import logger from '@/logging' import logger from "@/logging"
import OrderingMixin from '@/components/mixins/Ordering' import OrderingMixin from "@/components/mixins/Ordering"
import PaginationMixin from '@/components/mixins/Pagination' import PaginationMixin from "@/components/mixins/Pagination"
import TranslationsMixin from '@/components/mixins/Translations' import TranslationsMixin from "@/components/mixins/Translations"
import ArtistCard from '@/components/audio/artist/Card' import ArtistCard from "@/components/audio/artist/Card"
import Pagination from '@/components/Pagination' import Pagination from "@/components/Pagination"
const FETCH_URL = 'artists/' const FETCH_URL = "artists/"
export default { export default {
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
props: { props: {
defaultQuery: {type: String, required: false, default: ''} defaultQuery: { type: String, required: false, default: "" }
}, },
components: { components: {
ArtistCard, ArtistCard,
Pagination Pagination
}, },
data() { data() {
let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') let defaultOrdering = this.getOrderingFromString(
this.defaultOrdering || "-creation_date"
)
return { return {
isLoading: true, isLoading: true,
result: null, result: null,
page: parseInt(this.defaultPage), page: parseInt(this.defaultPage),
query: this.defaultQuery, query: this.defaultQuery,
paginateBy: parseInt(this.defaultPaginateBy || 12), paginateBy: parseInt(this.defaultPaginateBy || 12),
orderingDirection: defaultOrdering.direction || '+', orderingDirection: defaultOrdering.direction || "+",
ordering: defaultOrdering.field, ordering: defaultOrdering.field,
orderingOptions: [ orderingOptions: [["creation_date", "creation_date"], ["name", "name"]]
['creation_date', 'creation_date'],
['name', 'name']
]
} }
}, },
created() { created() {
this.fetchData() this.fetchData()
}, },
mounted() { mounted() {
$('.ui.dropdown').dropdown() $(".ui.dropdown").dropdown()
}, },
computed: { computed: {
labels() { labels() {
let searchPlaceholder = this.$gettext('Enter an artist name...') let searchPlaceholder = this.$gettext("Enter an artist name...")
let title = this.$gettext('Artists') let title = this.$gettext("Artists")
return { return {
searchPlaceholder, searchPlaceholder,
title title
@ -144,10 +143,10 @@ export default {
page_size: this.paginateBy, page_size: this.paginateBy,
name__icontains: this.query, name__icontains: this.query,
ordering: this.getOrderingAsString(), ordering: this.getOrderingAsString(),
playable: 'true' playable: "true"
} }
logger.default.debug('Fetching artists') logger.default.debug("Fetching artists")
axios.get(url, {params: params}).then((response) => { axios.get(url, { params: params }).then(response => {
self.result = response.data self.result = response.data
self.isLoading = false self.isLoading = false
}) })

View File

@ -1,6 +1,6 @@
<template> <template>
<div v-title="labels.title"> <main v-title="labels.title">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div class="ui stackable three column grid"> <div class="ui stackable three column grid">
<div class="column"> <div class="column">
<track-widget :url="'history/listenings/'" :filters="{scope: 'user', ordering: '-creation_date'}"> <track-widget :url="'history/listenings/'" :filters="{scope: 'user', ordering: '-creation_date'}">
@ -26,23 +26,23 @@
</album-widget> </album-widget>
</div> </div>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import Search from '@/components/audio/Search' import Search from "@/components/audio/Search"
import logger from '@/logging' import logger from "@/logging"
import ArtistCard from '@/components/audio/artist/Card' import ArtistCard from "@/components/audio/artist/Card"
import TrackWidget from '@/components/audio/track/Widget' import TrackWidget from "@/components/audio/track/Widget"
import AlbumWidget from '@/components/audio/album/Widget' import AlbumWidget from "@/components/audio/album/Widget"
import PlaylistWidget from '@/components/playlists/Widget' import PlaylistWidget from "@/components/playlists/Widget"
const ARTISTS_URL = 'artists/' const ARTISTS_URL = "artists/"
export default { export default {
name: 'library', name: "library",
components: { components: {
Search, Search,
ArtistCard, ArtistCard,
@ -62,7 +62,7 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
title: this.$gettext('Home') title: this.$gettext("Home")
} }
} }
}, },
@ -71,14 +71,14 @@ export default {
var self = this var self = this
this.isLoadingArtists = true this.isLoadingArtists = true
let params = { let params = {
ordering: '-creation_date', ordering: "-creation_date",
playable: true playable: true
} }
let url = ARTISTS_URL let url = ARTISTS_URL
logger.default.time('Loading latest artists') logger.default.time("Loading latest artists")
axios.get(url, {params: params}).then((response) => { axios.get(url, { params: params }).then(response => {
self.artists = response.data.results self.artists = response.data.results
logger.default.timeEnd('Loading latest artists') logger.default.timeEnd("Loading latest artists")
self.isLoadingArtists = false self.isLoadingArtists = false
}) })
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="main library pusher"> <div class="main library pusher">
<div class="ui secondary pointing menu"> <nav class="ui secondary pointing menu" role="navigation" :aria-label="labels.secondaryMenu">
<router-link class="ui item" to="/library" exact> <router-link class="ui item" to="/library" exact>
<translate>Browse</translate> <translate>Browse</translate>
</router-link> </router-link>
@ -13,7 +13,7 @@
<router-link class="ui item" to="/library/playlists" exact> <router-link class="ui item" to="/library/playlists" exact>
<translate>Playlists</translate> <translate>Playlists</translate>
</router-link> </router-link>
</div> </nav>
<router-view :key="$route.fullPath"></router-view> <router-view :key="$route.fullPath"></router-view>
</div> </div>
</template> </template>
@ -22,7 +22,15 @@
export default { export default {
computed: { computed: {
showImports() { showImports() {
return this.$store.state.auth.availablePermissions['upload'] || this.$store.state.auth.availablePermissions['library'] return (
this.$store.state.auth.availablePermissions["upload"] ||
this.$store.state.auth.availablePermissions["library"]
)
},
labels() {
return {
secondaryMenu: this.$gettext("Secondary menu")
}
} }
} }
} }
@ -30,7 +38,7 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss"> <style lang="scss">
@import '../../style/vendor/media'; @import "../../style/vendor/media";
.library { .library {
.ui.segment.head { .ui.segment.head {
@ -46,18 +54,16 @@ export default {
} }
&.with-background { &.with-background {
.header { .header {
&, .sub { &,
.sub {
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.8); text-shadow: 0 1px 0 rgba(0, 0, 0, 0.8);
color: white !important; color: white !important;
} }
} }
.segment-content { .segment-content {
background-color: rgba(0, 0, 0, 0.5) background-color: rgba(0, 0, 0, 0.5);
} }
} }
} }
} }
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div v-title="labels.title"> <main v-title="labels.title">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2 class="ui header"> <h2 class="ui header">
<translate>Browsing radios</translate> <translate>Browsing radios</translate>
</h2> </h2>
@ -86,60 +86,59 @@
:total="result.count" :total="result.count"
></pagination> ></pagination>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import _ from 'lodash' import _ from "lodash"
import $ from 'jquery' import $ from "jquery"
import logger from '@/logging' import logger from "@/logging"
import OrderingMixin from '@/components/mixins/Ordering' import OrderingMixin from "@/components/mixins/Ordering"
import PaginationMixin from '@/components/mixins/Pagination' import PaginationMixin from "@/components/mixins/Pagination"
import TranslationsMixin from '@/components/mixins/Translations' import TranslationsMixin from "@/components/mixins/Translations"
import RadioCard from '@/components/radios/Card' import RadioCard from "@/components/radios/Card"
import Pagination from '@/components/Pagination' import Pagination from "@/components/Pagination"
const FETCH_URL = 'radios/radios/' const FETCH_URL = "radios/radios/"
export default { export default {
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
props: { props: {
defaultQuery: {type: String, required: false, default: ''} defaultQuery: { type: String, required: false, default: "" }
}, },
components: { components: {
RadioCard, RadioCard,
Pagination Pagination
}, },
data() { data() {
let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') let defaultOrdering = this.getOrderingFromString(
this.defaultOrdering || "-creation_date"
)
return { return {
isLoading: true, isLoading: true,
result: null, result: null,
page: parseInt(this.defaultPage), page: parseInt(this.defaultPage),
query: this.defaultQuery, query: this.defaultQuery,
paginateBy: parseInt(this.defaultPaginateBy || 12), paginateBy: parseInt(this.defaultPaginateBy || 12),
orderingDirection: defaultOrdering.direction || '+', orderingDirection: defaultOrdering.direction || "+",
ordering: defaultOrdering.field, ordering: defaultOrdering.field,
orderingOptions: [ orderingOptions: [["creation_date", "creation_date"], ["name", "name"]]
['creation_date', 'creation_date'],
['name', 'name']
]
} }
}, },
created() { created() {
this.fetchData() this.fetchData()
}, },
mounted() { mounted() {
$('.ui.dropdown').dropdown() $(".ui.dropdown").dropdown()
}, },
computed: { computed: {
labels() { labels() {
let searchPlaceholder = this.$gettext('Enter a radio name...') let searchPlaceholder = this.$gettext("Enter a radio name...")
let title = this.$gettext('Radios') let title = this.$gettext("Radios")
return { return {
searchPlaceholder, searchPlaceholder,
title title
@ -167,8 +166,8 @@ export default {
name__icontains: this.query, name__icontains: this.query,
ordering: this.getOrderingAsString() ordering: this.getOrderingAsString()
} }
logger.default.debug('Fetching radios') logger.default.debug("Fetching radios")
axios.get(url, {params: params}).then((response) => { axios.get(url, { params: params }).then(response => {
self.result = response.data self.result = response.data
self.isLoading = false self.isLoading = false
}) })

View File

@ -1,10 +1,10 @@
<template> <template>
<div> <main>
<div v-if="isLoadingTrack" class="ui vertical segment" v-title="labels.title"> <div v-if="isLoadingTrack" class="ui vertical segment" v-title="labels.title">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div> </div>
<template v-if="track"> <template v-if="track">
<div :class="['ui', 'head', {'with-background': cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="track.title"> <section :class="['ui', 'head', {'with-background': cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="track.title">
<div class="segment-content"> <div class="segment-content">
<h2 class="ui center aligned icon header"> <h2 class="ui center aligned icon header">
<i class="circular inverted music orange icon"></i> <i class="circular inverted music orange icon"></i>
@ -49,8 +49,8 @@
<translate>Download</translate> <translate>Download</translate>
</a> </a>
</div> </div>
</div> </section>
<div class="ui vertical stripe center aligned segment" v-if="upload"> <section class="ui vertical stripe center aligned segment" v-if="upload">
<h2 class="ui header"><translate>Track information</translate></h2> <h2 class="ui header"><translate>Track information</translate></h2>
<table class="ui very basic collapsing celled center aligned table"> <table class="ui very basic collapsing celled center aligned table">
<tbody> <tbody>
@ -100,8 +100,8 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </section>
<div class="ui vertical stripe center aligned segment"> <section class="ui vertical stripe center aligned segment">
<h2> <h2>
<translate>Lyrics</translate> <translate>Lyrics</translate>
</h2> </h2>
@ -117,34 +117,33 @@
<translate>Search on lyrics.wikia.com</translate> <translate>Search on lyrics.wikia.com</translate>
</a> </a>
</template> </template>
</div> </section>
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2> <h2>
<translate>User libraries</translate> <translate>User libraries</translate>
</h2> </h2>
<library-widget :url="'tracks/' + id + '/libraries/'"> <library-widget :url="'tracks/' + id + '/libraries/'">
<translate slot="subtitle">This track is present in the following libraries:</translate> <translate slot="subtitle">This track is present in the following libraries:</translate>
</library-widget> </library-widget>
</div> </section>
</template> </template>
</div> </main>
</template> </template>
<script> <script>
import time from "@/utils/time"
import axios from "axios"
import url from "@/utils/url"
import logger from "@/logging"
import PlayButton from "@/components/audio/PlayButton"
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon"
import TrackPlaylistIcon from "@/components/playlists/TrackPlaylistIcon"
import LibraryWidget from "@/components/federation/LibraryWidget"
import time from '@/utils/time' const FETCH_URL = "tracks/"
import axios from 'axios'
import url from '@/utils/url'
import logger from '@/logging'
import PlayButton from '@/components/audio/PlayButton'
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
import LibraryWidget from '@/components/federation/LibraryWidget'
const FETCH_URL = 'tracks/'
export default { export default {
props: ['id'], props: ["id"],
components: { components: {
PlayButton, PlayButton,
TrackPlaylistIcon, TrackPlaylistIcon,
@ -168,9 +167,9 @@ export default {
fetchData() { fetchData() {
var self = this var self = this
this.isLoadingTrack = true this.isLoadingTrack = true
let url = FETCH_URL + this.id + '/' let url = FETCH_URL + this.id + "/"
logger.default.debug('Fetching track "' + this.id + '"') logger.default.debug('Fetching track "' + this.id + '"')
axios.get(url).then((response) => { axios.get(url).then(response => {
self.track = response.data self.track = response.data
self.isLoadingTrack = false self.isLoadingTrack = false
}) })
@ -178,21 +177,24 @@ export default {
fetchLyrics() { fetchLyrics() {
var self = this var self = this
this.isLoadingLyrics = true this.isLoadingLyrics = true
let url = FETCH_URL + this.id + '/lyrics/' let url = FETCH_URL + this.id + "/lyrics/"
logger.default.debug('Fetching lyrics for track "' + this.id + '"') logger.default.debug('Fetching lyrics for track "' + this.id + '"')
axios.get(url).then((response) => { axios.get(url).then(
response => {
self.lyrics = response.data self.lyrics = response.data
self.isLoadingLyrics = false self.isLoadingLyrics = false
}, (response) => { },
console.error('No lyrics available') response => {
console.error("No lyrics available")
self.isLoadingLyrics = false self.isLoadingLyrics = false
}) }
)
} }
}, },
computed: { computed: {
labels() { labels() {
return { return {
title: this.$gettext('Track') title: this.$gettext("Track")
} }
}, },
upload() { upload() {
@ -201,23 +203,32 @@ export default {
} }
}, },
wikipediaUrl() { wikipediaUrl() {
return 'https://en.wikipedia.org/w/index.php?search=' + encodeURI(this.track.title + ' ' + this.track.artist.name) return (
"https://en.wikipedia.org/w/index.php?search=" +
encodeURI(this.track.title + " " + this.track.artist.name)
)
}, },
musicbrainzUrl() { musicbrainzUrl() {
if (this.track.mbid) { if (this.track.mbid) {
return 'https://musicbrainz.org/recording/' + this.track.mbid return "https://musicbrainz.org/recording/" + this.track.mbid
} }
}, },
downloadUrl() { downloadUrl() {
let u = this.$store.getters['instance/absoluteUrl'](this.upload.listen_url) let u = this.$store.getters["instance/absoluteUrl"](
this.upload.listen_url
)
if (this.$store.state.auth.authenticated) { if (this.$store.state.auth.authenticated) {
u = url.updateQueryString(u, 'jwt', encodeURI(this.$store.state.auth.token)) u = url.updateQueryString(
u,
"jwt",
encodeURI(this.$store.state.auth.token)
)
} }
return u return u
}, },
lyricsSearchUrl() { lyricsSearchUrl() {
let base = 'http://lyrics.wikia.com/wiki/Special:Search?query=' let base = "http://lyrics.wikia.com/wiki/Special:Search?query="
let query = this.track.artist.name + ' ' + this.track.title let query = this.track.artist.name + " " + this.track.title
return base + encodeURI(query) return base + encodeURI(query)
}, },
cover() { cover() {
@ -225,9 +236,13 @@ export default {
}, },
headerStyle() { headerStyle() {
if (!this.cover) { if (!this.cover) {
return '' return ""
} }
return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.cover) + ')' return (
"background-image: url(" +
this.$store.getters["instance/absoluteUrl"](this.cover) +
")"
)
} }
}, },
watch: { watch: {

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="ui vertical stripe segment" v-title="labels.title"> <div class="ui vertical stripe segment" v-title="labels.title">
<div> <div>
<div> <section>
<h2 class="ui header"> <h2 class="ui header">
<translate>Builder</translate> <translate>Builder</translate>
</h2> </h2>
@ -87,17 +87,17 @@
</h3> </h3>
<track-table v-if="checkResult.candidates.sample" :tracks="checkResult.candidates.sample"></track-table> <track-table v-if="checkResult.candidates.sample" :tracks="checkResult.candidates.sample"></track-table>
</template> </template>
</div> </section>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import $ from 'jquery' import $ from "jquery"
import _ from 'lodash' import _ from "lodash"
import BuilderFilter from './Filter' import BuilderFilter from "./Filter"
import TrackTable from '@/components/audio/track/Table' import TrackTable from "@/components/audio/track/Table"
import RadioButton from '@/components/radios/Button' import RadioButton from "@/components/radios/Button"
export default { export default {
props: { props: {
@ -116,8 +116,8 @@ export default {
currentFilterType: null, currentFilterType: null,
filters: [], filters: [],
checkResult: null, checkResult: null,
radioName: '', radioName: "",
radioDesc: '', radioDesc: "",
isPublic: true isPublic: true
} }
}, },
@ -130,13 +130,13 @@ export default {
}) })
}, },
mounted() { mounted() {
$('.ui.dropdown').dropdown() $(".ui.dropdown").dropdown()
}, },
methods: { methods: {
fetchFilters: function() { fetchFilters: function() {
let self = this let self = this
let url = 'radios/radios/filters/' let url = "radios/radios/filters/"
return axios.get(url).then((response) => { return axios.get(url).then(response => {
self.availableFilters = response.data self.availableFilters = response.data
}) })
}, },
@ -159,12 +159,14 @@ export default {
fetch: function() { fetch: function() {
let self = this let self = this
self.isLoading = true self.isLoading = true
let url = 'radios/radios/' + this.id + '/' let url = "radios/radios/" + this.id + "/"
axios.get(url).then((response) => { axios.get(url).then(response => {
self.filters = response.data.config.map(f => { self.filters = response.data.config.map(f => {
return { return {
config: f, config: f,
filter: this.availableFilters.filter(e => { return e.type === f.type })[0], filter: this.availableFilters.filter(e => {
return e.type === f.type
})[0],
hash: +new Date() hash: +new Date()
} }
}) })
@ -176,18 +178,16 @@ export default {
}, },
fetchCandidates: function() { fetchCandidates: function() {
let self = this let self = this
let url = 'radios/radios/validate/' let url = "radios/radios/validate/"
let final = this.filters.map(f => { let final = this.filters.map(f => {
let c = _.clone(f.config) let c = _.clone(f.config)
c.type = f.filter.type c.type = f.filter.type
return c return c
}) })
final = { final = {
'filters': [ filters: [{ type: "group", filters: final }]
{'type': 'group', filters: final}
]
} }
axios.post(url, final).then((response) => { axios.post(url, final).then(response => {
self.checkResult = response.data.filters[0] self.checkResult = response.data.filters[0]
}) })
}, },
@ -202,24 +202,24 @@ export default {
return c return c
}) })
final = { final = {
'name': this.radioName, name: this.radioName,
'description': this.radioDesc, description: this.radioDesc,
'is_public': this.isPublic, is_public: this.isPublic,
'config': final config: final
} }
if (this.id) { if (this.id) {
let url = 'radios/radios/' + this.id + '/' let url = "radios/radios/" + this.id + "/"
axios.put(url, final).then((response) => { axios.put(url, final).then(response => {
self.isLoading = false self.isLoading = false
self.success = true self.success = true
}) })
} else { } else {
let url = 'radios/radios/' let url = "radios/radios/"
axios.post(url, final).then((response) => { axios.post(url, final).then(response => {
self.success = true self.success = true
self.isLoading = false self.isLoading = false
self.$router.push({ self.$router.push({
name: 'library.radios.detail', name: "library.radios.detail",
params: { params: {
id: response.data.id id: response.data.id
} }
@ -230,10 +230,10 @@ export default {
}, },
computed: { computed: {
labels() { labels() {
let title = this.$gettext('Radio Builder') let title = this.$gettext("Radio Builder")
let placeholder = { let placeholder = {
'name': this.$gettext('My awesome radio'), name: this.$gettext("My awesome radio"),
'description': this.$gettext('My awesome description') description: this.$gettext("My awesome description")
} }
return { return {
title, title,
@ -241,9 +241,7 @@ export default {
} }
}, },
canSave: function() { canSave: function() {
return ( return this.radioName.length > 0 && this.checkErrors.length === 0
this.radioName.length > 0 && this.checkErrors.length === 0
)
}, },
checkErrors: function() { checkErrors: function() {
if (!this.checkResult) { if (!this.checkResult) {

View File

@ -5,9 +5,9 @@
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div> </div>
<template v-if="data.id"> <template v-if="data.id">
<div class="header"> <header class="header">
<a :href="getMusicbrainzUrl('artist', data.id)" target="_blank" :title="labels.musicbrainz">{{ data.name }}</a> <a :href="getMusicbrainzUrl('artist', data.id)" target="_blank" :title="labels.musicbrainz">{{ data.name }}</a>
</div> </header>
<div class="description"> <div class="description">
<table class="ui very basic fixed single line compact table"> <table class="ui very basic fixed single line compact table">
<tbody> <tbody>
@ -32,9 +32,9 @@
</template> </template>
<script> <script>
import Vue from 'vue' import Vue from "vue"
import CardMixin from './CardMixin' import CardMixin from "./CardMixin"
import time from '@/utils/time' import time from "@/utils/time"
export default Vue.extend({ export default Vue.extend({
mixins: [CardMixin], mixins: [CardMixin],
@ -46,15 +46,15 @@ export default Vue.extend({
computed: { computed: {
labels() { labels() {
return { return {
musicbrainz: this.$gettext('View on MusicBrainz') musicbrainz: this.$gettext("View on MusicBrainz")
} }
}, },
type() { type() {
return 'artist' return "artist"
}, },
releasesGroups() { releasesGroups() {
return this.data['release-group-list'].filter(r => { return this.data["release-group-list"].filter(r => {
return r.type === 'Album' return r.type === "Album"
}) })
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="main pusher" v-title="labels.title"> <main class="main pusher" v-title="labels.title">
<div class="ui vertical aligned stripe segment"> <section class="ui vertical aligned stripe segment">
<div v-if="isLoading" :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']"> <div v-if="isLoading" :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']">
<div class="ui text loader"><translate>Loading notifications...</translate></div> <div class="ui text loader"><translate>Loading notifications...</translate></div>
</div> </div>
@ -27,16 +27,16 @@
<translate>We don't have any notification to display!</translate> <translate>We don't have any notification to display!</translate>
</p> </p>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import {mapState} from 'vuex' import { mapState } from "vuex"
import axios from 'axios' import axios from "axios"
import logger from '@/logging' import logger from "@/logging"
import NotificationRow from '@/components/notifications/NotificationRow' import NotificationRow from "@/components/notifications/NotificationRow"
export default { export default {
data() { data() {
@ -53,16 +53,16 @@ export default {
}, },
created() { created() {
this.fetch(this.filters) this.fetch(this.filters)
this.$store.commit('ui/addWebsocketEventHandler', { this.$store.commit("ui/addWebsocketEventHandler", {
eventName: 'inbox.item_added', eventName: "inbox.item_added",
id: 'notificationPage', id: "notificationPage",
handler: this.handleNewNotification handler: this.handleNewNotification
}) })
}, },
destroyed() { destroyed() {
this.$store.commit('ui/removeWebsocketEventHandler', { this.$store.commit("ui/removeWebsocketEventHandler", {
eventName: 'inbox.item_added', eventName: "inbox.item_added",
id: 'notificationPage', id: "notificationPage"
}) })
}, },
computed: { computed: {
@ -71,7 +71,7 @@ export default {
}), }),
labels() { labels() {
return { return {
title: this.$gettext('Notifications'), title: this.$gettext("Notifications")
} }
} }
}, },
@ -82,7 +82,7 @@ export default {
fetch(params) { fetch(params) {
this.isLoading = true this.isLoading = true
let self = this let self = this
axios.get('federation/inbox/', {params: params}).then((response) => { axios.get("federation/inbox/", { params: params }).then(response => {
self.isLoading = false self.isLoading = false
self.notifications = response.data self.notifications = response.data
}) })
@ -91,24 +91,23 @@ export default {
let self = this let self = this
let before = this.notifications.results[0].id let before = this.notifications.results[0].id
let payload = { let payload = {
action: 'read', action: "read",
objects: 'all', objects: "all",
filters: { filters: {
is_read: false, is_read: false,
before before
} }
} }
axios.post('federation/inbox/action/', payload).then((response) => { axios.post("federation/inbox/action/", payload).then(response => {
self.$store.commit('ui/notifications', {type: 'inbox', count: 0}) self.$store.commit("ui/notifications", { type: "inbox", count: 0 })
self.notifications.results.forEach(n => { self.notifications.results.forEach(n => {
n.is_read = true n.is_read = true
}) })
}) })
}, }
}, },
watch: { watch: {
'filters.is_read' () { "filters.is_read"() {
this.fetch(this.filters) this.fetch(this.filters)
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="main pusher" v-title="labels.settings"> <main class="main pusher" v-title="labels.settings">
<div class="ui vertical stripe segment"> <div class="ui vertical stripe segment">
<div class="ui text container"> <div class="ui text container">
<div :class="['ui', {'loading': isLoading}, 'form']"></div> <div :class="['ui', {'loading': isLoading}, 'form']"></div>
@ -24,14 +24,14 @@
</div> </div>
</div> </div>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import $ from 'jquery' import $ from "jquery"
import SettingsGroup from '@/components/admin/SettingsGroup' import SettingsGroup from "@/components/admin/SettingsGroup"
export default { export default {
components: { components: {
@ -51,7 +51,7 @@ export default {
if (self.$store.state.route.hash) { if (self.$store.state.route.hash) {
self.scrollTo(self.$store.state.route.hash.substr(1)) self.scrollTo(self.$store.state.route.hash.substr(1))
} }
$('select.dropdown').dropdown() $("select.dropdown").dropdown()
}) })
}) })
}, },
@ -63,7 +63,7 @@ export default {
fetchSettings() { fetchSettings() {
let self = this let self = this
self.isLoading = true self.isLoading = true
return axios.get('instance/admin/settings/').then((response) => { return axios.get("instance/admin/settings/").then(response => {
self.settingsData = response.data self.settingsData = response.data
self.isLoading = false self.isLoading = false
}) })
@ -72,89 +72,81 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
settings: this.$gettext('Instance settings') settings: this.$gettext("Instance settings")
} }
}, },
groups() { groups() {
// somehow, extraction fails if in the return block directly // somehow, extraction fails if in the return block directly
let instanceLabel = this.$gettext('Instance information') let instanceLabel = this.$gettext("Instance information")
let usersLabel = this.$gettext('Users') let usersLabel = this.$gettext("Users")
let musicLabel = this.$gettext('Music') let musicLabel = this.$gettext("Music")
let playlistsLabel = this.$gettext('Playlists') let playlistsLabel = this.$gettext("Playlists")
let federationLabel = this.$gettext('Federation') let federationLabel = this.$gettext("Federation")
let subsonicLabel = this.$gettext('Subsonic') let subsonicLabel = this.$gettext("Subsonic")
let statisticsLabel = this.$gettext('Statistics') let statisticsLabel = this.$gettext("Statistics")
let errorLabel = this.$gettext('Error reporting') let errorLabel = this.$gettext("Error reporting")
return [ return [
{ {
label: instanceLabel, label: instanceLabel,
id: 'instance', id: "instance",
settings: [ settings: [
'instance__name', "instance__name",
'instance__short_description', "instance__short_description",
'instance__long_description' "instance__long_description"
] ]
}, },
{ {
label: usersLabel, label: usersLabel,
id: 'users', id: "users",
settings: [ settings: [
'users__registration_enabled', "users__registration_enabled",
'common__api_authentication_required', "common__api_authentication_required",
'users__default_permissions', "users__default_permissions",
'users__upload_quota' "users__upload_quota"
] ]
}, },
{ {
label: musicLabel, label: musicLabel,
id: 'music', id: "music",
settings: [ settings: [
'music__transcoding_enabled', "music__transcoding_enabled",
'music__transcoding_cache_duration', "music__transcoding_cache_duration"
] ]
}, },
{ {
label: playlistsLabel, label: playlistsLabel,
id: 'playlists', id: "playlists",
settings: [ settings: ["playlists__max_tracks"]
'playlists__max_tracks'
]
}, },
{ {
label: federationLabel, label: federationLabel,
id: 'federation', id: "federation",
settings: [ settings: [
'federation__enabled', "federation__enabled",
'federation__music_needs_approval', "federation__music_needs_approval",
'federation__collection_page_size', "federation__collection_page_size",
'federation__music_cache_duration', "federation__music_cache_duration",
'federation__actor_fetch_delay' "federation__actor_fetch_delay"
] ]
}, },
{ {
label: subsonicLabel, label: subsonicLabel,
id: 'subsonic', id: "subsonic",
settings: [ settings: ["subsonic__enabled"]
'subsonic__enabled'
]
}, },
{ {
label: statisticsLabel, label: statisticsLabel,
id: 'statistics', id: "statistics",
settings: [ settings: [
'instance__nodeinfo_enabled', "instance__nodeinfo_enabled",
'instance__nodeinfo_stats_enabled', "instance__nodeinfo_stats_enabled",
'instance__nodeinfo_private' "instance__nodeinfo_private"
] ]
}, },
{ {
label: errorLabel, label: errorLabel,
id: 'reporting', id: "reporting",
settings: [ settings: ["raven__front_enabled", "raven__front_dsn"]
'raven__front_enabled',
'raven__front_dsn'
]
} }
] ]
} }
@ -163,7 +155,9 @@ export default {
settingsData() { settingsData() {
let self = this let self = this
this.$nextTick(() => { this.$nextTick(() => {
$(self.$el).find('.sticky').sticky({context: '#settings-grid'}) $(self.$el)
.find(".sticky")
.sticky({ context: "#settings-grid" })
}) })
} }
} }

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="main pusher" v-title="labels.title"> <div class="main pusher" v-title="labels.title">
<div class="ui secondary pointing menu"> <nav class="ui secondary pointing menu" role="navigation" :aria-label="labels.secondaryMenu">
<router-link <router-link
class="ui item" class="ui item"
:to="{name: 'manage.library.files'}"><translate>Files</translate></router-link> :to="{name: 'manage.library.files'}"><translate>Files</translate></router-link>
</div> </nav>
<router-view :key="$route.fullPath"></router-view> <router-view :key="$route.fullPath"></router-view>
</div> </div>
</template> </template>
@ -13,9 +13,11 @@
export default { export default {
computed: { computed: {
labels() { labels() {
let title = this.$gettext('Manage library') let title = this.$gettext("Manage library")
let secondaryMenu = this.$gettext("Secondary menu")
return { return {
title title,
secondaryMenu
} }
} }
} }
@ -23,10 +25,8 @@ export default {
</script> </script>
<style scoped> <style scoped>
.ui.menu .item > .label { .ui.menu .item > .label {
position: absolute; position: absolute;
right: -2em; right: -2em;
} }
</style> </style>

View File

@ -1,15 +1,15 @@
<template> <template>
<div v-title="labels.title"> <main v-title="labels.title">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2 class="ui header"><translate>Library files</translate></h2> <h2 class="ui header"><translate>Library files</translate></h2>
<div class="ui hidden divider"></div> <div class="ui hidden divider"></div>
<library-files-table :show-library="true"></library-files-table> <library-files-table :show-library="true"></library-files-table>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import LibraryFilesTable from '@/components/manage/library/FilesTable' import LibraryFilesTable from "@/components/manage/library/FilesTable"
export default { export default {
components: { components: {
@ -18,7 +18,7 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
title: this.$gettext('Files') title: this.$gettext("Files")
} }
} }
} }

View File

@ -1,13 +1,13 @@
<template> <template>
<div class="main pusher" v-title="labels.manageUsers"> <div class="main pusher" v-title="labels.manageUsers">
<div class="ui secondary pointing menu"> <nav class="ui secondary pointing menu" role="navigation" :aria-label="labels.secondaryMenu">
<router-link <router-link
class="ui item" class="ui item"
:to="{name: 'manage.users.users.list'}"><translate>Users</translate></router-link> :to="{name: 'manage.users.users.list'}"><translate>Users</translate></router-link>
<router-link <router-link
class="ui item" class="ui item"
:to="{name: 'manage.users.invitations.list'}"><translate>Invitations</translate></router-link> :to="{name: 'manage.users.invitations.list'}"><translate>Invitations</translate></router-link>
</div> </nav>
<router-view :key="$route.fullPath"></router-view> <router-view :key="$route.fullPath"></router-view>
</div> </div>
</template> </template>
@ -17,7 +17,8 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
manageUsers: this.$gettext('Manage users') manageUsers: this.$gettext("Manage users"),
secondaryMenu: this.$gettext("Secondary menu")
} }
} }
} }

View File

@ -1,17 +1,17 @@
<template> <template>
<div v-title="labels.invitations"> <main v-title="labels.invitations">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2 class="ui header"><translate>Invitations</translate></h2> <h2 class="ui header"><translate>Invitations</translate></h2>
<invitation-form></invitation-form> <invitation-form></invitation-form>
<div class="ui hidden divider"></div> <div class="ui hidden divider"></div>
<invitations-table></invitations-table> <invitations-table></invitations-table>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import InvitationForm from '@/components/manage/users/InvitationForm' import InvitationForm from "@/components/manage/users/InvitationForm"
import InvitationsTable from '@/components/manage/users/InvitationsTable' import InvitationsTable from "@/components/manage/users/InvitationsTable"
export default { export default {
components: { components: {
@ -21,7 +21,7 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
invitations: this.$gettext('Invitations') invitations: this.$gettext("Invitations")
} }
} }
} }

View File

@ -1,10 +1,10 @@
<template> <template>
<div> <main>
<div v-if="isLoading" class="ui vertical segment"> <div v-if="isLoading" class="ui vertical segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div> </div>
<template v-if="object"> <template v-if="object">
<div :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']" v-title="object.username"> <section :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']" v-title="object.username">
<div class="segment-content"> <div class="segment-content">
<h2 class="ui center aligned icon header"> <h2 class="ui center aligned icon header">
<i class="circular inverted user red icon"></i> <i class="circular inverted user red icon"></i>
@ -102,19 +102,18 @@
</div> </div>
<div class="ui hidden divider"></div> <div class="ui hidden divider"></div>
<button @click="fetchData" class="ui basic button"><translate>Refresh</translate></button> <button @click="fetchData" class="ui basic button"><translate>Refresh</translate></button>
</div> </section>
</template> </template>
</div> </main>
</template> </template>
<script> <script>
import $ from "jquery"
import $ from 'jquery' import axios from "axios"
import axios from 'axios' import logger from "@/logging"
import logger from '@/logging'
export default { export default {
props: ['id'], props: ["id"],
data() { data() {
return { return {
isLoading: true, isLoading: true,
@ -129,8 +128,8 @@ export default {
fetchData() { fetchData() {
var self = this var self = this
this.isLoading = true this.isLoading = true
let url = 'manage/users/users/' + this.id + '/' let url = "manage/users/users/" + this.id + "/"
axios.get(url).then((response) => { axios.get(url).then(response => {
self.object = response.data self.object = response.data
self.permissions = [] self.permissions = []
self.allPermissions.forEach(p => { self.allPermissions.forEach(p => {
@ -146,47 +145,59 @@ export default {
if (toNull && !newValue) { if (toNull && !newValue) {
newValue = null newValue = null
} }
console.log(newValue, typeof(newValue)) console.log(newValue, typeof newValue)
let params = {} let params = {}
if (attr === 'permissions') { if (attr === "permissions") {
params['permissions'] = {} params["permissions"] = {}
this.allPermissions.forEach(p => { this.allPermissions.forEach(p => {
params['permissions'][p.code] = this.permissions.indexOf(p.code) > -1 params["permissions"][p.code] = this.permissions.indexOf(p.code) > -1
}) })
} else { } else {
params[attr] = newValue params[attr] = newValue
} }
axios.patch('manage/users/users/' + this.id + '/', params).then((response) => { axios.patch("manage/users/users/" + this.id + "/", params).then(
logger.default.info(`${attr} was updated succcessfully to ${newValue}`) response => {
}, (error) => { logger.default.info(
logger.default.error(`Error while setting ${attr} to ${newValue}`, error) `${attr} was updated succcessfully to ${newValue}`
}) )
},
error => {
logger.default.error(
`Error while setting ${attr} to ${newValue}`,
error
)
}
)
} }
}, },
computed: { computed: {
labels() { labels() {
return { return {
inactive: this.$gettext('Determine if the user account is active or not. Inactive users cannot login or use the service.'), inactive: this.$gettext(
uploadQuota: this.$gettext('Determine how much content the user can upload. Leave empty to use the default value of the instance.') "Determine if the user account is active or not. Inactive users cannot login or use the service."
),
uploadQuota: this.$gettext(
"Determine how much content the user can upload. Leave empty to use the default value of the instance."
)
} }
}, },
allPermissions() { allPermissions() {
return [ return [
{ {
'code': 'upload', code: "upload",
'label': this.$gettext('Upload') label: this.$gettext("Upload")
}, },
{ {
'code': 'library', code: "library",
'label': this.$gettext('Library') label: this.$gettext("Library")
}, },
{ {
'code': 'federation', code: "federation",
'label': this.$gettext('Federation') label: this.$gettext("Federation")
}, },
{ {
'code': 'settings', code: "settings",
'label': this.$gettext('Settings') label: this.$gettext("Settings")
} }
] ]
} }
@ -194,7 +205,7 @@ export default {
watch: { watch: {
object() { object() {
this.$nextTick(() => { this.$nextTick(() => {
$('select.dropdown').dropdown() $("select.dropdown").dropdown()
}) })
} }
} }

View File

@ -1,15 +1,15 @@
<template> <template>
<div v-title="labels.users"> <main v-title="labels.users">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2 class="ui header"><translate>Users</translate></h2> <h2 class="ui header"><translate>Users</translate></h2>
<div class="ui hidden divider"></div> <div class="ui hidden divider"></div>
<users-table></users-table> <users-table></users-table>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import UsersTable from '@/components/manage/users/UsersTable' import UsersTable from "@/components/manage/users/UsersTable"
export default { export default {
components: { components: {
@ -18,7 +18,7 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
users: this.$gettext('Users') users: this.$gettext("Users")
} }
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="main pusher" v-title="labels.confirm"> <main class="main pusher" v-title="labels.confirm">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div class="ui small text container"> <div class="ui small text container">
<h2><translate>Confirm your email</translate></h2> <h2><translate>Confirm your email</translate></h2>
<form v-if="!success" class="ui form" @submit.prevent="submit()"> <form v-if="!success" class="ui form" @submit.prevent="submit()">
@ -28,15 +28,15 @@
</router-link> </router-link>
</div> </div>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
export default { export default {
props: ['defaultKey'], props: ["defaultKey"],
data() { data() {
return { return {
isLoading: false, isLoading: false,
@ -48,7 +48,7 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
confirm: this.$gettext('Confirm your email') confirm: this.$gettext("Confirm your email")
} }
} }
}, },
@ -60,16 +60,18 @@ export default {
let payload = { let payload = {
key: this.key key: this.key
} }
return axios.post('auth/registration/verify-email/', payload).then(response => { return axios.post("auth/registration/verify-email/", payload).then(
response => {
self.isLoading = false self.isLoading = false
self.success = true self.success = true
}, error => { },
error => {
self.errors = error.backendErrors self.errors = error.backendErrors
self.isLoading = false self.isLoading = false
}) }
)
} }
} }
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="main pusher" v-title="labels.reset"> <main class="main pusher" v-title="labels.reset">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div class="ui small text container"> <div class="ui small text container">
<h2><translate>Reset your password</translate></h2> <h2><translate>Reset your password</translate></h2>
<form class="ui form" @submit.prevent="submit()"> <form class="ui form" @submit.prevent="submit()">
@ -28,15 +28,15 @@
<translate>Ask for a password reset</translate></button> <translate>Ask for a password reset</translate></button>
</form> </form>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
export default { export default {
props: ['defaultEmail'], props: ["defaultEmail"],
data() { data() {
return { return {
email: this.defaultEmail, email: this.defaultEmail,
@ -49,8 +49,10 @@ export default {
}, },
computed: { computed: {
labels() { labels() {
let reset = this.$gettext('Reset your password') let reset = this.$gettext("Reset your password")
let placeholder = this.$gettext('Input the email address binded to your account') let placeholder = this.$gettext(
"Input the email address binded to your account"
)
return { return {
reset, reset,
placeholder placeholder
@ -65,18 +67,20 @@ export default {
let payload = { let payload = {
email: this.email email: this.email
} }
return axios.post('auth/password/reset/', payload).then(response => { return axios.post("auth/password/reset/", payload).then(
response => {
self.isLoading = false self.isLoading = false
self.$router.push({ self.$router.push({
name: 'auth.password-reset-confirm' name: "auth.password-reset-confirm"
}) })
}, error => { },
error => {
self.errors = error.backendErrors self.errors = error.backendErrors
self.isLoading = false self.isLoading = false
}) }
)
} }
} }
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="main pusher" v-title="labels.changePassword"> <main class="main pusher" v-title="labels.changePassword">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<div class="ui small text container"> <div class="ui small text container">
<h2><translate>Change your password</translate></h2> <h2><translate>Change your password</translate></h2>
<form v-if="!success" class="ui form" @submit.prevent="submit()"> <form v-if="!success" class="ui form" @submit.prevent="submit()">
@ -33,22 +33,22 @@
</router-link> </router-link>
</div> </div>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import PasswordInput from '@/components/forms/PasswordInput' import PasswordInput from "@/components/forms/PasswordInput"
export default { export default {
props: ['defaultToken', 'defaultUid'], props: ["defaultToken", "defaultUid"],
components: { components: {
PasswordInput PasswordInput
}, },
data() { data() {
return { return {
newPassword: '', newPassword: "",
isLoading: false, isLoading: false,
errors: [], errors: [],
token: this.defaultToken, token: this.defaultToken,
@ -59,7 +59,7 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
changePassword: this.$gettext('Change your password') changePassword: this.$gettext("Change your password")
} }
} }
}, },
@ -74,16 +74,18 @@ export default {
new_password1: this.newPassword, new_password1: this.newPassword,
new_password2: this.newPassword new_password2: this.newPassword
} }
return axios.post('auth/password/reset/confirm/', payload).then(response => { return axios.post("auth/password/reset/confirm/", payload).then(
response => {
self.isLoading = false self.isLoading = false
self.success = true self.success = true
}, error => { },
error => {
self.errors = error.backendErrors self.errors = error.backendErrors
self.isLoading = false self.isLoading = false
}) }
)
} }
} }
} }
</script> </script>

View File

@ -1,24 +1,25 @@
<template> <template>
<div class="main pusher" v-title="labels.title"> <main class="main pusher" v-title="labels.title">
<div class="ui secondary pointing menu"> <nav class="ui secondary pointing menu" role="navigation" :aria-label="labels.secondaryMenu">
<router-link <router-link
class="ui item" class="ui item"
:to="{name: 'content.libraries.index'}"><translate>Libraries</translate></router-link> :to="{name: 'content.libraries.index'}"><translate>Libraries</translate></router-link>
<router-link <router-link
class="ui item" class="ui item"
:to="{name: 'content.libraries.files'}"><translate>Tracks</translate></router-link> :to="{name: 'content.libraries.files'}"><translate>Tracks</translate></router-link>
</div> </nav>
<router-view :key="$route.fullPath"></router-view> <router-view :key="$route.fullPath"></router-view>
</div> </main>
</template> </template>
<script> <script>
export default { export default {
computed: { computed: {
labels() { labels() {
let title = this.$gettext('Add content') let title = this.$gettext("Add content")
let secondaryMenu = this.$gettext("Secondary menu")
return { return {
title title,
secondaryMenu
} }
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="ui vertical aligned stripe segment" v-title="labels.title"> <section class="ui vertical aligned stripe segment" v-title="labels.title">
<div class="ui text container"> <div class="ui text container">
<h1>{{ labels.title }}</h1> <h1>{{ labels.title }}</h1>
<p><translate>We offer various way to grab new content and make it available here.</translate></p> <p><translate>We offer various way to grab new content and make it available here.</translate></p>
@ -22,21 +22,24 @@
</div> </div>
</div> </div>
</div> </section>
</template> </template>
<script> <script>
import {humanSize} from '@/filters' import { humanSize } from "@/filters"
export default { export default {
computed: { computed: {
labels() { labels() {
return { return {
title: this.$gettext('Add and manage content') title: this.$gettext("Add and manage content")
} }
}, },
defaultQuota() { defaultQuota() {
let quota = this.$store.state.instance.settings.users.upload_quota.value * 1000 * 1000 let quota =
this.$store.state.instance.settings.users.upload_quota.value *
1000 *
1000
return humanSize(quota) return humanSize(quota)
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="ui vertical aligned stripe segment"> <section class="ui vertical aligned stripe segment">
<div v-if="isLoadingLibrary" :class="['ui', {'active': isLoadingLibrary}, 'inverted', 'dimmer']"> <div v-if="isLoadingLibrary" :class="['ui', {'active': isLoadingLibrary}, 'inverted', 'dimmer']">
<div class="ui text loader"><translate>Loading library data...</translate></div> <div class="ui text loader"><translate>Loading library data...</translate></div>
</div> </div>
@ -64,15 +64,15 @@
<library-form :library="library" @updated="libraryUpdated" @deleted="libraryDeleted" /> <library-form :library="library" @updated="libraryUpdated" @deleted="libraryDeleted" />
</div> </div>
</detail-area> </detail-area>
</div> </section>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import DetailMixin from './DetailMixin' import DetailMixin from "./DetailMixin"
import DetailArea from './DetailArea' import DetailArea from "./DetailArea"
import LibraryForm from './Form' import LibraryForm from "./Form"
import LibraryFilesTable from './FilesTable' import LibraryFilesTable from "./FilesTable"
export default { export default {
mixins: [DetailMixin], mixins: [DetailMixin],
@ -83,7 +83,7 @@ export default {
}, },
data() { data() {
return { return {
currentTab: 'follows', currentTab: "follows",
isLoadingFollows: false, isLoadingFollows: false,
follows: null follows: null
} }
@ -98,13 +98,13 @@ export default {
}, },
libraryDeleted() { libraryDeleted() {
this.$router.push({ this.$router.push({
name: 'content.libraries.index' name: "content.libraries.index"
}) })
}, },
fetchFollows() { fetchFollows() {
let self = this let self = this
self.isLoadingLibrary = true self.isLoadingLibrary = true
axios.get(`libraries/${this.id}/follows/`).then((response) => { axios.get(`libraries/${this.id}/follows/`).then(response => {
self.follows = response.data self.follows = response.data
self.isLoadingFollows = false self.isLoadingFollows = false
}) })
@ -113,11 +113,13 @@ export default {
let self = this let self = this
let action let action
if (value) { if (value) {
action = 'accept' action = "accept"
} else { } else {
action = 'reject' action = "reject"
} }
axios.post(`federation/follows/library/${follow.uuid}/${action}/`).then((response) => { axios
.post(`federation/follows/library/${follow.uuid}/${action}/`)
.then(response => {
follow.isLoading = false follow.isLoading = false
follow.approved = value follow.approved = value
}) })

View File

@ -1,14 +1,14 @@
<template> <template>
<div class="ui vertical aligned stripe segment"> <section class="ui vertical aligned stripe segment">
<library-files-table :default-query="query"></library-files-table> <library-files-table :default-query="query"></library-files-table>
</div> </section>
</template> </template>
<script> <script>
import LibraryFilesTable from './FilesTable' import LibraryFilesTable from "./FilesTable"
export default { export default {
props: ['query'], props: ["query"],
components: { components: {
LibraryFilesTable LibraryFilesTable
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="ui vertical aligned stripe segment"> <section class="ui vertical aligned stripe segment">
<div v-if="isLoading" :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']"> <div v-if="isLoading" :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']">
<div class="ui text loader"><translate>Loading Libraries...</translate></div> <div class="ui text loader"><translate>Loading Libraries...</translate></div>
</div> </div>
@ -24,14 +24,14 @@
</div> </div>
</div> </div>
</div> </div>
</div> </section>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import LibraryForm from './Form' import LibraryForm from "./Form"
import LibraryCard from './Card' import LibraryCard from "./Card"
import Quota from './Quota' import Quota from "./Quota"
export default { export default {
data() { data() {
@ -53,7 +53,7 @@ export default {
fetch() { fetch() {
this.isLoading = true this.isLoading = true
let self = this let self = this
axios.get('libraries/').then((response) => { axios.get("libraries/").then(response => {
self.isLoading = false self.isLoading = false
self.libraries = response.data.results self.libraries = response.data.results
if (self.libraries.length === 0) { if (self.libraries.length === 0) {

View File

@ -1,9 +1,9 @@
<template> <template>
<div> <main>
<div v-if="isLoading" class="ui vertical segment" v-title="labels.playlist"> <div v-if="isLoading" class="ui vertical segment" v-title="labels.playlist">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div> </div>
<div v-if="!isLoading && playlist" class="ui head vertical center aligned stripe segment" v-title="playlist.name"> <section v-if="!isLoading && playlist" class="ui head vertical center aligned stripe segment" v-title="playlist.name">
<div class="segment-content"> <div class="segment-content">
<h2 class="ui center aligned icon header"> <h2 class="ui center aligned icon header">
<i class="circular inverted list yellow icon"></i> <i class="circular inverted list yellow icon"></i>
@ -39,8 +39,8 @@
<p slot="modal-confirm"><translate>Delete playlist</translate></p> <p slot="modal-confirm"><translate>Delete playlist</translate></p>
</dangerous-button> </dangerous-button>
</div> </div>
</div> </section>
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<template v-if="edit"> <template v-if="edit">
<playlist-editor <playlist-editor
@playlist-updated="playlist = $event" @playlist-updated="playlist = $event"
@ -51,15 +51,15 @@
<h2><translate>Tracks</translate></h2> <h2><translate>Tracks</translate></h2>
<track-table :display-position="true" :tracks="tracks"></track-table> <track-table :display-position="true" :tracks="tracks"></track-table>
</template> </template>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import TrackTable from '@/components/audio/track/Table' import TrackTable from "@/components/audio/track/Table"
import RadioButton from '@/components/radios/Button' import RadioButton from "@/components/radios/Button"
import PlayButton from '@/components/audio/PlayButton' import PlayButton from "@/components/audio/PlayButton"
import PlaylistEditor from '@/components/playlists/Editor' import PlaylistEditor from "@/components/playlists/Editor"
export default { export default {
props: { props: {
@ -87,7 +87,7 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
playlist: this.$gettext('Playlist') playlist: this.$gettext("Playlist")
} }
} }
}, },
@ -103,23 +103,26 @@ export default {
fetch: function() { fetch: function() {
let self = this let self = this
self.isLoading = true self.isLoading = true
let url = 'playlists/' + this.id + '/' let url = "playlists/" + this.id + "/"
axios.get(url).then((response) => { axios.get(url).then(response => {
self.playlist = response.data self.playlist = response.data
axios.get(url + 'tracks/').then((response) => { axios
.get(url + "tracks/")
.then(response => {
self.updatePlts(response.data.results) self.updatePlts(response.data.results)
}).then(() => { })
.then(() => {
self.isLoading = false self.isLoading = false
}) })
}) })
}, },
deletePlaylist() { deletePlaylist() {
let self = this let self = this
let url = 'playlists/' + this.id + '/' let url = "playlists/" + this.id + "/"
axios.delete(url).then((response) => { axios.delete(url).then(response => {
self.$store.dispatch('playlists/fetchOwn') self.$store.dispatch("playlists/fetchOwn")
self.$router.push({ self.$router.push({
path: '/library' path: "/library"
}) })
}) })
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div v-title="labels.playlists"> <main v-title="labels.playlists">
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2 class="ui header"><translate>Browsing playlists</translate></h2> <h2 class="ui header"><translate>Browsing playlists</translate></h2>
<div :class="['ui', {'loading': isLoading}, 'form']"> <div :class="['ui', {'loading': isLoading}, 'form']">
<template v-if="$store.state.auth.authenticated"> <template v-if="$store.state.auth.authenticated">
@ -50,46 +50,48 @@
:total="result.count" :total="result.count"
></pagination> ></pagination>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import _ from 'lodash' import _ from "lodash"
import $ from 'jquery' import $ from "jquery"
import OrderingMixin from '@/components/mixins/Ordering' import OrderingMixin from "@/components/mixins/Ordering"
import PaginationMixin from '@/components/mixins/Pagination' import PaginationMixin from "@/components/mixins/Pagination"
import TranslationsMixin from '@/components/mixins/Translations' import TranslationsMixin from "@/components/mixins/Translations"
import PlaylistCardList from '@/components/playlists/CardList' import PlaylistCardList from "@/components/playlists/CardList"
import Pagination from '@/components/Pagination' import Pagination from "@/components/Pagination"
const FETCH_URL = 'playlists/' const FETCH_URL = "playlists/"
export default { export default {
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
props: { props: {
defaultQuery: {type: String, required: false, default: ''} defaultQuery: { type: String, required: false, default: "" }
}, },
components: { components: {
PlaylistCardList, PlaylistCardList,
Pagination Pagination
}, },
data() { data() {
let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') let defaultOrdering = this.getOrderingFromString(
this.defaultOrdering || "-creation_date"
)
return { return {
isLoading: true, isLoading: true,
result: null, result: null,
page: parseInt(this.defaultPage), page: parseInt(this.defaultPage),
query: this.defaultQuery, query: this.defaultQuery,
paginateBy: parseInt(this.defaultPaginateBy || 12), paginateBy: parseInt(this.defaultPaginateBy || 12),
orderingDirection: defaultOrdering.direction || '+', orderingDirection: defaultOrdering.direction || "+",
ordering: defaultOrdering.field, ordering: defaultOrdering.field,
orderingOptions: [ orderingOptions: [
['creation_date', 'creation_date'], ["creation_date", "creation_date"],
['modification_date', 'modification_date'], ["modification_date", "modification_date"],
['name', 'name'] ["name", "name"]
] ]
} }
}, },
@ -97,12 +99,12 @@ export default {
this.fetchData() this.fetchData()
}, },
mounted() { mounted() {
$('.ui.dropdown').dropdown() $(".ui.dropdown").dropdown()
}, },
computed: { computed: {
labels() { labels() {
let playlists = this.$gettext('Playlists') let playlists = this.$gettext("Playlists")
let searchPlaceholder = this.$gettext('Enter an playlist name...') let searchPlaceholder = this.$gettext("Enter an playlist name...")
return { return {
playlists, playlists,
searchPlaceholder searchPlaceholder
@ -130,7 +132,7 @@ export default {
q: this.query, q: this.query,
ordering: this.getOrderingAsString() ordering: this.getOrderingAsString()
} }
axios.get(url, {params: params}).then((response) => { axios.get(url, { params: params }).then(response => {
self.result = response.data self.result = response.data
self.isLoading = false self.isLoading = false
}) })

View File

@ -1,9 +1,9 @@
<template> <template>
<div> <main>
<div v-if="isLoading" class="ui vertical segment" v-title="labels.title"> <div v-if="isLoading" class="ui vertical segment" v-title="labels.title">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div> </div>
<div v-if="!isLoading && radio" class="ui head vertical center aligned stripe segment" v-title="radio.name"> <section v-if="!isLoading && radio" class="ui head vertical center aligned stripe segment" v-title="radio.name">
<div class="segment-content"> <div class="segment-content">
<h2 class="ui center aligned icon header"> <h2 class="ui center aligned icon header">
<i class="circular inverted feed blue icon"></i> <i class="circular inverted feed blue icon"></i>
@ -30,8 +30,8 @@
</dangerous-button> </dangerous-button>
</template> </template>
</div> </div>
</div> </section>
<div class="ui vertical stripe segment"> <section class="ui vertical stripe segment">
<h2><translate>Tracks</translate></h2> <h2><translate>Tracks</translate></h2>
<track-table :tracks="tracks"></track-table> <track-table :tracks="tracks"></track-table>
<div class="ui center aligned basic segment"> <div class="ui center aligned basic segment">
@ -43,15 +43,15 @@
:total="totalTracks" :total="totalTracks"
></pagination> ></pagination>
</div> </div>
</div> </section>
</div> </main>
</template> </template>
<script> <script>
import axios from 'axios' import axios from "axios"
import TrackTable from '@/components/audio/track/Table' import TrackTable from "@/components/audio/track/Table"
import RadioButton from '@/components/radios/Button' import RadioButton from "@/components/radios/Button"
import Pagination from '@/components/Pagination' import Pagination from "@/components/Pagination"
export default { export default {
props: { props: {
@ -77,7 +77,7 @@ export default {
computed: { computed: {
labels() { labels() {
return { return {
title: this.$gettext('Radio') title: this.$gettext("Radio")
} }
} }
}, },
@ -88,23 +88,26 @@ export default {
fetch: function() { fetch: function() {
let self = this let self = this
self.isLoading = true self.isLoading = true
let url = 'radios/radios/' + this.id + '/' let url = "radios/radios/" + this.id + "/"
axios.get(url).then((response) => { axios.get(url).then(response => {
self.radio = response.data self.radio = response.data
axios.get(url + 'tracks/', {params: {page: this.page}}).then((response) => { axios
.get(url + "tracks/", { params: { page: this.page } })
.then(response => {
this.totalTracks = response.data.count this.totalTracks = response.data.count
this.tracks = response.data.results this.tracks = response.data.results
}).then(() => { })
.then(() => {
self.isLoading = false self.isLoading = false
}) })
}) })
}, },
deleteRadio() { deleteRadio() {
let self = this let self = this
let url = 'radios/radios/' + this.id + '/' let url = "radios/radios/" + this.id + "/"
axios.delete(url).then((response) => { axios.delete(url).then(response => {
self.$router.push({ self.$router.push({
path: '/library' path: "/library"
}) })
}) })
} }