Fix #612: Improved accessibility by using main/section/nav tags and aria-labels in most critical places

This commit is contained in:
Eliot Berriot 2018-11-19 23:33:22 +01:00
parent 9005ebbd6d
commit 29171853b3
No known key found for this signature in database
GPG Key ID: DD6965E2476E5C27
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,28 +31,28 @@
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({
instance: state => state.instance.settings.instance instance: state => state.instance.settings.instance
}), }),
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,45 +53,51 @@
<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({
messages: state => state.ui.messages messages: state => state.ui.messages
}), }),
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>
<style scoped> <style scoped>
footer p { footer p {
color: grey; color: grey;
} }
</style> </style>

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,21 +16,21 @@
<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>
export default { export default {
data: function () { data: function() {
return { return {
path: window.location.href path: window.location.href
} }
}, },
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,30 +24,42 @@
</template> </template>
<script> <script>
import _ from 'lodash' import _ from "lodash"
export default { export default {
props: { props: {
current: {type: Number, default: 1}, current: { type: Number, default: 1 },
paginateBy: {type: Number, default: 25}, paginateBy: { type: Number, default: 25 },
total: {type: Number}, total: { type: Number },
compact: {type: Boolean, default: false} compact: { type: Boolean, default: false }
}, },
computed: { computed: {
pages: function () { labels() {
return {
pagination: this.$gettext("Pagination")
}
},
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,25 +71,25 @@ 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)
} }
} }
}) })
return final return final
}, },
maxPage: function () { maxPage: function() {
return Math.ceil(this.total / this.paginateBy) return Math.ceil(this.total / this.paginateBy)
} }
}, },
methods: { methods: {
selectPage: function (page) { selectPage: function(page) {
if (page > this.maxPage || page < 1) { if (page > this.maxPage || page < 1) {
return return
} }
if (this.current !== page) { if (this.current !== page) {
this.$emit('page-changed', page) this.$emit("page-changed", page)
} }
} }
} }
@ -87,6 +99,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> <style scoped>
.ui.pagination.menu .item { .ui.pagination.menu .item {
cursor: pointer; cursor: pointer;
} }
</style> </style>

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,44 +154,46 @@
<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,
Logo, Logo,
draggable draggable
}, },
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) {
clearInterval(this.fetchInterval) clearInterval(this.fetchInterval)
} }
@ -197,82 +203,92 @@ export default {
queue: state => state.queue, queue: state => state.queue,
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: {
get () { get() {
return this.$store.state.queue.tracks return this.$store.state.queue.tracks
}, },
set (value) { set(value) {
this.tracksChangeBuffer = value this.tracksChangeBuffer = value
} }
}, },
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
} }
}, },
watch: { watch: {
url: function () { url: function() {
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;
.sidebar { .sidebar {
background: $sidebar-color; background: $sidebar-color;
@include media(">tablet") { @include media(">tablet") {
display:flex; display: flex;
flex-direction:column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
} }
@include media(">desktop") { @include media(">desktop") {
@ -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: {
@ -233,8 +233,13 @@ export default {
GlobalEvents, GlobalEvents,
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,
@ -245,7 +250,7 @@ export default {
dummyAudio: null dummyAudio: null
} }
}, },
mounted () { mounted() {
// we trigger the watcher explicitely it does not work otherwise // we trigger the watcher explicitely it does not work otherwise
this.sliderVolume = this.volume this.sliderVolume = this.volume
// this is needed to unlock audio playing under some browsers, // this is needed to unlock audio playing under some browsers,
@ -254,57 +259,57 @@ 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() {
this.dummyAudio.unload() this.dummyAudio.unload()
}, },
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
if (this.isShuffling || disabled) { if (this.isShuffling || disabled) {
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()
}) })
}) })
}, 100) }, 100)
}, },
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() {
if (!this.currentTrack.album.cover) { if (!this.currentTrack.album.cover) {
this.ambiantColors = this.defaultAmbiantColors this.ambiantColors = this.defaultAmbiantColors
return return
@ -312,9 +317,9 @@ export default {
let image = this.$refs.cover let image = this.$refs.cover
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,
@ -363,29 +376,35 @@ export default {
clear clear
} }
}, },
style: function () { style: function() {
let style = { let style = {
'background': this.ambiantGradiant background: this.ambiantGradiant
} }
return style return style
}, },
ambiantGradiant: function () { ambiantGradiant: function() {
let indexConf = [ let indexConf = [
{orientation: 330, percent: 100, opacity: 0.7}, { orientation: 330, percent: 100, opacity: 0.7 },
{orientation: 240, percent: 90, opacity: 0.7}, { orientation: 240, percent: 90, opacity: 0.7 },
{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
let [r, g, b] = e .map((e, i) => {
let conf = indexConf[i] let [r, g, b] = e
return `linear-gradient(${conf.orientation}deg, rgba(${r}, ${g}, ${b}, ${conf.opacity}) 10%, rgba(255, 255, 255, 0) ${conf.percent}%)` let conf = indexConf[i]
}).join(', ') return `linear-gradient(${
conf.orientation
}deg, rgba(${r}, ${g}, ${b}, ${
conf.opacity
}) 10%, rgba(255, 255, 255, 0) ${conf.percent}%)`
})
.join(", ")
return gradients return gradients
} }
}, },
watch: { watch: {
currentTrack (newValue, oldValue) { currentTrack(newValue, oldValue) {
if (!this.isShuffling && newValue != oldValue) { if (!this.isShuffling && newValue != oldValue) {
this.audioKey = String(new Date()) this.audioKey = String(new Date())
} }
@ -393,11 +412,11 @@ export default {
this.ambiantColors = this.defaultAmbiantColors this.ambiantColors = this.defaultAmbiantColors
} }
}, },
volume (newValue) { volume(newValue) {
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,14 +566,13 @@ export default {
margin: 0; margin: 0;
} }
@keyframes MOVE-BG { @keyframes MOVE-BG {
from { from {
transform: translateX(0px); transform: translateX(0px);
} }
to { to {
transform: translateX(46px); transform: translateX(46px);
} }
} }
.indicating.progress { .indicating.progress {
@ -565,7 +585,7 @@ export default {
.ui.inverted.progress .buffer.bar { .ui.inverted.progress .buffer.bar {
position: absolute; position: absolute;
background-color:rgba(255, 255, 255, 0.15); background-color: rgba(255, 255, 255, 0.15);
} }
.indicating.progress .bar { .indicating.progress .bar {
left: -46px; left: -46px;
@ -576,12 +596,12 @@ 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;
animation-duration: 2s; animation-duration: 2s;
animation-timing-function: linear; animation-timing-function: linear;
animation-iteration-count: infinite; animation-iteration-count: infinite;
} }
</style> </style>

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,39 +43,39 @@
</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
}, },
data () { data() {
return { return {
// 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
} }
}, },
mounted () { mounted() {
this.$refs.username.focus() this.$refs.username.focus()
}, },
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
@ -83,30 +83,31 @@ export default {
} }
}, },
methods: { methods: {
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
credentials, .dispatch("auth/login", {
next: '/library', credentials,
onError: error => { next: "/library",
if (error.response.status === 400) { onError: error => {
self.error = 'invalid_credentials' if (error.response.status === 400) {
} else { self.error = "invalid_credentials"
self.error = 'unknown_error' } else {
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,16 +8,16 @@
<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>
export default { 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,36 +25,37 @@
</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 />
</div> </section>
</div> </div>
</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"
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],
@ -126,14 +126,14 @@ export default {
PasswordInput, PasswordInput,
SubsonicTokenForm SubsonicTokenForm
}, },
data () { data() {
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"]
} }
} }
} }
@ -157,108 +157,120 @@ export default {
}) })
return d return d
}, },
mounted () { mounted() {
$('select.dropdown').dropdown() $("select.dropdown").dropdown()
}, },
methods: { methods: {
submitSettings () { submitSettings() {
this.settings.success = false this.settings.success = false
this.settings.errors = [] this.settings.errors = []
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 => {
self.settings.success = true logger.default.info("Updated settings successfully")
return axios.get('users/users/me/').then((response) => { self.settings.success = true
self.$store.dispatch('auth/updateProfile', response.data) return axios.get("users/users/me/").then(response => {
}) self.$store.dispatch("auth/updateProfile", response.data)
}, error => { })
logger.default.error('Error while updating settings') },
self.isLoading = false error => {
self.settings.errors = error.backendErrors logger.default.error("Error while updating settings")
}) self.isLoading = false
self.settings.errors = error.backendErrors
}
)
}, },
submitAvatar () { submitAvatar() {
this.isLoadingAvatar = true this.isLoadingAvatar = true
this.avatarErrors = [] this.avatarErrors = []
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(
this.isLoadingAvatar = false response => {
self.currentAvatar = response.data.avatar this.isLoadingAvatar = false
self.$store.commit('auth/avatar', self.currentAvatar) self.currentAvatar = response.data.avatar
}, error => { self.$store.commit("auth/avatar", self.currentAvatar)
self.isLoadingAvatar = false },
self.avatarErrors = error.backendErrors error => {
}) self.isLoadingAvatar = false
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 => { })
this.isLoadingAvatar = false .then(
self.currentAvatar = {} response => {
self.$store.commit('auth/avatar', self.currentAvatar) this.isLoadingAvatar = false
}, error => { self.currentAvatar = {}
self.isLoadingAvatar = false self.$store.commit("auth/avatar", self.currentAvatar)
self.avatarErrors = error.backendErrors },
}) error => {
self.isLoadingAvatar = false
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 => {
self.$router.push({ logger.default.info("Password successfully changed")
name: 'profile', self.$router.push({
params: { name: "profile",
username: self.$store.state.auth.username params: {
}}) username: self.$store.state.auth.username
}, error => { }
if (error.response.status === 400) { })
self.passwordError = 'invalid_credentials' },
} else { error => {
self.passwordError = 'unknown_error' if (error.response.status === 400) {
self.passwordError = "invalid_credentials"
} else {
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() {
let self = this let self = this
return this.settings.order.map(id => { return this.settings.order.map(id => {
return self.settings.fields[id] return self.settings.fields[id]
}) })
}, },
settingsValues () { settingsValues() {
let self = this let self = this
let s = {} let s = {}
this.settings.order.forEach(setting => { this.settings.order.forEach(setting => {
@ -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,49 +53,51 @@
</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,
invitation: this.defaultInvitation invitation: this.defaultInvitation
} }
}, },
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
} }
}) })
}, },
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,
@ -105,7 +107,7 @@ export default {
} }
}, },
methods: { methods: {
submit () { submit() {
var self = this var self = this
self.isLoading = true self.isLoading = true
this.errors = [] this.errors = []
@ -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 => {
self.$router.push({ logger.default.info("Successfully created account")
name: 'profile', self.$router.push({
params: { name: "profile",
username: this.username params: {
}}) username: this.username
}, error => { }
self.errors = error.backendErrors })
self.isLoading = false },
}) error => {
self.errors = error.backendErrors
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],
@ -79,8 +79,10 @@ export default {
RadioButton, RadioButton,
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,31 +90,31 @@ 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"]
] ]
} }
}, },
created () { created() {
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")
} }
} }
}, },
methods: { methods: {
updateQueryString: function () { updateQueryString: function() {
this.$router.replace({ this.$router.replace({
query: { query: {
page: this.page, page: this.page,
@ -121,42 +123,42 @@ export default {
} }
}) })
}, },
fetchFavorites (url) { fetchFavorites(url) {
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
}) })
}, },
selectPage: function (page) { selectPage: function(page) {
this.page = page this.page = page
} }
}, },
watch: { watch: {
page: function () { page: function() {
this.updateQueryString() this.updateQueryString()
}, },
paginateBy: function () { paginateBy: function() {
this.updateQueryString() this.updateQueryString()
}, },
orderingDirection: function () { orderingDirection: function() {
this.updateQueryString() this.updateQueryString()
}, },
ordering: function () { ordering: function() {
this.updateQueryString() this.updateQueryString()
} }
} }

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,86 +38,93 @@
<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,
LibraryWidget LibraryWidget
}, },
data () { data() {
return { return {
isLoading: true, isLoading: true,
album: null album: null
} }
}, },
created () { created() {
this.fetchData() this.fetchData()
}, },
methods: { methods: {
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
}) })
} }
}, },
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: {
id () { id() {
this.fetchData() this.fetchData()
} }
} }
@ -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,
@ -88,7 +88,7 @@ export default {
TrackTable, TrackTable,
LibraryWidget LibraryWidget
}, },
data () { data() {
return { return {
isLoading: true, isLoading: true,
isLoadingAlbums: true, isLoadingAlbums: true,
@ -99,54 +99,63 @@ export default {
tracks: [] tracks: []
} }
}, },
created () { created() {
this.fetchData() this.fetchData()
}, },
methods: { methods: {
fetchData () { fetchData() {
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
self.totalAlbums = response.data.count .get("albums/", {
let parsed = JSON.parse(JSON.stringify(response.data.results)) params: { artist: self.id, ordering: "-release_date" }
self.albums = parsed.map((album) => {
return backend.Album.clean(album)
}) })
.then(response => {
self.totalAlbums = response.data.count
let parsed = JSON.parse(JSON.stringify(response.data.results))
self.albums = parsed.map(album => {
return backend.Album.clean(album)
})
self.isLoadingAlbums = false self.isLoadingAlbums = false
}) })
}) })
} }
}, },
computed: { computed: {
labels () { labels() {
return { return {
title: this.$gettext('Artist') title: this.$gettext("Artist")
} }
}, },
isPlayable () { isPlayable() {
return this.artist.albums.filter((a) => { return (
return a.is_playable this.artist.albums.filter(a => {
}).length > 0 return a.is_playable
}).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() {
let tracks = [] let tracks = []
this.albums.forEach(album => { this.albums.forEach(album => {
album.tracks.forEach(track => { album.tracks.forEach(track => {
@ -155,22 +164,28 @@ export default {
}) })
return tracks return tracks
}, },
cover () { cover() {
return this.artist.albums.filter(album => { return this.artist.albums
return album.cover .filter(album => {
}).map(album => { return album.cover
return album.cover })
})[0] .map(album => {
return album.cover
})[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: {
id () { id() {
this.fetchData() this.fetchData()
} }
} }

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
@ -125,7 +124,7 @@ export default {
} }
}, },
methods: { methods: {
updateQueryString: _.debounce(function () { updateQueryString: _.debounce(function() {
this.$router.replace({ this.$router.replace({
query: { query: {
query: this.query, query: this.query,
@ -135,7 +134,7 @@ export default {
} }
}) })
}, 500), }, 500),
fetchData: _.debounce(function () { fetchData: _.debounce(function() {
var self = this var self = this
this.isLoading = true this.isLoading = true
let url = FETCH_URL let url = FETCH_URL
@ -144,36 +143,36 @@ 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
}) })
}, 500), }, 500),
selectPage: function (page) { selectPage: function(page) {
this.page = page this.page = page
} }
}, },
watch: { watch: {
page () { page() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
}, },
paginateBy () { paginateBy() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
}, },
ordering () { ordering() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
}, },
orderingDirection () { orderingDirection() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
}, },
query () { query() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
} }

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,
@ -50,35 +50,35 @@ export default {
AlbumWidget, AlbumWidget,
PlaylistWidget PlaylistWidget
}, },
data () { data() {
return { return {
artists: [], artists: [],
isLoadingArtists: false isLoadingArtists: false
} }
}, },
created () { created() {
this.fetchArtists() this.fetchArtists()
}, },
computed: { computed: {
labels () { labels() {
return { return {
title: this.$gettext('Home') title: this.$gettext("Home")
} }
} }
}, },
methods: { methods: {
fetchArtists () { fetchArtists() {
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>
@ -21,8 +21,16 @@
<script> <script>
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
@ -147,7 +146,7 @@ export default {
} }
}, },
methods: { methods: {
updateQueryString: _.debounce(function () { updateQueryString: _.debounce(function() {
this.$router.replace({ this.$router.replace({
query: { query: {
query: this.query, query: this.query,
@ -157,7 +156,7 @@ export default {
} }
}) })
}, 500), }, 500),
fetchData: _.debounce(function () { fetchData: _.debounce(function() {
var self = this var self = this
this.isLoading = true this.isLoading = true
let url = FETCH_URL let url = FETCH_URL
@ -167,34 +166,34 @@ 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
}) })
}, 500), }, 500),
selectPage: function (page) { selectPage: function(page) {
this.page = page this.page = page
} }
}, },
watch: { watch: {
page () { page() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
}, },
paginateBy () { paginateBy() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
}, },
ordering () { ordering() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
}, },
orderingDirection () { orderingDirection() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
}, },
query () { query() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
} }

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,41 +117,40 @@
<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,
TrackFavoriteIcon, TrackFavoriteIcon,
LibraryWidget LibraryWidget
}, },
data () { data() {
return { return {
time, time,
isLoadingTrack: true, isLoadingTrack: true,
@ -160,78 +159,94 @@ export default {
lyrics: null lyrics: null
} }
}, },
created () { created() {
this.fetchData() this.fetchData()
this.fetchLyrics() this.fetchLyrics()
}, },
methods: { methods: {
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
}) })
}, },
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(
self.lyrics = response.data response => {
self.isLoadingLyrics = false self.lyrics = response.data
}, (response) => { self.isLoadingLyrics = false
console.error('No lyrics available') },
self.isLoadingLyrics = false response => {
}) console.error("No lyrics available")
self.isLoadingLyrics = false
}
)
} }
}, },
computed: { computed: {
labels () { labels() {
return { return {
title: this.$gettext('Track') title: this.$gettext("Track")
} }
}, },
upload () { upload() {
if (this.track.uploads) { if (this.track.uploads) {
return this.track.uploads[0] return this.track.uploads[0]
} }
}, },
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() {
return null return null
}, },
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: {
id () { id() {
this.fetchData() this.fetchData()
} }
} }

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,28 +87,28 @@
</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: {
id: {required: false} id: { required: false }
}, },
components: { components: {
BuilderFilter, BuilderFilter,
TrackTable, TrackTable,
RadioButton RadioButton
}, },
data: function () { data: function() {
return { return {
isLoading: false, isLoading: false,
success: false, success: false,
@ -116,12 +116,12 @@ export default {
currentFilterType: null, currentFilterType: null,
filters: [], filters: [],
checkResult: null, checkResult: null,
radioName: '', radioName: "",
radioDesc: '', radioDesc: "",
isPublic: true isPublic: true
} }
}, },
created: function () { created: function() {
let self = this let self = this
this.fetchFilters().then(() => { this.fetchFilters().then(() => {
if (self.id) { if (self.id) {
@ -129,18 +129,18 @@ 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
}) })
}, },
add () { add() {
this.filters.push({ this.filters.push({
config: {}, config: {},
filter: this.currentFilter, filter: this.currentFilter,
@ -148,23 +148,25 @@ export default {
}) })
this.fetchCandidates() this.fetchCandidates()
}, },
updateConfig (index, field, value) { updateConfig(index, field, value) {
this.filters[index].config[field] = value this.filters[index].config[field] = value
this.fetchCandidates() this.fetchCandidates()
}, },
deleteFilter (index) { deleteFilter(index) {
this.filters.splice(index, 1) this.filters.splice(index, 1)
this.fetchCandidates() this.fetchCandidates()
}, },
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()
} }
}) })
@ -174,24 +176,22 @@ export default {
self.isLoading = false self.isLoading = false
}) })
}, },
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]
}) })
}, },
save: function () { save: function() {
let self = this let self = this
self.success = false self.success = false
self.isLoading = true self.isLoading = true
@ -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
} }
@ -229,30 +229,28 @@ 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,
placeholder placeholder
} }
}, },
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) {
return [] return []
} }
let errors = this.checkResult.errors let errors = this.checkResult.errors
return errors return errors
}, },
currentFilter: function () { currentFilter: function() {
let self = this let self = this
return this.availableFilters.filter(e => { return this.availableFilters.filter(e => {
return e.type === self.currentFilterType return e.type === self.currentFilterType
@ -261,7 +259,7 @@ export default {
}, },
watch: { watch: {
filters: { filters: {
handler: function () { handler: function() {
this.fetchCandidates() this.fetchCandidates()
}, },
deep: true deep: true

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,29 +32,29 @@
</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],
data () { data() {
return { return {
time time
} }
}, },
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"
}) })
} }
} }
@ -64,6 +64,6 @@ export default Vue.extend({
<!-- 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.card { .ui.card {
width: 100% !important; width: 100% !important;
} }
</style> </style>

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,19 +27,19 @@
<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() {
return { return {
isLoading: false, isLoading: false,
notifications: null, notifications: null,
@ -51,64 +51,63 @@ export default {
components: { components: {
NotificationRow NotificationRow
}, },
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: {
...mapState({ ...mapState({
events: state => state.instance.events events: state => state.instance.events
}), }),
labels () { labels() {
return { return {
title: this.$gettext('Notifications'), title: this.$gettext("Notifications")
} }
} }
}, },
methods: { methods: {
handleNewNotification (event) { handleNewNotification(event) {
this.notifications.results.unshift(event.item) this.notifications.results.unshift(event.item)
}, },
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
}) })
}, },
markAllAsRead () { markAllAsRead() {
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,146 +24,140 @@
</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: {
SettingsGroup SettingsGroup
}, },
data () { data() {
return { return {
isLoading: false, isLoading: false,
settingsData: null, settingsData: null,
current: null current: null
} }
}, },
created () { created() {
let self = this let self = this
this.fetchSettings().then(r => { this.fetchSettings().then(r => {
self.$nextTick(() => { self.$nextTick(() => {
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()
}) })
}) })
}, },
methods: { methods: {
scrollTo (id) { scrollTo(id) {
this.current = id this.current = id
document.getElementById(id).scrollIntoView() document.getElementById(id).scrollIntoView()
}, },
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
}) })
} }
}, },
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'
]
} }
] ]
} }
}, },
watch: { watch: {
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>
@ -12,10 +12,12 @@
<script> <script>
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,24 +1,24 @@
<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: {
LibraryFilesTable LibraryFilesTable
}, },
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>
@ -15,9 +15,10 @@
<script> <script>
export default { 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: {
@ -19,9 +19,9 @@ export default {
InvitationsTable InvitationsTable
}, },
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,35 +102,34 @@
</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,
object: null, object: null,
permissions: [] permissions: []
} }
}, },
created () { created() {
this.fetchData() this.fetchData()
}, },
methods: { methods: {
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 => {
@ -141,60 +140,72 @@ export default {
self.isLoading = false self.isLoading = false
}) })
}, },
update (attr, toNull) { update(attr, toNull) {
let newValue = this.object[attr] let newValue = this.object[attr]
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")
} }
] ]
} }
}, },
watch: { watch: {
object () { object() {
this.$nextTick(() => { this.$nextTick(() => {
$('select.dropdown').dropdown() $("select.dropdown").dropdown()
}) })
} }
} }

View File

@ -1,24 +1,24 @@
<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: {
UsersTable UsersTable
}, },
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,16 +28,16 @@
</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,
errors: [], errors: [],
@ -46,30 +46,32 @@ export default {
} }
}, },
computed: { computed: {
labels () { labels() {
return { return {
confirm: this.$gettext('Confirm your email') confirm: this.$gettext("Confirm your email")
} }
} }
}, },
methods: { methods: {
submit () { submit() {
let self = this let self = this
self.isLoading = true self.isLoading = true
self.errors = [] self.errors = []
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(
self.isLoading = false response => {
self.success = true self.isLoading = false
}, error => { self.success = true
self.errors = error.backendErrors },
self.isLoading = false error => {
}) self.errors = error.backendErrors
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,29 +28,31 @@
<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,
isLoading: false, isLoading: false,
errors: [] errors: []
} }
}, },
mounted () { mounted() {
this.$refs.email.focus() this.$refs.email.focus()
}, },
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
@ -58,25 +60,27 @@ export default {
} }
}, },
methods: { methods: {
submit () { submit() {
let self = this let self = this
self.isLoading = true self.isLoading = true
self.errors = [] self.errors = []
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(
self.isLoading = false response => {
self.$router.push({ self.isLoading = false
name: 'auth.password-reset-confirm' self.$router.push({
}) name: "auth.password-reset-confirm"
}, error => { })
self.errors = error.backendErrors },
self.isLoading = false error => {
}) self.errors = error.backendErrors
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,
@ -57,14 +57,14 @@ export default {
} }
}, },
computed: { computed: {
labels () { labels() {
return { return {
changePassword: this.$gettext('Change your password') changePassword: this.$gettext("Change your password")
} }
} }
}, },
methods: { methods: {
submit () { submit() {
let self = this let self = this
self.isLoading = true self.isLoading = true
self.errors = [] self.errors = []
@ -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(
self.isLoading = false response => {
self.success = true self.isLoading = false
}, error => { self.success = true
self.errors = error.backendErrors },
self.isLoading = false error => {
}) self.errors = error.backendErrors
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],
@ -81,46 +81,48 @@ export default {
LibraryForm, LibraryForm,
LibraryFilesTable LibraryFilesTable
}, },
data () { data() {
return { return {
currentTab: 'follows', currentTab: "follows",
isLoadingFollows: false, isLoadingFollows: false,
follows: null follows: null
} }
}, },
created () { created() {
this.fetchFollows() this.fetchFollows()
}, },
methods: { methods: {
libraryUpdated () { libraryUpdated() {
this.hiddenForm = true this.hiddenForm = true
this.fetch() this.fetch()
}, },
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
}) })
}, },
updateApproved (follow, value) { updateApproved(follow, value) {
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
follow.isLoading = false .post(`federation/follows/library/${follow.uuid}/${action}/`)
follow.approved = value .then(response => {
}) follow.isLoading = false
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,24 +24,24 @@
</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() {
return { return {
isLoading: false, isLoading: false,
hiddenForm: true, hiddenForm: true,
libraries: [] libraries: []
} }
}, },
created () { created() {
this.fetch() this.fetch()
}, },
components: { components: {
@ -50,10 +50,10 @@ export default {
Quota Quota
}, },
methods: { methods: {
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) {
@ -61,7 +61,7 @@ export default {
} }
}) })
}, },
libraryCreated (library) { libraryCreated(library) {
this.hiddenForm = true this.hiddenForm = true
this.libraries.unshift(library) this.libraries.unshift(library)
} }

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,20 +51,20 @@
<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: {
id: {required: true}, id: { required: true },
defaultEdit: {type: Boolean, default: false} defaultEdit: { type: Boolean, default: false }
}, },
components: { components: {
PlaylistEditor, PlaylistEditor,
@ -72,7 +72,7 @@ export default {
PlayButton, PlayButton,
RadioButton RadioButton
}, },
data: function () { data: function() {
return { return {
edit: this.defaultEdit, edit: this.defaultEdit,
isLoading: false, isLoading: false,
@ -81,18 +81,18 @@ export default {
playlistTracks: [] playlistTracks: []
} }
}, },
created: function () { created: function() {
this.fetch() this.fetch()
}, },
computed: { computed: {
labels () { labels() {
return { return {
playlist: this.$gettext('Playlist') playlist: this.$gettext("Playlist")
} }
} }
}, },
methods: { methods: {
updatePlts (v) { updatePlts(v) {
this.playlistTracks = v this.playlistTracks = v
this.tracks = v.map((e, i) => { this.tracks = v.map((e, i) => {
let track = e.track let track = e.track
@ -100,26 +100,29 @@ export default {
return track return track
}) })
}, },
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
self.updatePlts(response.data.results) .get(url + "tracks/")
}).then(() => { .then(response => {
self.isLoading = false self.updatePlts(response.data.results)
}) })
.then(() => {
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,59 +50,61 @@
: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"]
] ]
} }
}, },
created () { created() {
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
@ -110,7 +112,7 @@ export default {
} }
}, },
methods: { methods: {
updateQueryString: _.debounce(function () { updateQueryString: _.debounce(function() {
this.$router.replace({ this.$router.replace({
query: { query: {
query: this.query, query: this.query,
@ -120,7 +122,7 @@ export default {
} }
}) })
}, 250), }, 250),
fetchData: _.debounce(function () { fetchData: _.debounce(function() {
var self = this var self = this
this.isLoading = true this.isLoading = true
let url = FETCH_URL let url = FETCH_URL
@ -130,33 +132,33 @@ 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
}) })
}, 500), }, 500),
selectPage: function (page) { selectPage: function(page) {
this.page = page this.page = page
} }
}, },
watch: { watch: {
page () { page() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
}, },
paginateBy () { paginateBy() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
}, },
ordering () { ordering() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
}, },
orderingDirection () { orderingDirection() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
}, },
query () { query() {
this.updateQueryString() this.updateQueryString()
this.fetchData() this.fetchData()
} }

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,26 +43,26 @@
: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: {
id: {required: true} id: { required: true }
}, },
components: { components: {
TrackTable, TrackTable,
RadioButton, RadioButton,
Pagination Pagination
}, },
data: function () { data: function() {
return { return {
isLoading: false, isLoading: false,
radio: null, radio: null,
@ -71,46 +71,49 @@ export default {
page: 1 page: 1
} }
}, },
created: function () { created: function() {
this.fetch() this.fetch()
}, },
computed: { computed: {
labels () { labels() {
return { return {
title: this.$gettext('Radio') title: this.$gettext("Radio")
} }
} }
}, },
methods: { methods: {
selectPage: function (page) { selectPage: function(page) {
this.page = page this.page = page
}, },
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
this.totalTracks = response.data.count .get(url + "tracks/", { params: { page: this.page } })
this.tracks = response.data.results .then(response => {
}).then(() => { this.totalTracks = response.data.count
self.isLoading = false this.tracks = response.data.results
}) })
.then(() => {
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"
}) })
}) })
} }
}, },
watch: { watch: {
page: function () { page: function() {
this.fetch() this.fetch()
} }
} }