Commit 11d6c7cf authored by Ciarán Ainsworth's avatar Ciarán Ainsworth
Browse files

Added placeholders across the application

parent 860522a2
Placeholders will now be shown if no content is available across the application (#750)
\ No newline at end of file
......@@ -35,7 +35,14 @@
</div>
</div>
</div>
<div v-if="!isLoading && albums.length === 0">No results matching your query.</div>
<template v-if="!isLoading && albums.length === 0">
<div class="ui placeholder segment">
<div class="ui icon header">
<i class="compact disc icon"></i>
No results matching your query
</div>
</div>
</template>
</div>
</template>
......
......@@ -7,7 +7,7 @@
<button :disabled="!previousPage" @click="fetchData(previousPage)" :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle up', 'icon']"></i></button>
<button :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle down', 'icon']"></i></button>
<button @click="fetchData(url)" :class="['ui', 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'refresh', 'icon']"></i></button>
<div class="ui divided unstackable items">
<div v-if="count > 0" class="ui divided unstackable items">
<div :class="['item', itemClasses]" v-for="object in objects" :key="object.id">
<div class="ui tiny image">
<img v-if="object.track.album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.medium_square_crop)">
......@@ -51,6 +51,17 @@
<div class="ui loader"></div>
</div>
</div>
<div v-else class="ui placeholder segment">
<div class="ui icon header">
<i class="music icon"></i>
<translate translate-context="Content/Home/Placeholder">
Nothing found
</translate>
</div>
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
</div>
</div>
</template>
......
......@@ -18,7 +18,7 @@
</h2>
<radio-button v-if="hasFavorites" type="favorites"></radio-button>
</section>
<section class="ui vertical stripe segment">
<section v-if="hasFavorites" class="ui vertical stripe segment">
<div :class="['ui', {'loading': isLoading}, 'form']">
<div class="fields">
<div class="field">
......@@ -46,7 +46,6 @@
</div>
</div>
</div>
<track-table v-if="results" :tracks="results.results"></track-table>
<div class="ui center aligned basic segment">
<pagination
......@@ -58,6 +57,18 @@
></pagination>
</div>
</section>
<div v-else class="ui placeholder segment">
<div class="ui icon header">
<i class="broken heart icon"></i>
<translate
translate-context="Content/Home/Placeholder"
>No tracks have been added to your favorites yet</translate>
</div>
<router-link :to="'/library'" class="ui green labeled icon button">
<i class="headphones icon"></i>
<translate translate-context="Content/*/Verb">Browse the library</translate>
</router-link>
</div>
</main>
</template>
......
......@@ -59,6 +59,23 @@
:key="album.id"
:album="album"></album-card>
</div>
<div v-else class="ui placeholder segment sixteen wide column" style="text-align: center; display: flex; align-items: center">
<div class="ui icon header">
<i class="compact disc icon"></i>
<translate translate-context="Content/Albums/Placeholder">
No results matching your query
</translate>
</div>
<router-link
v-if="$store.state.auth.authenticated"
:to="{name: 'content.index'}"
class="ui green button labeled icon">
<i class="upload icon"></i>
<translate translate-context="Content/*/Verb">
Add some music
</translate>
</router-link>
</div>
</div>
<div class="ui center aligned basic segment">
<pagination
......
......@@ -48,6 +48,23 @@
</div>
<artist-card :artist="artist" v-for="artist in result.results" :key="artist.id"></artist-card>
</div>
<div v-else-if="!isLoading" class="ui placeholder segment sixteen wide column" style="text-align: center; display: flex; align-items: center">
<div class="ui icon header">
<i class="compact disc icon"></i>
<translate translate-context="Content/Artists/Placeholder">
No results matching your query
</translate>
</div>
<router-link
v-if="$store.state.auth.authenticated"
:to="{name: 'content.index'}"
class="ui green button labeled icon">
<i class="upload icon"></i>
<translate translate-context="Content/*/Verb">
Add some music
</translate>
</router-link>
</div>
<div class="ui center aligned basic segment">
<pagination
v-if="result && result.count > paginateBy"
......
......@@ -60,6 +60,23 @@
</div>
</div>
<div class="ui hidden divider"></div>
<div v-if="result && !result.results.length > 0" class="ui placeholder segment">
<div class="ui icon header">
<i class="feed icon"></i>
<translate translate-context="Content/Radios/Placeholder">
No results matching your query
</translate>
</div>
<router-link
v-if="$store.state.auth.authenticated"
:to="{name: 'library.radios.build'}"
class="ui green button labeled icon">
<i class="rss icon"></i>
<translate translate-context="Content/*/Verb">
Create a radio
</translate>
</router-link>
</div>
<div
v-if="result"
v-masonry
......@@ -76,7 +93,7 @@
v-for="radio in result.results"
:key="radio.id"
:custom-radio="radio"></radio-card>
</div>
</div>
</div>
<div class="ui center aligned basic segment">
<pagination
......
<template>
<div>
<div v-if="result.count > 0">
<div class="ui inline form">
<div class="fields">
<div class="ui field">
......@@ -90,6 +90,12 @@
</span>
</div>
</div>
<div v-else class="ui placeholder segment">
<div class="ui icon header">
<i class="server icon"></i>
<translate translate-context="Content/Home/Placeholder">No interactions with other pods yet</translate>
</div>
</div>
</template>
<script>
......
<template>
<div class="ui placeholder segment">
<div class="ui icon header">
<i class="list icon"></i>
<translate translate-context="Content/Home/Placeholder">
No playlists have been created yet
</translate>
</div>
<button
@click="$store.commit('playlists/chooseTrack', null)"
class="ui primary button"
>
<translate translate-context="Content/Home/CreatePlaylist">
Create Playlist
</translate>
</button>
</div>
</template>
......@@ -38,6 +38,7 @@
</ul>
</div>
</div>
<div v-if="playlists.length > 0">
<h4 class="ui header"><translate translate-context="Popup/Playlist/Title">Available playlists</translate></h4>
<table class="ui unstackable very basic table">
<thead>
......@@ -72,6 +73,17 @@
</tr>
</tbody>
</table>
</div>
<template v-else>
<div class="ui placeholder segment">
<div class="ui icon header">
<i class="list icon"></i>
<translate translate-context="Content/Home/Placeholder">
No playlists have been created yet
</translate>
</div>
</div>
</template>
</div>
</div>
<div class="actions">
......
......@@ -12,9 +12,24 @@
<template v-if="playlistsExist">
<playlist-card v-for="playlist in objects" :key="playlist.id" :playlist="playlist"></playlist-card>
</template>
<template v-else>
<placeholder-widget></placeholder-widget>
</template>
<div v-else class="ui placeholder segment">
<div class="ui icon header">
<i class="list icon"></i>
<translate translate-context="Content/Home/Placeholder">
No playlists have been created yet
</translate>
</div>
<button
v-if="$store.state.auth.authenticated"
@click="$store.commit('playlists/chooseTrack', null)"
class="ui green icon labeled button"
>
<i class="list icon"></i>
<translate translate-context="Content/Home/CreatePlaylist">
Create Playlist
</translate>
</button>
</div>
</div>
</template>
......@@ -22,7 +37,6 @@
import _ from '@/lodash'
import axios from 'axios'
import PlaylistCard from '@/components/playlists/Card'
import PlaceholderWidget from '@/components/playlists/PlaceholderWidget'
export default {
props: {
......@@ -30,8 +44,7 @@ export default {
url: {type: String, required: true}
},
components: {
PlaylistCard,
PlaceholderWidget
PlaylistCard
},
data () {
return {
......
......@@ -3,34 +3,67 @@
<div class="ui inline form">
<div class="fields">
<div class="ui six wide field">
<label><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
<label>
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
</label>
<form @submit.prevent="search.query = $refs.search.value">
<input name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" />
<input
name="search"
ref="search"
type="text"
:value="search.query"
:placeholder="labels.searchPlaceholder"
/>
</form>
</div>
<div class="field">
<label><translate translate-context="Content/*/*/Noun">Import status</translate></label>
<select class="ui dropdown" @change="addSearchToken('status', $event.target.value)" :value="getTokenValue('status', '')">
<option value=""><translate translate-context="Content/*/Dropdown">All</translate></option>
<option value="pending"><translate translate-context="Content/Library/*/Short">Pending</translate></option>
<option value="skipped"><translate translate-context="Content/Library/*">Skipped</translate></option>
<option value="errored"><translate translate-context="Content/Library/Dropdown">Failed</translate></option>
<option value="finished"><translate translate-context="Content/Library/*">Finished</translate></option>
<label>
<translate translate-context="Content/*/*/Noun">Import status</translate>
</label>
<select
class="ui dropdown"
@change="addSearchToken('status', $event.target.value)"
:value="getTokenValue('status', '')"
>
<option value>
<translate translate-context="Content/*/Dropdown">All</translate>
</option>
<option value="pending">
<translate translate-context="Content/Library/*/Short">Pending</translate>
</option>
<option value="skipped">
<translate translate-context="Content/Library/*">Skipped</translate>
</option>
<option value="errored">
<translate translate-context="Content/Library/Dropdown">Failed</translate>
</option>
<option value="finished">
<translate translate-context="Content/Library/*">Finished</translate>
</option>
</select>
</div>
<div class="field">
<label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
<label>
<translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate>
</label>
<select class="ui dropdown" v-model="ordering">
<option v-for="option in orderingOptions" :value="option[0]">
{{ sharedLabels.filters[option[1]] }}
</option>
<option
v-for="option in orderingOptions"
:value="option[0]"
>{{ sharedLabels.filters[option[1]] }}</option>
</select>
</div>
<div class="field">
<label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label>
<label>
<translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate>
</label>
<select class="ui dropdown" v-model="orderingDirection">
<option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
<option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
<option value="+">
<translate translate-context="Content/Search/Dropdown">Ascending</translate>
</option>
<option value="-">
<translate translate-context="Content/Search/Dropdown">Descending</translate>
</option>
</select>
</div>
</div>
......@@ -38,10 +71,18 @@
<import-status-modal :upload="detailedUpload" :show.sync="showUploadDetailModal" />
<div class="dimmable">
<div v-if="isLoading" class="ui active inverted dimmer">
<div class="ui loader"></div>
<div class="ui loader"></div>
</div>
<div v-else-if="!result && result.results.length === 0 && !needsRefresh" class="ui placeholder segment">
<div class="ui icon header">
<i class="upload icon"></i>
<translate
translate-context="Content/Home/Placeholder"
>No tracks have been added to this library yet</translate>
</div>
</div>
<action-table
v-if="result"
v-else
@action-launched="fetchData"
:id-field="'uuid'"
:objects-data="result"
......@@ -51,15 +92,30 @@
:needs-refresh="needsRefresh"
:action-url="'uploads/action/'"
@refresh="fetchData"
:filters="actionFilters">
:filters="actionFilters"
>
<template slot="header-cells">
<th><translate translate-context="*/*/*/Noun">Title</translate></th>
<th><translate translate-context="*/*/*/Noun">Artist</translate></th>
<th><translate translate-context="*/*/*">Album</translate></th>
<th><translate translate-context="*/*/*/Noun">Upload date</translate></th>
<th><translate translate-context="Content/*/*/Noun">Import status</translate></th>
<th><translate translate-context="Content/*/*">Duration</translate></th>
<th><translate translate-context="Content/*/*/Noun">Size</translate></th>
<th>
<translate translate-context="*/*/*/Noun">Title</translate>
</th>
<th>
<translate translate-context="*/*/*/Noun">Artist</translate>
</th>
<th>
<translate translate-context="*/*/*">Album</translate>
</th>
<th>
<translate translate-context="*/*/*/Noun">Upload date</translate>
</th>
<th>
<translate translate-context="Content/*/*/Noun">Import status</translate>
</th>
<th>
<translate translate-context="Content/*/*">Duration</translate>
</th>
<th>
<translate translate-context="Content/*/*/Noun">Size</translate>
</th>
</template>
<template slot="row-cells" slot-scope="scope">
<template v-if="scope.obj.track">
......@@ -67,10 +123,18 @@
<span :title="scope.obj.track.title">{{ scope.obj.track.title|truncate(25) }}</span>
</td>
<td>
<span class="discrete link" @click="addSearchToken('artist', scope.obj.track.artist.name)" :title="scope.obj.track.artist.name">{{ scope.obj.track.artist.name|truncate(20) }}</span>
<span
class="discrete link"
@click="addSearchToken('artist', scope.obj.track.artist.name)"
:title="scope.obj.track.artist.name"
>{{ scope.obj.track.artist.name|truncate(20) }}</span>
</td>
<td>
<span class="discrete link" @click="addSearchToken('album', scope.obj.track.album.title)" :title="scope.obj.track.album.title">{{ scope.obj.track.album.title|truncate(20) }}</span>
<span
class="discrete link"
@click="addSearchToken('album', scope.obj.track.album.title)"
:title="scope.obj.track.album.title"
>{{ scope.obj.track.album.title|truncate(20) }}</span>
</td>
</template>
<template v-else>
......@@ -82,22 +146,24 @@
<human-date :date="scope.obj.creation_date"></human-date>
</td>
<td>
<span class="discrete link" @click="addSearchToken('status', scope.obj.import_status)" :title="sharedLabels.fields.import_status.choices[scope.obj.import_status].help">
{{ sharedLabels.fields.import_status.choices[scope.obj.import_status].label }}
</span>
<button class="ui tiny basic icon button" :title="sharedLabels.fields.import_status.detailTitle" @click="detailedUpload = scope.obj; showUploadDetailModal = true">
<span
class="discrete link"
@click="addSearchToken('status', scope.obj.import_status)"
:title="sharedLabels.fields.import_status.choices[scope.obj.import_status].help"
>{{ sharedLabels.fields.import_status.choices[scope.obj.import_status].label }}</span>
<button
class="ui tiny basic icon button"
:title="sharedLabels.fields.import_status.detailTitle"
@click="detailedUpload = scope.obj; showUploadDetailModal = true"
>
<i class="question circle outline icon"></i>
</button>
</td>
<td v-if="scope.obj.duration">
{{ time.parse(scope.obj.duration) }}
</td>
<td v-if="scope.obj.duration">{{ time.parse(scope.obj.duration) }}</td>
<td v-else>
<translate translate-context="*/*/*">N/A</translate>
</td>
<td v-if="scope.obj.size">
{{ scope.obj.size | humanSize }}
</td>
<td v-if="scope.obj.size">{{ scope.obj.size | humanSize }}</td>
<td v-else>
<translate translate-context="*/*/*">N/A</translate>
</td>
......@@ -112,44 +178,50 @@
:current="page"
:paginate-by="paginateBy"
:total="result.count"
></pagination>
></pagination>
<span v-if="result && result.results.length > 0">
<translate translate-context="Content/*/Paragraph"
:translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
Showing results %{ start }-%{ end } on %{ total }
</translate>
<translate
translate-context="Content/*/Paragraph"
:translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"
>Showing results %{ start }-%{ end } on %{ total }</translate>
</span>
</div>
</div>
</template>
<script>
import axios from 'axios'
import _ from '@/lodash'
import time from '@/utils/time'
import {normalizeQuery, parseTokens} from '@/search'
import axios from "axios";
import _ from "@/lodash";
import time from "@/utils/time";
import { normalizeQuery, parseTokens } from "@/search";
import Pagination from '@/components/Pagination'
import ActionTable from '@/components/common/ActionTable'
import OrderingMixin from '@/components/mixins/Ordering'
import TranslationsMixin from '@/components/mixins/Translations'
import SmartSearchMixin from '@/components/mixins/SmartSearch'
import ImportStatusModal from '@/components/library/ImportStatusModal'
import Pagination from "@/components/Pagination";
import ActionTable from "@/components/common/ActionTable";
import OrderingMixin from "@/components/mixins/Ordering";
import TranslationsMixin from "@/components/mixins/Translations";
import SmartSearchMixin from "@/components/mixins/SmartSearch";
import ImportStatusModal from "@/components/library/ImportStatusModal";
export default {
mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin],
props: {
filters: {type: Object, required: false},
needsRefresh: {type: Boolean, required: false, default: false},
customObjects: {type: Array, required: false, default: () => { return [] }}
filters: { type: Object, required: false },
needsRefresh: { type: Boolean, required: false, default: false },
customObjects: {
type: Array,
required: false,
default: () => {
return [];
}
}
},
components: {
Pagination,
ActionTable,
ImportStatusModal
},
data () {
data() {
return {
time,
detailedUpload: null,
......@@ -162,100 +234,109 @@ export default {
query: this.defaultQuery,
tokens: parseTokens(normalizeQuery(this.defaultQuery))
},
orderingDirection: '-',
ordering: 'creation_date',
orderingDirection: "-",
ordering: "creation_date",
orderingOptions: [
['creation_date', 'creation_date'],
['title', 'track_title'],
['size', 'size'],
['duration', 'duration'],
['bitrate', 'bitrate'],
['album_title', 'album_title'],
['artist_name', 'artist_name']
["creation_date", "creation_date"],
["title", "track_title"],
["size", "size"],
["duration", "duration"],
["bitrate", "bitrate"],
["album_title", "album_title"],
["artist_name", "artist_name"]
]
}
};
},
created () {
this.fetchData()
created() {
this.fetchData();
},
methods: {
fetchData () {
this.$emit('fetch-start')
let params = _.merge({
'page': this.page,
'page_size': this.paginateBy,
'ordering': this.getOrderingAsString(),
'q': this.search.query
}, this.filters || {})
let self = this
self.isLoading = true
self.checked = []
axios.get('/uploads/', {params: params}).then((response) => {
self.result = response.data
self.isLoading = false
}, error => {
self.isLoading = false
self.errors = error.backendErrors
})
},
selectPage: function (page) {
this.page = page
fetchData() {
this.$emit(