Skip to content
Snippets Groups Projects
Commit c0904ca8 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

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
parents 9005ebbd 29171853
Branches
Tags
No related merge requests found
Showing
with 738 additions and 634 deletions
Improved accessibility by using main/section/nav tags and aria-labels in most critical places (#612)
<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,20 +31,20 @@
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')
this.$store.dispatch("instance/fetchSettings")
},
computed: {
...mapState({
......@@ -52,7 +52,7 @@ export default {
}),
labels() {
return {
title: this.$gettext('About this instance')
title: this.$gettext("About this instance")
}
}
}
......
<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,25 +53,28 @@
<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?'))
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({
......@@ -79,14 +82,17 @@ export default {
}),
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']
let instances = [
this.$store.getters["instance/defaultUrl"](),
"https://demo.funkwhale.audio"
]
return instances
},
}
}
}
</script>
......
<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() {
return {
musicbrainzUrl: 'https://musicbrainz.org/'
musicbrainzUrl: "https://musicbrainz.org/"
}
},
computed: {
labels() {
return {
title: this.$gettext('Welcome')
title: this.$gettext("Welcome")
}
}
}
......
<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,8 +16,8 @@
<i class="right arrow icon"></i>
</router-link>
</div>
</div>
</div>
</section>
</main>
</template>
<script>
......@@ -30,7 +30,7 @@ export default {
computed: {
labels() {
return {
title: this.$gettext('Page Not Found')
title: this.$gettext("Page Not Found")
}
}
}
......
<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,7 +24,7 @@
</template>
<script>
import _ from 'lodash'
import _ from "lodash"
export default {
props: {
......@@ -34,20 +34,32 @@ export default {
compact: { type: Boolean, default: false }
},
computed: {
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,8 +71,8 @@ export default {
if (consecutive) {
final.push(p)
} else {
if (p !== 'skip') {
final.push('skip')
if (p !== "skip") {
final.push("skip")
final.push(p)
}
}
......@@ -77,7 +89,7 @@ export default {
return
}
if (this.current !== page) {
this.$emit('page-changed', page)
this.$emit("page-changed", page)
}
}
}
......
<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,25 +154,25 @@
<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,
......@@ -177,15 +181,17 @@ export default {
},
data() {
return {
selectedTab: 'library',
selectedTab: "library",
backend: backend,
tracksChangeBuffer: null,
isCollapsed: true,
fetchInterval: null,
fetchInterval: null
}
},
mounted() {
$(this.$el).find('.menu .item').tab()
$(this.$el)
.find(".menu .item")
.tab()
},
destroy() {
if (this.fetchInterval) {
......@@ -198,11 +204,15 @@ export default {
url: state => state.route.path
}),
labels() {
let pendingRequests = this.$gettext('Pending import requests')
let pendingFollows = this.$gettext('Pending follow requests')
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: {
......@@ -215,32 +225,38 @@ export default {
},
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})
this.$store.commit("queue/reorder", {
tracks: this.tracksChangeBuffer,
oldIndex: event.oldIndex,
newIndex: event.newIndex
})
},
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
}
},
......@@ -249,22 +265,22 @@ export default {
this.isCollapsed = true
},
selectedTab: function(newValue) {
if (newValue === 'queue') {
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;
......@@ -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;
......
<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: {
......@@ -234,7 +234,12 @@ export default {
AudioTrack
},
data() {
let defaultAmbiantColors = [[46, 46, 46], [46, 46, 46], [46, 46, 46], [46, 46, 46]]
let defaultAmbiantColors = [
[46, 46, 46],
[46, 46, 46],
[46, 46, 46],
[46, 46, 46]
]
return {
isShuffling: false,
sliderVolume: this.volume,
......@@ -254,7 +259,7 @@ export default {
this.dummyAudio = new Howl({
preload: false,
autoplay: false,
src: ['noop.webm', 'noop.mp3']
src: ["noop.webm", "noop.mp3"]
})
},
destroyed() {
......@@ -262,11 +267,11 @@ export default {
},
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() {
let disabled = this.queue.tracks.length === 0
......@@ -274,12 +279,12 @@ export default {
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()
})
......@@ -288,20 +293,20 @@ export default {
},
next() {
let self = this
this.$store.dispatch('queue/next').then(() => {
self.$emit('next')
this.$store.dispatch("queue/next").then(() => {
self.$emit("next")
})
},
previous() {
let self = this
this.$store.dispatch('queue/previous').then(() => {
self.$emit('previous')
this.$store.dispatch("queue/previous").then(() => {
self.$emit("previous")
})
},
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() {
......@@ -313,8 +318,8 @@ export default {
this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4)
},
handleError({ sound, error }) {
this.$store.commit('player/isLoadingAudio', false)
this.$store.dispatch('player/trackErrored')
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')
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,
......@@ -365,7 +378,7 @@ export default {
},
style: function() {
let style = {
'background': this.ambiantGradiant
background: this.ambiantGradiant
}
return style
},
......@@ -376,11 +389,17 @@ export default {
{ orientation: 150, percent: 80, opacity: 0.7 },
{ orientation: 60, percent: 70, opacity: 0.7 }
]
let gradients = this.ambiantColors.map((e, i) => {
let gradients = this.ambiantColors
.map((e, i) => {
let [r, g, b] = e
let 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 `linear-gradient(${
conf.orientation
}deg, rgba(${r}, ${g}, ${b}, ${
conf.opacity
}) 10%, rgba(255, 255, 255, 0) ${conf.percent}%)`
})
.join(", ")
return gradients
}
},
......@@ -397,7 +416,7 @@ export default {
this.sliderVolume = newValue
},
sliderVolume(newValue) {
this.$store.commit('player/volume', newValue)
this.$store.commit("player/volume", newValue)
}
}
}
......@@ -405,7 +424,6 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<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;
}
.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,7 +566,6 @@ export default {
margin: 0;
}
@keyframes MOVE-BG {
from {
transform: translateX(0px);
......@@ -576,7 +596,7 @@ export default {
grey 1px,
grey 10px,
transparent 10px,
transparent 20px,
transparent 20px
) !important;
animation-name: MOVE-BG;
......
<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,16 +43,16 @@
</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
......@@ -62,10 +62,10 @@ export default {
// We need to initialize the component with any
// properties that will be used in it
credentials: {
username: '',
password: ''
username: "",
password: ""
},
error: '',
error: "",
isLoading: false
}
},
......@@ -74,8 +74,8 @@ export default {
},
computed: {
labels() {
let usernamePlaceholder = this.$gettext('Enter your username or email')
let title = this.$gettext('Log In')
let usernamePlaceholder = this.$gettext("Enter your username or email")
let title = this.$gettext("Log In")
return {
usernamePlaceholder,
title
......@@ -86,27 +86,28 @@ export default {
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', {
this.$store
.dispatch("auth/login", {
credentials,
next: '/library',
next: "/library",
onError: error => {
if (error.response.status === 400) {
self.error = 'invalid_credentials'
self.error = "invalid_credentials"
} else {
self.error = 'unknown_error'
self.error = "unknown_error"
}
}
}).then(e => {
})
.then(e => {
self.isLoading = false
})
}
}
}
</script>
......
<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,8 +8,8 @@
<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>
......@@ -17,7 +17,7 @@ export default {
computed: {
labels() {
return {
title: this.$gettext('Log Out')
title: this.$gettext("Log Out")
}
}
}
......
<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,34 +25,35 @@
</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'],
props: ["username"],
created() {
this.$store.dispatch('auth/fetchProfile')
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})
let msg = this.$gettext("%{ username }'s profile")
let usernameProfile = this.$gettextInterpolate(msg, {
username: this.username
})
return {
usernameProfile
}
},
signupDate() {
let d = new Date(this.profile.date_joined)
return dateFormat(d, 'longDate')
return dateFormat(d, "longDate")
},
isLoading() {
return !this.profile
......
<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 />
</section>
</div>
</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],
......@@ -130,10 +130,10 @@ export default {
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"]
}
}
}
......@@ -158,7 +158,7 @@ export default {
return d
},
mounted() {
$('select.dropdown').dropdown()
$("select.dropdown").dropdown()
},
methods: {
submitSettings() {
......@@ -167,17 +167,20 @@ export default {
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')
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)
return axios.get("users/users/me/").then(response => {
self.$store.dispatch("auth/updateProfile", response.data)
})
}, error => {
logger.default.error('Error while updating settings')
},
error => {
logger.default.error("Error while updating settings")
self.isLoading = false
self.settings.errors = error.backendErrors
})
}
)
},
submitAvatar() {
this.isLoadingAvatar = true
......@@ -185,71 +188,80 @@ export default {
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 => {
})
.then(
response => {
this.isLoadingAvatar = false
self.currentAvatar = response.data.avatar
self.$store.commit('auth/avatar', self.currentAvatar)
}, error => {
self.$store.commit("auth/avatar", self.currentAvatar)
},
error => {
self.isLoadingAvatar = false
self.avatarErrors = error.backendErrors
})
}
)
},
removeAvatar() {
this.isLoadingAvatar = true
let self = this
this.avatar = null
axios.patch(
`users/users/${this.$store.state.auth.username}/`,
{avatar: null}
).then(response => {
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.$store.commit("auth/avatar", self.currentAvatar)
},
error => {
self.isLoadingAvatar = false
self.avatarErrors = error.backendErrors
})
}
)
},
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')
let url = "auth/registration/change-password/"
return axios.post(url, credentials).then(
response => {
logger.default.info("Password successfully changed")
self.$router.push({
name: 'profile',
name: "profile",
params: {
username: self.$store.state.auth.username
}})
}, error => {
}
})
},
error => {
if (error.response.status === 400) {
self.passwordError = 'invalid_credentials'
self.passwordError = "invalid_credentials"
} else {
self.passwordError = 'unknown_error'
self.passwordError = "unknown_error"
}
self.isLoading = false
})
}
)
}
},
computed: {
labels() {
return {
title: this.$gettext('Account Settings')
title: this.$gettext("Account Settings")
}
},
orderedSettingsFields() {
......@@ -268,7 +280,6 @@ export default {
return s
}
}
}
</script>
......
<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,29 +53,29 @@
</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: '/'}
next: { type: String, default: "/" }
},
components: {
PasswordInput
},
data() {
return {
username: '',
email: '',
password: '',
username: "",
email: "",
password: "",
isLoadingInstanceSetting: true,
errors: [],
isLoading: false,
......@@ -84,7 +84,7 @@ export default {
},
created() {
let self = this
this.$store.dispatch('instance/fetchSettings', {
this.$store.dispatch("instance/fetchSettings", {
callback: function() {
self.isLoadingInstanceSetting = false
}
......@@ -92,10 +92,12 @@ export default {
},
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')
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,
......@@ -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')
return axios.post("auth/registration/", payload).then(
response => {
logger.default.info("Successfully created account")
self.$router.push({
name: 'profile',
name: "profile",
params: {
username: this.username
}})
}, error => {
}
})
},
error => {
self.errors = error.backendErrors
self.isLoading = false
})
}
)
}
}
}
......
<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],
......@@ -80,7 +80,9 @@ export default {
Pagination
},
data() {
let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
let defaultOrdering = this.getOrderingFromString(
this.defaultOrdering || "-creation_date"
)
return {
results: null,
isLoading: false,
......@@ -88,13 +90,13 @@ 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"]
]
}
},
......@@ -102,12 +104,12 @@ export default {
this.fetchFavorites(FAVORITES_URL)
},
mounted() {
$('.ui.dropdown').dropdown()
$(".ui.dropdown").dropdown()
},
computed: {
labels() {
return {
title: this.$gettext('Your Favorites')
title: this.$gettext("Your Favorites")
}
}
},
......@@ -125,20 +127,20 @@ export default {
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
})
},
......
<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,37 +38,37 @@
<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,
......@@ -87,9 +87,9 @@ export default {
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
})
......@@ -98,22 +98,29 @@ export default {
computed: {
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)
return (
"https://en.wikipedia.org/w/index.php?search=" +
encodeURI(this.album.title + " " + this.album.artist.name)
)
},
musicbrainzUrl() {
if (this.album.mbid) {
return 'https://musicbrainz.org/release/' + this.album.mbid
return "https://musicbrainz.org/release/" + this.album.mbid
}
},
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: {
......@@ -126,5 +133,4 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
</style>
<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,
......@@ -107,18 +107,22 @@ export default {
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) => {
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) => {
self.albums = parsed.map(album => {
return backend.Album.clean(album)
})
......@@ -130,20 +134,25 @@ export default {
computed: {
labels() {
return {
title: this.$gettext('Artist')
title: this.$gettext("Artist")
}
},
isPlayable() {
return this.artist.albums.filter((a) => {
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)
return (
"https://en.wikipedia.org/w/index.php?search=" +
encodeURI(this.artist.name)
)
},
musicbrainzUrl() {
if (this.artist.mbid) {
return 'https://musicbrainz.org/artist/' + this.artist.mbid
return "https://musicbrainz.org/artist/" + this.artist.mbid
}
},
allTracks() {
......@@ -156,17 +165,23 @@ export default {
return tracks
},
cover() {
return this.artist.albums.filter(album => {
return this.artist.albums
.filter(album => {
return album.cover
}).map(album => {
})
.map(album => {
return album.cover
})[0]
},
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: {
......
<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')
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() {
this.fetchData()
},
mounted() {
$('.ui.dropdown').dropdown()
$(".ui.dropdown").dropdown()
},
computed: {
labels() {
let searchPlaceholder = this.$gettext('Enter an artist name...')
let title = this.$gettext('Artists')
let searchPlaceholder = this.$gettext("Enter an artist name...")
let title = this.$gettext("Artists")
return {
searchPlaceholder,
title
......@@ -144,10 +143,10 @@ 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
})
......
<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,
......@@ -62,7 +62,7 @@ export default {
computed: {
labels() {
return {
title: this.$gettext('Home')
title: this.$gettext("Home")
}
}
},
......@@ -71,14 +71,14 @@ export default {
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
})
}
......
<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>
......@@ -22,7 +22,15 @@
export default {
computed: {
showImports() {
return this.$store.state.auth.availablePermissions['upload'] || this.$store.state.auth.availablePermissions['library']
return (
this.$store.state.auth.availablePermissions["upload"] ||
this.$store.state.auth.availablePermissions["library"]
)
},
labels() {
return {
secondaryMenu: this.$gettext("Secondary menu")
}
}
}
}
......@@ -30,7 +38,7 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<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>
<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')
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() {
this.fetchData()
},
mounted() {
$('.ui.dropdown').dropdown()
$(".ui.dropdown").dropdown()
},
computed: {
labels() {
let searchPlaceholder = this.$gettext('Enter a radio name...')
let title = this.$gettext('Radios')
let searchPlaceholder = this.$gettext("Enter a radio name...")
let title = this.$gettext("Radios")
return {
searchPlaceholder,
title
......@@ -167,8 +166,8 @@ 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
})
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment