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

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

Closes #612

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

View File

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

View File

@ -1,6 +1,6 @@
<template>
<div class="main pusher" v-title="labels.title">
<div class="ui vertical center aligned stripe segment">
<main class="main pusher" v-title="labels.title">
<section class="ui vertical center aligned stripe segment">
<div class="ui text container">
<h1 class="ui huge header">
<translate v-if="instance.name.value" :translate-params="{instance: instance.name.value}">
@ -10,8 +10,8 @@
</h1>
<stats></stats>
</div>
</div>
<div class="ui vertical stripe segment">
</section>
<section class="ui vertical stripe segment">
<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>
</p>
@ -31,28 +31,28 @@
class="ui middle aligned stackable text container"
v-html="$options.filters.markdown(instance.long_description.value)">
</div>
</div>
</div>
</section>
</main>
</template>
<script>
import {mapState} from 'vuex'
import Stats from '@/components/instance/Stats'
import { mapState } from "vuex"
import Stats from "@/components/instance/Stats"
export default {
components: {
Stats
},
created () {
this.$store.dispatch('instance/fetchSettings')
created() {
this.$store.dispatch("instance/fetchSettings")
},
computed: {
...mapState({
instance: state => state.instance.settings.instance
}),
labels () {
labels() {
return {
title: this.$gettext('About this instance')
title: this.$gettext("About this instance")
}
}
}

View File

@ -1,8 +1,8 @@
<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 stackable equal height stackable grid">
<div class="four wide column">
<section class="four wide column">
<h4 v-translate class="ui header">
<translate :translate-params="{instanceName: instanceHostname}" >About %{instanceName}</translate>
</h4>
@ -25,24 +25,24 @@
</select>
</div>
</div>
</div>
<div class="four wide column">
</section>
<section class="four wide column">
<h4 v-translate class="ui header">Using Funkwhale</h4>
<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/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>
</div>
<div class="four wide column">
</section>
<section class="four wide column">
<h4 v-translate class="ui header">Getting help</h4>
<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://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>
</div>
</div>
<div class="four wide column">
</section>
<section class="four wide column">
<h4 v-translate class="ui header">About Funkwhale</h4>
<div class="ui link list">
<a href="https://funkwhale.audio" class="item" target="_blank"><translate>Official website</translate></a>
@ -53,45 +53,51 @@
<p>
<translate>The funkwhale logo was kindly designed and provided by Francis Gading.</translate>
</p>
</div>
</section>
</div>
</div>
</footer>
</template>
<script>
import {mapState} from 'vuex'
import { mapState } from "vuex"
export default {
props: ['version'],
props: ["version"],
methods: {
switchInstance () {
let confirm = window.confirm(this.$gettext('This will erase your local data and disconnect you, do you want to continue?'))
switchInstance() {
let confirm = window.confirm(
this.$gettext(
"This will erase your local data and disconnect you, do you want to continue?"
)
)
if (confirm) {
this.$store.commit('instance/instanceUrl', null)
this.$store.commit("instance/instanceUrl", null)
}
},
}
},
computed: {
...mapState({
messages: state => state.ui.messages
}),
instanceHostname () {
instanceHostname() {
let url = this.$store.state.instance.instanceUrl
let parser = document.createElement('a');
let parser = document.createElement("a")
parser.href = url
return parser.hostname
},
suggestedInstances () {
let instances = [this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio']
suggestedInstances() {
let instances = [
this.$store.getters["instance/defaultUrl"](),
"https://demo.funkwhale.audio"
]
return instances
},
}
}
}
</script>
<style scoped>
footer p {
color: grey;
}
footer p {
color: grey;
}
</style>

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<template>
<div class="ui pagination menu">
<div class="ui pagination menu" role="navigation" :aria-label="labels.pagination">
<a href
:disabled="current - 1 < 1"
@click.prevent.stop="selectPage(current - 1)"
@ -24,30 +24,42 @@
</template>
<script>
import _ from 'lodash'
import _ from "lodash"
export default {
props: {
current: {type: Number, default: 1},
paginateBy: {type: Number, default: 25},
total: {type: Number},
compact: {type: Boolean, default: false}
current: { type: Number, default: 1 },
paginateBy: { type: Number, default: 25 },
total: { type: Number },
compact: { type: Boolean, default: false }
},
computed: {
pages: function () {
labels() {
return {
pagination: this.$gettext("Pagination")
}
},
pages: function() {
let range = 2
let current = this.current
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 allowed = beginning.concat(middle, end)
allowed = _.uniq(allowed)
allowed = _.sortBy(allowed, [(e) => { return e }])
allowed = _.sortBy(allowed, [
e => {
return e
}
])
let final = []
allowed.forEach(p => {
let last = final.slice(-1)[0]
let consecutive = true
if (last === 'skip') {
if (last === "skip") {
consecutive = false
} else {
if (!last) {
@ -59,25 +71,25 @@ export default {
if (consecutive) {
final.push(p)
} else {
if (p !== 'skip') {
final.push('skip')
if (p !== "skip") {
final.push("skip")
final.push(p)
}
}
})
return final
},
maxPage: function () {
maxPage: function() {
return Math.ceil(this.total / this.paginateBy)
}
},
methods: {
selectPage: function (page) {
selectPage: function(page) {
if (page > this.maxPage || page < 1) {
return
}
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 -->
<style scoped>
.ui.pagination.menu .item {
cursor: pointer;
cursor: pointer;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar',]">
<div class="ui inverted segment header-wrapper">
<aside :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar',]">
<header class="ui inverted segment header-wrapper">
<search-bar @search="isCollapsed = false">
<router-link :title="'Funkwhale'" :to="{name: logoUrl}">
<i class="logo bordered inverted orange big icon">
@ -12,12 +12,12 @@
:class="['ui', 'basic', 'big', {'inverted': isCollapsed}, 'orange', 'icon', 'collapse', 'button']">
<i class="sidebar icon"></i></span>
</search-bar>
</div>
</header>
<div class="menu-area">
<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="item" href @click.prevent.stop="selectedTab = 'queue'" data-tab="queue">
<a class="active item" role="button" @click.prevent.stop="selectedTab = 'library'" data-tab="library"><translate>Browse</translate></a>
<a class="item" role="button" @click.prevent.stop="selectedTab = 'queue'" data-tab="queue">
<translate>Queue</translate>&nbsp;
<template v-if="queue.tracks.length === 0">
<translate>(empty)</translate>
@ -29,10 +29,10 @@
</div>
</div>
<div class="tabs">
<div class="ui bottom attached active tab" data-tab="library">
<div class="ui inverted vertical large fluid menu">
<section class="ui bottom attached active tab" data-tab="library" :aria-label="labels.mainMenu">
<nav class="ui inverted vertical large fluid menu" role="navigation" :aria-label="labels.mainMenu">
<div class="item">
<div class="header"><translate>My account</translate></div>
<header class="header"><translate>My account</translate></header>
<div class="menu">
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'profile', params: {username: $store.state.auth.username}}">
<i class="user icon"></i>
@ -61,7 +61,7 @@
</div>
</div>
<div class="item">
<div class="header"><translate>Music</translate></div>
<header class="header"><translate>Music</translate></header>
<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" 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 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">
<router-link
class="item"
@ -91,8 +91,8 @@
</router-link>
</div>
</div>
</div>
</div>
</nav>
</section>
<div v-if="queue.previousQueue " class="ui black icon message">
<i class="history icon"></i>
<div class="content">
@ -113,17 +113,21 @@
</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">
<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="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-else src="../assets/audio/default-cover.png">
</td>
<td colspan="4">
<button class="title reset ellipsis">
<button class="title reset ellipsis" :aria-label="labels.selectTrack">
<strong>{{ track.title }}</strong><br />
{{ track.artist.name }}
</button>
@ -134,7 +138,7 @@
</template>
</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>
</button>
</td>
@ -150,44 +154,46 @@
<div @click="$store.dispatch('radios/stop')" class="ui basic inverted red button"><translate>Stop radio</translate></div>
</div>
</div>
</div>
</section>
</div>
<player @next="scrollToCurrent" @previous="scrollToCurrent"></player>
</div>
</aside>
</template>
<script>
import {mapState, mapActions} from 'vuex'
import { mapState, mapActions } from "vuex"
import Player from '@/components/audio/Player'
import Logo from '@/components/Logo'
import SearchBar from '@/components/audio/SearchBar'
import backend from '@/audio/backend'
import draggable from 'vuedraggable'
import Player from "@/components/audio/Player"
import Logo from "@/components/Logo"
import SearchBar from "@/components/audio/SearchBar"
import backend from "@/audio/backend"
import draggable from "vuedraggable"
import $ from 'jquery'
import $ from "jquery"
export default {
name: 'sidebar',
name: "sidebar",
components: {
Player,
SearchBar,
Logo,
draggable
},
data () {
data() {
return {
selectedTab: 'library',
selectedTab: "library",
backend: backend,
tracksChangeBuffer: null,
isCollapsed: true,
fetchInterval: null,
fetchInterval: null
}
},
mounted () {
$(this.$el).find('.menu .item').tab()
mounted() {
$(this.$el)
.find(".menu .item")
.tab()
},
destroy () {
destroy() {
if (this.fetchInterval) {
clearInterval(this.fetchInterval)
}
@ -197,82 +203,92 @@ export default {
queue: state => state.queue,
url: state => state.route.path
}),
labels () {
let pendingRequests = this.$gettext('Pending import requests')
let pendingFollows = this.$gettext('Pending follow requests')
labels() {
let mainMenu = this.$gettext("Main menu")
let selectTrack = this.$gettext("Play this track")
let pendingRequests = this.$gettext("Pending import requests")
let pendingFollows = this.$gettext("Pending follow requests")
return {
pendingRequests,
pendingFollows
pendingFollows,
mainMenu,
selectTrack
}
},
tracks: {
get () {
get() {
return this.$store.state.queue.tracks
},
set (value) {
set(value) {
this.tracksChangeBuffer = value
}
},
logoUrl () {
logoUrl() {
if (this.$store.state.auth.authenticated) {
return 'library.index'
return "library.index"
} else {
return 'index'
return "index"
}
}
},
methods: {
...mapActions({
cleanTrack: 'queue/cleanTrack'
cleanTrack: "queue/cleanTrack"
}),
reorder: function (event) {
this.$store.commit('queue/reorder', {
tracks: this.tracksChangeBuffer, oldIndex: event.oldIndex, newIndex: event.newIndex})
reorder: function(event) {
this.$store.commit("queue/reorder", {
tracks: this.tracksChangeBuffer,
oldIndex: event.oldIndex,
newIndex: event.newIndex
})
},
scrollToCurrent () {
scrollToCurrent() {
let current = $(this.$el).find('[data-tab="queue"] .active')[0]
if (!current) {
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
container.scrollTop = 0
current.scrollIntoView(true)
// Scroll back nothing if element is at bottom of container else do it
// 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
}
},
watch: {
url: function () {
url: function() {
this.isCollapsed = true
},
selectedTab: function (newValue) {
if (newValue === 'queue') {
selectedTab: function(newValue) {
if (newValue === "queue") {
this.scrollToCurrent()
}
},
'$store.state.queue.currentIndex': function () {
if (this.selectedTab !== 'queue') {
"$store.state.queue.currentIndex": function() {
if (this.selectedTab !== "queue") {
this.scrollToCurrent()
}
},
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
@import '../style/vendor/media';
@import "../style/vendor/media";
$sidebar-color: #3d3e3f;
.sidebar {
background: $sidebar-color;
background: $sidebar-color;
@include media(">tablet") {
display:flex;
flex-direction:column;
display: flex;
flex-direction: column;
justify-content: space-between;
}
@include media(">desktop") {
@ -284,7 +300,9 @@ $sidebar-color: #3d3e3f;
position: static !important;
width: 100% !important;
&.collapsed {
.menu-area, .player-wrapper, .tabs {
.menu-area,
.player-wrapper,
.tabs {
display: none;
}
}
@ -378,7 +396,9 @@ $sidebar-color: #3d3e3f;
.ui.search {
display: flex;
.collapse.button, .collapse.button:hover, .collapse.button:active {
.collapse.button,
.collapse.button:hover,
.collapse.button:active {
box-shadow: none !important;
margin: 0px;
display: flex;

View File

@ -1,5 +1,5 @@
<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">
<audio-track
ref="currentAudio"
@ -213,18 +213,18 @@
@keydown.s.prevent.exact="shuffle"
/>
</div>
</div>
</section>
</template>
<script>
import {mapState, mapGetters, mapActions} from 'vuex'
import GlobalEvents from '@/components/utils/global-events'
import ColorThief from '@/vendor/color-thief'
import {Howl} from 'howler'
import { mapState, mapGetters, mapActions } from "vuex"
import GlobalEvents from "@/components/utils/global-events"
import ColorThief from "@/vendor/color-thief"
import { Howl } from "howler"
import AudioTrack from '@/components/audio/Track'
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
import AudioTrack from "@/components/audio/Track"
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon"
import TrackPlaylistIcon from "@/components/playlists/TrackPlaylistIcon"
export default {
components: {
@ -233,8 +233,13 @@ export default {
GlobalEvents,
AudioTrack
},
data () {
let defaultAmbiantColors = [[46, 46, 46], [46, 46, 46], [46, 46, 46], [46, 46, 46]]
data() {
let defaultAmbiantColors = [
[46, 46, 46],
[46, 46, 46],
[46, 46, 46],
[46, 46, 46]
]
return {
isShuffling: false,
sliderVolume: this.volume,
@ -245,7 +250,7 @@ export default {
dummyAudio: null
}
},
mounted () {
mounted() {
// we trigger the watcher explicitely it does not work otherwise
this.sliderVolume = this.volume
// this is needed to unlock audio playing under some browsers,
@ -254,57 +259,57 @@ export default {
this.dummyAudio = new Howl({
preload: false,
autoplay: false,
src: ['noop.webm', 'noop.mp3']
src: ["noop.webm", "noop.mp3"]
})
},
destroyed () {
destroyed() {
this.dummyAudio.unload()
},
methods: {
...mapActions({
togglePlay: 'player/togglePlay',
mute: 'player/mute',
unmute: 'player/unmute',
clean: 'queue/clean',
updateProgress: 'player/updateProgress'
togglePlay: "player/togglePlay",
mute: "player/mute",
unmute: "player/unmute",
clean: "queue/clean",
updateProgress: "player/updateProgress"
}),
shuffle () {
shuffle() {
let disabled = this.queue.tracks.length === 0
if (this.isShuffling || disabled) {
return
}
let self = this
let msg = this.$gettext('Queue shuffled!')
let msg = this.$gettext("Queue shuffled!")
this.isShuffling = true
setTimeout(() => {
self.$store.dispatch('queue/shuffle', () => {
self.$store.dispatch("queue/shuffle", () => {
self.isShuffling = false
self.$store.commit('ui/addMessage', {
self.$store.commit("ui/addMessage", {
content: msg,
date: new Date()
})
})
}, 100)
},
next () {
next() {
let self = this
this.$store.dispatch('queue/next').then(() => {
self.$emit('next')
this.$store.dispatch("queue/next").then(() => {
self.$emit("next")
})
},
previous () {
previous() {
let self = this
this.$store.dispatch('queue/previous').then(() => {
self.$emit('previous')
this.$store.dispatch("queue/previous").then(() => {
self.$emit("previous")
})
},
touchProgress (e) {
touchProgress(e) {
let time
let target = this.$refs.progress
time = e.layerX / target.offsetWidth * this.duration
time = (e.layerX / target.offsetWidth) * this.duration
this.$refs.currentAudio.setCurrentTime(time)
},
updateBackground () {
updateBackground() {
if (!this.currentTrack.album.cover) {
this.ambiantColors = this.defaultAmbiantColors
return
@ -312,9 +317,9 @@ export default {
let image = this.$refs.cover
this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4)
},
handleError ({sound, error}) {
this.$store.commit('player/isLoadingAudio', false)
this.$store.dispatch('player/trackErrored')
handleError({ sound, error }) {
this.$store.commit("player/isLoadingAudio", false)
this.$store.dispatch("player/trackErrored")
}
},
computed: {
@ -330,26 +335,34 @@ export default {
queue: state => state.queue
}),
...mapGetters({
currentTrack: 'queue/currentTrack',
hasNext: 'queue/hasNext',
emptyQueue: 'queue/isEmpty',
durationFormatted: 'player/durationFormatted',
currentTimeFormatted: 'player/currentTimeFormatted',
progress: 'player/progress'
currentTrack: "queue/currentTrack",
hasNext: "queue/hasNext",
emptyQueue: "queue/isEmpty",
durationFormatted: "player/durationFormatted",
currentTimeFormatted: "player/currentTimeFormatted",
progress: "player/progress"
}),
labels () {
let previousTrack = this.$gettext('Previous track')
let play = this.$gettext('Play track')
let pause = this.$gettext('Pause track')
let next = this.$gettext('Next track')
let unmute = this.$gettext('Unmute')
let mute = this.$gettext('Mute')
let loopingDisabled = this.$gettext('Looping disabled. Click to switch to single-track looping.')
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')
labels() {
let audioPlayer = this.$gettext("Media player")
let previousTrack = this.$gettext("Previous track")
let play = this.$gettext("Play track")
let pause = this.$gettext("Pause track")
let next = this.$gettext("Next track")
let unmute = this.$gettext("Unmute")
let mute = this.$gettext("Mute")
let loopingDisabled = this.$gettext(
"Looping disabled. Click to switch to single-track looping."
)
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 {
audioPlayer,
previousTrack,
play,
pause,
@ -363,29 +376,35 @@ export default {
clear
}
},
style: function () {
style: function() {
let style = {
'background': this.ambiantGradiant
background: this.ambiantGradiant
}
return style
},
ambiantGradiant: function () {
ambiantGradiant: function() {
let indexConf = [
{orientation: 330, percent: 100, opacity: 0.7},
{orientation: 240, percent: 90, opacity: 0.7},
{orientation: 150, percent: 80, opacity: 0.7},
{orientation: 60, percent: 70, opacity: 0.7}
{ orientation: 330, percent: 100, opacity: 0.7 },
{ orientation: 240, percent: 90, opacity: 0.7 },
{ orientation: 150, percent: 80, opacity: 0.7 },
{ orientation: 60, percent: 70, opacity: 0.7 }
]
let gradients = this.ambiantColors.map((e, i) => {
let [r, g, b] = e
let conf = indexConf[i]
return `linear-gradient(${conf.orientation}deg, rgba(${r}, ${g}, ${b}, ${conf.opacity}) 10%, rgba(255, 255, 255, 0) ${conf.percent}%)`
}).join(', ')
let gradients = this.ambiantColors
.map((e, i) => {
let [r, g, b] = e
let conf = indexConf[i]
return `linear-gradient(${
conf.orientation
}deg, rgba(${r}, ${g}, ${b}, ${
conf.opacity
}) 10%, rgba(255, 255, 255, 0) ${conf.percent}%)`
})
.join(", ")
return gradients
}
},
watch: {
currentTrack (newValue, oldValue) {
currentTrack(newValue, oldValue) {
if (!this.isShuffling && newValue != oldValue) {
this.audioKey = String(new Date())
}
@ -393,11 +412,11 @@ export default {
this.ambiantColors = this.defaultAmbiantColors
}
},
volume (newValue) {
volume(newValue) {
this.sliderVolume = newValue
},
sliderVolume (newValue) {
this.$store.commit('player/volume', newValue)
sliderVolume(newValue) {
this.$store.commit("player/volume", newValue)
}
}
}
@ -405,7 +424,6 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.ui.progress {
margin: 0.5rem 0 1rem;
}
@ -423,18 +441,21 @@ export default {
.ui.item {
.meta {
font-size: 90%;
line-height: 1.2
line-height: 1.2;
}
}
.timer.total {
text-align: right;
text-align: right;
}
.timer.start {
cursor: pointer
cursor: pointer;
}
.track-area {
margin-top: 0;
.header, .meta, .artist, .album {
.header,
.meta,
.artist,
.album {
color: white !important;
}
}
@ -468,57 +489,57 @@ export default {
left: 25%;
cursor: pointer;
}
input[type=range]:focus {
input[type="range"]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
input[type="range"]::-webkit-slider-runnable-track {
cursor: pointer;
}
input[type=range]::-webkit-slider-thumb {
input[type="range"]::-webkit-slider-thumb {
background: white;
cursor: pointer;
-webkit-appearance: none;
border-radius: 3px;
width: 10px;
}
input[type=range]::-moz-range-track {
input[type="range"]::-moz-range-track {
cursor: pointer;
background: white;
opacity: 0.3;
}
input[type=range]::-moz-focus-outer {
input[type="range"]::-moz-focus-outer {
border: 0;
}
input[type=range]::-moz-range-thumb {
input[type="range"]::-moz-range-thumb {
background: white;
cursor: pointer;
border-radius: 3px;
width: 10px;
}
input[type=range]::-ms-track {
input[type="range"]::-ms-track {
cursor: pointer;
background: transparent;
border-color: transparent;
color: transparent;
}
input[type=range]::-ms-fill-lower {
input[type="range"]::-ms-fill-lower {
background: white;
opacity: 0.3;
}
input[type=range]::-ms-fill-upper {
input[type="range"]::-ms-fill-upper {
background: white;
opacity: 0.3;
}
input[type=range]::-ms-thumb {
input[type="range"]::-ms-thumb {
background: white;
cursor: pointer;
border-radius: 3px;
width: 10px;
}
input[type=range]:focus::-ms-fill-lower {
input[type="range"]:focus::-ms-fill-lower {
background: white;
}
input[type=range]:focus::-ms-fill-upper {
input[type="range"]:focus::-ms-fill-upper {
background: white;
}
}
@ -545,14 +566,13 @@ export default {
margin: 0;
}
@keyframes MOVE-BG {
from {
transform: translateX(0px);
}
to {
transform: translateX(46px);
}
from {
transform: translateX(0px);
}
to {
transform: translateX(46px);
}
}
.indicating.progress {
@ -565,7 +585,7 @@ export default {
.ui.inverted.progress .buffer.bar {
position: absolute;
background-color:rgba(255, 255, 255, 0.15);
background-color: rgba(255, 255, 255, 0.15);
}
.indicating.progress .bar {
left: -46px;
@ -576,12 +596,12 @@ export default {
grey 1px,
grey 10px,
transparent 10px,
transparent 20px,
) !important;
transparent 20px
) !important;
animation-name: MOVE-BG;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
</style>

View File

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

View File

@ -1,6 +1,6 @@
<template>
<div class="main pusher" v-title="labels.title">
<div class="ui vertical stripe segment">
<main class="main pusher" v-title="labels.title">
<section class="ui vertical stripe segment">
<div class="ui small text container">
<h2>
<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>
<button class="ui button" @click="$store.dispatch('auth/logout')"><translate>Yes, log me out!</translate></button>
</div>
</div>
</div>
</section>
</main>
</template>
<script>
export default {
computed: {
labels () {
labels() {
return {
title: this.$gettext('Log Out')
title: this.$gettext("Log Out")
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
<template>
<div>
<main>
<div v-if="isLoading" class="ui vertical segment" v-title="">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
<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">
<h2 class="ui center aligned icon header">
<i class="circular inverted sound yellow icon"></i>
@ -38,86 +38,93 @@
<translate>View on MusicBrainz</translate>
</a>
</div>
</div>
<div class="ui vertical stripe segment">
</section>
<section class="ui vertical stripe segment">
<h2>
<translate>Tracks</translate>
</h2>
<track-table v-if="album" :artist="album.artist" :display-position="true" :tracks="album.tracks"></track-table>
</div>
<div class="ui vertical stripe segment">
</section>
<section class="ui vertical stripe segment">
<h2>
<translate>User libraries</translate>
</h2>
<library-widget :url="'albums/' + id + '/libraries/'">
<translate slot="subtitle">This album is present in the following libraries:</translate>
</library-widget>
</div>
</section>
</template>
</div>
</main>
</template>
<script>
import axios from 'axios'
import logger from '@/logging'
import backend from '@/audio/backend'
import PlayButton from '@/components/audio/PlayButton'
import TrackTable from '@/components/audio/track/Table'
import LibraryWidget from '@/components/federation/LibraryWidget'
import axios from "axios"
import logger from "@/logging"
import backend from "@/audio/backend"
import PlayButton from "@/components/audio/PlayButton"
import TrackTable from "@/components/audio/track/Table"
import LibraryWidget from "@/components/federation/LibraryWidget"
const FETCH_URL = 'albums/'
const FETCH_URL = "albums/"
export default {
props: ['id'],
props: ["id"],
components: {
PlayButton,
TrackTable,
LibraryWidget
},
data () {
data() {
return {
isLoading: true,
album: null
}
},
created () {
created() {
this.fetchData()
},
methods: {
fetchData () {
fetchData() {
var self = this
this.isLoading = true
let url = FETCH_URL + this.id + '/'
let url = FETCH_URL + 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.isLoading = false
})
}
},
computed: {
labels () {
labels() {
return {
title: this.$gettext('Album')
title: this.$gettext("Album")
}
},
wikipediaUrl () {
return 'https://en.wikipedia.org/w/index.php?search=' + encodeURI(this.album.title + ' ' + this.album.artist.name)
wikipediaUrl() {
return (
"https://en.wikipedia.org/w/index.php?search=" +
encodeURI(this.album.title + " " + this.album.artist.name)
)
},
musicbrainzUrl () {
musicbrainzUrl() {
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) {
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: {
id () {
id() {
this.fetchData()
}
}
@ -126,5 +133,4 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
</style>

View File

@ -1,10 +1,10 @@
<template>
<div v-title="labels.title">
<main v-title="labels.title">
<div v-if="isLoading" class="ui vertical segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
<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">
<h2 class="ui center aligned icon header">
<i class="circular inverted users violet icon"></i>
@ -36,11 +36,11 @@
<translate>View on MusicBrainz</translate>
</a>
</div>
</div>
<div v-if="isLoadingAlbums" class="ui vertical stripe segment">
</section>
<section v-if="isLoadingAlbums" class="ui vertical stripe segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
<div v-else-if="albums && albums.length > 0" class="ui vertical stripe segment">
</section>
<section v-else-if="albums && albums.length > 0" class="ui vertical stripe segment">
<h2>
<translate>Albums by this artist</translate>
</h2>
@ -49,38 +49,38 @@
<album-card :mode="'rich'" class="fluid" :album="album"></album-card>
</div>
</div>
</div>
<div v-if="tracks.length > 0" class="ui vertical stripe segment">
</section>
<section v-if="tracks.length > 0" class="ui vertical stripe segment">
<h2>
<translate>Tracks by this artist</translate>
</h2>
<track-table :display-position="true" :tracks="tracks"></track-table>
</div>
<div class="ui vertical stripe segment">
</section>
<section class="ui vertical stripe segment">
<h2>
<translate>User libraries</translate>
</h2>
<library-widget :url="'artists/' + id + '/libraries/'">
<translate slot="subtitle">This artist is present in the following libraries:</translate>
</library-widget>
</div>
</section>
</template>
</div>
</main>
</template>
<script>
import _ from 'lodash'
import axios from 'axios'
import logger from '@/logging'
import backend from '@/audio/backend'
import AlbumCard from '@/components/audio/album/Card'
import RadioButton from '@/components/radios/Button'
import PlayButton from '@/components/audio/PlayButton'
import TrackTable from '@/components/audio/track/Table'
import LibraryWidget from '@/components/federation/LibraryWidget'
import _ from "lodash"
import axios from "axios"
import logger from "@/logging"
import backend from "@/audio/backend"
import AlbumCard from "@/components/audio/album/Card"
import RadioButton from "@/components/radios/Button"
import PlayButton from "@/components/audio/PlayButton"
import TrackTable from "@/components/audio/track/Table"
import LibraryWidget from "@/components/federation/LibraryWidget"
export default {
props: ['id'],
props: ["id"],
components: {
AlbumCard,
RadioButton,
@ -88,7 +88,7 @@ export default {
TrackTable,
LibraryWidget
},
data () {
data() {
return {
isLoading: true,
isLoadingAlbums: true,
@ -99,54 +99,63 @@ export default {
tracks: []
}
},
created () {
created() {
this.fetchData()
},
methods: {
fetchData () {
fetchData() {
var self = this
this.isLoading = true
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.totalTracks = response.data.count
})
axios.get('artists/' + this.id + '/').then((response) => {
axios.get("artists/" + this.id + "/").then(response => {
self.artist = response.data
self.isLoading = false
self.isLoadingAlbums = true
axios.get('albums/', {params: {artist: self.id, ordering: '-release_date'}}).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)
axios
.get("albums/", {
params: { artist: self.id, ordering: "-release_date" }
})
.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: {
labels () {
labels() {
return {
title: this.$gettext('Artist')
title: this.$gettext("Artist")
}
},
isPlayable () {
return this.artist.albums.filter((a) => {
return a.is_playable
}).length > 0
isPlayable() {
return (
this.artist.albums.filter(a => {
return a.is_playable
}).length > 0
)
},
wikipediaUrl () {
return 'https://en.wikipedia.org/w/index.php?search=' + encodeURI(this.artist.name)
wikipediaUrl() {
return (
"https://en.wikipedia.org/w/index.php?search=" +
encodeURI(this.artist.name)
)
},
musicbrainzUrl () {
musicbrainzUrl() {
if (this.artist.mbid) {
return 'https://musicbrainz.org/artist/' + this.artist.mbid
return "https://musicbrainz.org/artist/" + this.artist.mbid
}
},
allTracks () {
allTracks() {
let tracks = []
this.albums.forEach(album => {
album.tracks.forEach(track => {
@ -155,22 +164,28 @@ export default {
})
return tracks
},
cover () {
return this.artist.albums.filter(album => {
return album.cover
}).map(album => {
return album.cover
})[0]
cover() {
return this.artist.albums
.filter(album => {
return album.cover
})
.map(album => {
return album.cover
})[0]
},
headerStyle () {
headerStyle() {
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: {
id () {
id() {
this.fetchData()
}
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<template>
<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>
<translate>Browse</translate>
</router-link>
@ -13,7 +13,7 @@
<router-link class="ui item" to="/library/playlists" exact>
<translate>Playlists</translate>
</router-link>
</div>
</nav>
<router-view :key="$route.fullPath"></router-view>
</div>
</template>
@ -21,8 +21,16 @@
<script>
export default {
computed: {
showImports () {
return this.$store.state.auth.availablePermissions['upload'] || this.$store.state.auth.availablePermissions['library']
showImports() {
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 -->
<style lang="scss">
@import '../../style/vendor/media';
@import "../../style/vendor/media";
.library {
.ui.segment.head {
@ -46,18 +54,16 @@ export default {
}
&.with-background {
.header {
&, .sub {
&,
.sub {
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.8);
color: white !important;
}
}
.segment-content {
background-color: rgba(0, 0, 0, 0.5)
background-color: rgba(0, 0, 0, 0.5);
}
}
}
}
</style>

View File

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

View File

@ -1,10 +1,10 @@
<template>
<div>
<main>
<div v-if="isLoadingTrack" class="ui vertical segment" v-title="labels.title">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
<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">
<h2 class="ui center aligned icon header">
<i class="circular inverted music orange icon"></i>
@ -49,8 +49,8 @@
<translate>Download</translate>
</a>
</div>
</div>
<div class="ui vertical stripe center aligned segment" v-if="upload">
</section>
<section class="ui vertical stripe center aligned segment" v-if="upload">
<h2 class="ui header"><translate>Track information</translate></h2>
<table class="ui very basic collapsing celled center aligned table">
<tbody>
@ -100,8 +100,8 @@
</tr>
</tbody>
</table>
</div>
<div class="ui vertical stripe center aligned segment">
</section>
<section class="ui vertical stripe center aligned segment">
<h2>
<translate>Lyrics</translate>
</h2>
@ -117,41 +117,40 @@
<translate>Search on lyrics.wikia.com</translate>
</a>
</template>
</div>
<div class="ui vertical stripe segment">
</section>
<section class="ui vertical stripe segment">
<h2>
<translate>User libraries</translate>
</h2>
<library-widget :url="'tracks/' + id + '/libraries/'">
<translate slot="subtitle">This track is present in the following libraries:</translate>
</library-widget>
</div>
</section>
</template>
</div>
</main>
</template>
<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'
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/'
const FETCH_URL = "tracks/"
export default {
props: ['id'],
props: ["id"],
components: {
PlayButton,
TrackPlaylistIcon,
TrackFavoriteIcon,
LibraryWidget
},
data () {
data() {
return {
time,
isLoadingTrack: true,
@ -160,78 +159,94 @@ export default {
lyrics: null
}
},
created () {
created() {
this.fetchData()
this.fetchLyrics()
},
methods: {
fetchData () {
fetchData() {
var self = this
this.isLoadingTrack = true
let url = FETCH_URL + this.id + '/'
let url = FETCH_URL + this.id + "/"
logger.default.debug('Fetching track "' + this.id + '"')
axios.get(url).then((response) => {
axios.get(url).then(response => {
self.track = response.data
self.isLoadingTrack = false
})
},
fetchLyrics () {
fetchLyrics() {
var self = this
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 + '"')
axios.get(url).then((response) => {
self.lyrics = response.data
self.isLoadingLyrics = false
}, (response) => {
console.error('No lyrics available')
self.isLoadingLyrics = false
})
axios.get(url).then(
response => {
self.lyrics = response.data
self.isLoadingLyrics = false
},
response => {
console.error("No lyrics available")
self.isLoadingLyrics = false
}
)
}
},
computed: {
labels () {
labels() {
return {
title: this.$gettext('Track')
title: this.$gettext("Track")
}
},
upload () {
upload() {
if (this.track.uploads) {
return this.track.uploads[0]
}
},
wikipediaUrl () {
return 'https://en.wikipedia.org/w/index.php?search=' + encodeURI(this.track.title + ' ' + this.track.artist.name)
wikipediaUrl() {
return (
"https://en.wikipedia.org/w/index.php?search=" +
encodeURI(this.track.title + " " + this.track.artist.name)
)
},
musicbrainzUrl () {
musicbrainzUrl() {
if (this.track.mbid) {
return 'https://musicbrainz.org/recording/' + this.track.mbid
return "https://musicbrainz.org/recording/" + this.track.mbid
}
},
downloadUrl () {
let u = this.$store.getters['instance/absoluteUrl'](this.upload.listen_url)
downloadUrl() {
let u = this.$store.getters["instance/absoluteUrl"](
this.upload.listen_url
)
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
},
lyricsSearchUrl () {
let base = 'http://lyrics.wikia.com/wiki/Special:Search?query='
let query = this.track.artist.name + ' ' + this.track.title
lyricsSearchUrl() {
let base = "http://lyrics.wikia.com/wiki/Special:Search?query="
let query = this.track.artist.name + " " + this.track.title
return base + encodeURI(query)
},
cover () {
cover() {
return null
},
headerStyle () {
headerStyle() {
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: {
id () {
id() {
this.fetchData()
}
}

View File

@ -1,7 +1,7 @@
<template>
<div class="ui vertical stripe segment" v-title="labels.title">
<div>
<div>
<section>
<h2 class="ui header">
<translate>Builder</translate>
</h2>
@ -87,28 +87,28 @@
</h3>
<track-table v-if="checkResult.candidates.sample" :tracks="checkResult.candidates.sample"></track-table>
</template>
</div>
</section>
</div>
</div>
</template>
<script>
import axios from 'axios'
import $ from 'jquery'
import _ from 'lodash'
import BuilderFilter from './Filter'
import TrackTable from '@/components/audio/track/Table'
import RadioButton from '@/components/radios/Button'
import axios from "axios"
import $ from "jquery"
import _ from "lodash"
import BuilderFilter from "./Filter"
import TrackTable from "@/components/audio/track/Table"
import RadioButton from "@/components/radios/Button"
export default {
props: {
id: {required: false}
id: { required: false }
},
components: {
BuilderFilter,
TrackTable,
RadioButton
},
data: function () {
data: function() {
return {
isLoading: false,
success: false,
@ -116,12 +116,12 @@ export default {
currentFilterType: null,
filters: [],
checkResult: null,
radioName: '',
radioDesc: '',
radioName: "",
radioDesc: "",
isPublic: true
}
},
created: function () {
created: function() {
let self = this
this.fetchFilters().then(() => {
if (self.id) {
@ -129,18 +129,18 @@ export default {
}
})
},
mounted () {
$('.ui.dropdown').dropdown()
mounted() {
$(".ui.dropdown").dropdown()
},
methods: {
fetchFilters: function () {
fetchFilters: function() {
let self = this
let url = 'radios/radios/filters/'
return axios.get(url).then((response) => {
let url = "radios/radios/filters/"
return axios.get(url).then(response => {
self.availableFilters = response.data
})
},
add () {
add() {
this.filters.push({
config: {},
filter: this.currentFilter,
@ -148,23 +148,25 @@ export default {
})
this.fetchCandidates()
},
updateConfig (index, field, value) {
updateConfig(index, field, value) {
this.filters[index].config[field] = value
this.fetchCandidates()
},
deleteFilter (index) {
deleteFilter(index) {
this.filters.splice(index, 1)
this.fetchCandidates()
},
fetch: function () {
fetch: function() {
let self = this
self.isLoading = true
let url = 'radios/radios/' + this.id + '/'
axios.get(url).then((response) => {
let url = "radios/radios/" + this.id + "/"
axios.get(url).then(response => {
self.filters = response.data.config.map(f => {
return {
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()
}
})
@ -174,24 +176,22 @@ export default {
self.isLoading = false
})
},
fetchCandidates: function () {
fetchCandidates: function() {
let self = this
let url = 'radios/radios/validate/'
let url = "radios/radios/validate/"
let final = this.filters.map(f => {
let c = _.clone(f.config)
c.type = f.filter.type
return c
})
final = {
'filters': [
{'type': 'group', filters: final}
]
filters: [{ type: "group", filters: final }]
}
axios.post(url, final).then((response) => {
axios.post(url, final).then(response => {
self.checkResult = response.data.filters[0]
})
},
save: function () {
save: function() {
let self = this
self.success = false
self.isLoading = true
@ -202,24 +202,24 @@ export default {
return c
})
final = {
'name': this.radioName,
'description': this.radioDesc,
'is_public': this.isPublic,
'config': final
name: this.radioName,
description: this.radioDesc,
is_public: this.isPublic,
config: final
}
if (this.id) {
let url = 'radios/radios/' + this.id + '/'
axios.put(url, final).then((response) => {
let url = "radios/radios/" + this.id + "/"
axios.put(url, final).then(response => {
self.isLoading = false
self.success = true
})
} else {
let url = 'radios/radios/'
axios.post(url, final).then((response) => {
let url = "radios/radios/"
axios.post(url, final).then(response => {
self.success = true
self.isLoading = false
self.$router.push({
name: 'library.radios.detail',
name: "library.radios.detail",
params: {
id: response.data.id
}
@ -229,30 +229,28 @@ export default {
}
},
computed: {
labels () {
let title = this.$gettext('Radio Builder')
labels() {
let title = this.$gettext("Radio Builder")
let placeholder = {
'name': this.$gettext('My awesome radio'),
'description': this.$gettext('My awesome description')
name: this.$gettext("My awesome radio"),
description: this.$gettext("My awesome description")
}
return {
title,
placeholder
}
},
canSave: function () {
return (
this.radioName.length > 0 && this.checkErrors.length === 0
)
canSave: function() {
return this.radioName.length > 0 && this.checkErrors.length === 0
},
checkErrors: function () {
checkErrors: function() {
if (!this.checkResult) {
return []
}
let errors = this.checkResult.errors
return errors
},
currentFilter: function () {
currentFilter: function() {
let self = this
return this.availableFilters.filter(e => {
return e.type === self.currentFilterType
@ -261,7 +259,7 @@ export default {
},
watch: {
filters: {
handler: function () {
handler: function() {
this.fetchCandidates()
},
deep: true

View File

@ -5,9 +5,9 @@
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
<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>
</div>
</header>
<div class="description">
<table class="ui very basic fixed single line compact table">
<tbody>
@ -32,29 +32,29 @@
</template>
<script>
import Vue from 'vue'
import CardMixin from './CardMixin'
import time from '@/utils/time'
import Vue from "vue"
import CardMixin from "./CardMixin"
import time from "@/utils/time"
export default Vue.extend({
mixins: [CardMixin],
data () {
data() {
return {
time
}
},
computed: {
labels () {
labels() {
return {
musicbrainz: this.$gettext('View on MusicBrainz')
musicbrainz: this.$gettext("View on MusicBrainz")
}
},
type () {
return 'artist'
type() {
return "artist"
},
releasesGroups () {
return this.data['release-group-list'].filter(r => {
return r.type === 'Album'
releasesGroups() {
return this.data["release-group-list"].filter(r => {
return r.type === "Album"
})
}
}
@ -64,6 +64,6 @@ export default Vue.extend({
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.ui.card {
width: 100% !important;
width: 100% !important;
}
</style>

View File

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

View File

@ -1,5 +1,5 @@
<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 text container">
<div :class="['ui', {'loading': isLoading}, 'form']"></div>
@ -24,146 +24,140 @@
</div>
</div>
</div>
</main>
</template>
<script>
import axios from 'axios'
import $ from 'jquery'
import axios from "axios"
import $ from "jquery"
import SettingsGroup from '@/components/admin/SettingsGroup'
import SettingsGroup from "@/components/admin/SettingsGroup"
export default {
components: {
SettingsGroup
},
data () {
data() {
return {
isLoading: false,
settingsData: null,
current: null
}
},
created () {
created() {
let self = this
this.fetchSettings().then(r => {
self.$nextTick(() => {
if (self.$store.state.route.hash) {
self.scrollTo(self.$store.state.route.hash.substr(1))
}
$('select.dropdown').dropdown()
$("select.dropdown").dropdown()
})
})
},
methods: {
scrollTo (id) {
scrollTo(id) {
this.current = id
document.getElementById(id).scrollIntoView()
},
fetchSettings () {
fetchSettings() {
let self = this
self.isLoading = true
return axios.get('instance/admin/settings/').then((response) => {
return axios.get("instance/admin/settings/").then(response => {
self.settingsData = response.data
self.isLoading = false
})
}
},
computed: {
labels () {
labels() {
return {
settings: this.$gettext('Instance settings')
settings: this.$gettext("Instance settings")
}
},
groups () {
groups() {
// somehow, extraction fails if in the return block directly
let instanceLabel = this.$gettext('Instance information')
let usersLabel = this.$gettext('Users')
let musicLabel = this.$gettext('Music')
let playlistsLabel = this.$gettext('Playlists')
let federationLabel = this.$gettext('Federation')
let subsonicLabel = this.$gettext('Subsonic')
let statisticsLabel = this.$gettext('Statistics')
let errorLabel = this.$gettext('Error reporting')
let instanceLabel = this.$gettext("Instance information")
let usersLabel = this.$gettext("Users")
let musicLabel = this.$gettext("Music")
let playlistsLabel = this.$gettext("Playlists")
let federationLabel = this.$gettext("Federation")
let subsonicLabel = this.$gettext("Subsonic")
let statisticsLabel = this.$gettext("Statistics")
let errorLabel = this.$gettext("Error reporting")
return [
{
label: instanceLabel,
id: 'instance',
id: "instance",
settings: [
'instance__name',
'instance__short_description',
'instance__long_description'
"instance__name",
"instance__short_description",
"instance__long_description"
]
},
{
label: usersLabel,
id: 'users',
id: "users",
settings: [
'users__registration_enabled',
'common__api_authentication_required',
'users__default_permissions',
'users__upload_quota'
"users__registration_enabled",
"common__api_authentication_required",
"users__default_permissions",
"users__upload_quota"
]
},
{
label: musicLabel,
id: 'music',
id: "music",
settings: [
'music__transcoding_enabled',
'music__transcoding_cache_duration',
"music__transcoding_enabled",
"music__transcoding_cache_duration"
]
},
{
label: playlistsLabel,
id: 'playlists',
settings: [
'playlists__max_tracks'
]
id: "playlists",
settings: ["playlists__max_tracks"]
},
{
label: federationLabel,
id: 'federation',
id: "federation",
settings: [
'federation__enabled',
'federation__music_needs_approval',
'federation__collection_page_size',
'federation__music_cache_duration',
'federation__actor_fetch_delay'
"federation__enabled",
"federation__music_needs_approval",
"federation__collection_page_size",
"federation__music_cache_duration",
"federation__actor_fetch_delay"
]
},
{
label: subsonicLabel,
id: 'subsonic',
settings: [
'subsonic__enabled'
]
id: "subsonic",
settings: ["subsonic__enabled"]
},
{
label: statisticsLabel,
id: 'statistics',
id: "statistics",
settings: [
'instance__nodeinfo_enabled',
'instance__nodeinfo_stats_enabled',
'instance__nodeinfo_private'
"instance__nodeinfo_enabled",
"instance__nodeinfo_stats_enabled",
"instance__nodeinfo_private"
]
},
{
label: errorLabel,
id: 'reporting',
settings: [
'raven__front_enabled',
'raven__front_dsn'
]
id: "reporting",
settings: ["raven__front_enabled", "raven__front_dsn"]
}
]
}
},
watch: {
settingsData () {
settingsData() {
let self = this
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>
<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
class="ui item"
:to="{name: 'manage.library.files'}"><translate>Files</translate></router-link>
</div>
</nav>
<router-view :key="$route.fullPath"></router-view>
</div>
</template>
@ -12,10 +12,12 @@
<script>
export default {
computed: {
labels () {
let title = this.$gettext('Manage library')
labels() {
let title = this.$gettext("Manage library")
let secondaryMenu = this.$gettext("Secondary menu")
return {
title
title,
secondaryMenu
}
}
}
@ -23,10 +25,8 @@ export default {
</script>
<style scoped>
.ui.menu .item > .label {
position: absolute;
right: -2em;
}
</style>

View File

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

View File

@ -1,13 +1,13 @@
<template>
<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
class="ui item"
:to="{name: 'manage.users.users.list'}"><translate>Users</translate></router-link>
<router-link
class="ui item"
:to="{name: 'manage.users.invitations.list'}"><translate>Invitations</translate></router-link>
</div>
</nav>
<router-view :key="$route.fullPath"></router-view>
</div>
</template>
@ -15,9 +15,10 @@
<script>
export default {
computed: {
labels () {
labels() {
return {
manageUsers: this.$gettext('Manage users')
manageUsers: this.$gettext("Manage users"),
secondaryMenu: this.$gettext("Secondary menu")
}
}
}

View File

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

View File

@ -1,10 +1,10 @@
<template>
<div>
<main>
<div v-if="isLoading" class="ui vertical segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
<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">
<h2 class="ui center aligned icon header">
<i class="circular inverted user red icon"></i>
@ -102,35 +102,34 @@
</div>
<div class="ui hidden divider"></div>
<button @click="fetchData" class="ui basic button"><translate>Refresh</translate></button>
</div>
</section>
</template>
</div>
</main>
</template>
<script>
import $ from 'jquery'
import axios from 'axios'
import logger from '@/logging'
import $ from "jquery"
import axios from "axios"
import logger from "@/logging"
export default {
props: ['id'],
data () {
props: ["id"],
data() {
return {
isLoading: true,
object: null,
permissions: []
}
},
created () {
created() {
this.fetchData()
},
methods: {
fetchData () {
fetchData() {
var self = this
this.isLoading = true
let url = 'manage/users/users/' + this.id + '/'
axios.get(url).then((response) => {
let url = "manage/users/users/" + this.id + "/"
axios.get(url).then(response => {
self.object = response.data
self.permissions = []
self.allPermissions.forEach(p => {
@ -141,60 +140,72 @@ export default {
self.isLoading = false
})
},
update (attr, toNull) {
update(attr, toNull) {
let newValue = this.object[attr]
if (toNull && !newValue) {
newValue = null
}
console.log(newValue, typeof(newValue))
console.log(newValue, typeof newValue)
let params = {}
if (attr === 'permissions') {
params['permissions'] = {}
if (attr === "permissions") {
params["permissions"] = {}
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 {
params[attr] = newValue
}
axios.patch('manage/users/users/' + this.id + '/', params).then((response) => {
logger.default.info(`${attr} was updated succcessfully to ${newValue}`)
}, (error) => {
logger.default.error(`Error while setting ${attr} to ${newValue}`, error)
})
axios.patch("manage/users/users/" + this.id + "/", params).then(
response => {
logger.default.info(
`${attr} was updated succcessfully to ${newValue}`
)
},
error => {
logger.default.error(
`Error while setting ${attr} to ${newValue}`,
error
)
}
)
}
},
computed: {
labels () {
labels() {
return {
inactive: this.$gettext('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.')
inactive: this.$gettext(
"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 [
{
'code': 'upload',
'label': this.$gettext('Upload')
code: "upload",
label: this.$gettext("Upload")
},
{
'code': 'library',
'label': this.$gettext('Library')
code: "library",
label: this.$gettext("Library")
},
{
'code': 'federation',
'label': this.$gettext('Federation')
code: "federation",
label: this.$gettext("Federation")
},
{
'code': 'settings',
'label': this.$gettext('Settings')
code: "settings",
label: this.$gettext("Settings")
}
]
}
},
watch: {
object () {
object() {
this.$nextTick(() => {
$('select.dropdown').dropdown()
$("select.dropdown").dropdown()
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<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 class="ui text loader"><translate>Loading library data...</translate></div>
</div>
@ -64,15 +64,15 @@
<library-form :library="library" @updated="libraryUpdated" @deleted="libraryDeleted" />
</div>
</detail-area>
</div>
</section>
</template>
<script>
import axios from 'axios'
import DetailMixin from './DetailMixin'
import DetailArea from './DetailArea'
import LibraryForm from './Form'
import LibraryFilesTable from './FilesTable'
import axios from "axios"
import DetailMixin from "./DetailMixin"
import DetailArea from "./DetailArea"
import LibraryForm from "./Form"
import LibraryFilesTable from "./FilesTable"
export default {
mixins: [DetailMixin],
@ -81,46 +81,48 @@ export default {
LibraryForm,
LibraryFilesTable
},
data () {
data() {
return {
currentTab: 'follows',
currentTab: "follows",
isLoadingFollows: false,
follows: null
}
},
created () {
created() {
this.fetchFollows()
},
methods: {
libraryUpdated () {
libraryUpdated() {
this.hiddenForm = true
this.fetch()
},
libraryDeleted () {
libraryDeleted() {
this.$router.push({
name: 'content.libraries.index'
name: "content.libraries.index"
})
},
fetchFollows () {
fetchFollows() {
let self = this
self.isLoadingLibrary = true
axios.get(`libraries/${this.id}/follows/`).then((response) => {
axios.get(`libraries/${this.id}/follows/`).then(response => {
self.follows = response.data
self.isLoadingFollows = false
})
},
updateApproved (follow, value) {
updateApproved(follow, value) {
let self = this
let action
if (value) {
action = 'accept'
action = "accept"
} else {
action = 'reject'
action = "reject"
}
axios.post(`federation/follows/library/${follow.uuid}/${action}/`).then((response) => {
follow.isLoading = false
follow.approved = value
})
axios
.post(`federation/follows/library/${follow.uuid}/${action}/`)
.then(response => {
follow.isLoading = false
follow.approved = value
})
}
}
}

View File

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

View File

@ -1,5 +1,5 @@
<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 class="ui text loader"><translate>Loading Libraries...</translate></div>
</div>
@ -24,24 +24,24 @@
</div>
</div>
</div>
</div>
</section>
</template>
<script>
import axios from 'axios'
import LibraryForm from './Form'
import LibraryCard from './Card'
import Quota from './Quota'
import axios from "axios"
import LibraryForm from "./Form"
import LibraryCard from "./Card"
import Quota from "./Quota"
export default {
data () {
data() {
return {
isLoading: false,
hiddenForm: true,
libraries: []
}
},
created () {
created() {
this.fetch()
},
components: {
@ -50,10 +50,10 @@ export default {
Quota
},
methods: {
fetch () {
fetch() {
this.isLoading = true
let self = this
axios.get('libraries/').then((response) => {
axios.get("libraries/").then(response => {
self.isLoading = false
self.libraries = response.data.results
if (self.libraries.length === 0) {
@ -61,7 +61,7 @@ export default {
}
})
},
libraryCreated (library) {
libraryCreated(library) {
this.hiddenForm = true
this.libraries.unshift(library)
}

View File

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

View File

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

View File

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