diff --git a/api/config/urls.py b/api/config/urls.py index 8c490a5e6599e2f44bb60bb110a6a362f4153f93..de67ebb571de4b5f4e15cedd969d1adadeb42aee 100644 --- a/api/config/urls.py +++ b/api/config/urls.py @@ -31,3 +31,9 @@ if settings.DEBUG: url(r'^404/$', default_views.page_not_found), url(r'^500/$', default_views.server_error), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + + if 'debug_toolbar' in settings.INSTALLED_APPS: + import debug_toolbar + urlpatterns += [ + url(r'^__debug__/', include(debug_toolbar.urls)), + ] diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py index ba3fa453d77443eb75247b3987849db972963e1c..ff937a0f5c3aac8d5609b188d18b796e3b809e3a 100644 --- a/api/funkwhale_api/music/filters.py +++ b/api/funkwhale_api/music/filters.py @@ -8,5 +8,5 @@ class ArtistFilter(django_filters.FilterSet): class Meta: model = models.Artist fields = { - 'name': ['exact', 'iexact', 'startswith'] + 'name': ['exact', 'iexact', 'startswith', 'icontains'] } diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 43daf9d5a19404309b3ccf1f85e2e4c4713a9840..cf9d8749021ca8f5a1c03f297924de2ed1a583ca 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -13,14 +13,14 @@ class TagSerializer(serializers.ModelSerializer): class SimpleArtistSerializer(serializers.ModelSerializer): class Meta: model = models.Artist - fields = ('id', 'mbid', 'name') + fields = ('id', 'mbid', 'name', 'creation_date') class ArtistSerializer(serializers.ModelSerializer): tags = TagSerializer(many=True, read_only=True) class Meta: model = models.Artist - fields = ('id', 'mbid', 'name', 'tags') + fields = ('id', 'mbid', 'name', 'tags', 'creation_date') class TrackFileSerializer(serializers.ModelSerializer): diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index c32fa8f7ff49caa1ef9fbfff533e12a32835373e..5bfefc29b8c79f7521984f866f258930f84aa821 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -47,16 +47,15 @@ class TagViewSetMixin(object): class ArtistViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet): queryset = ( models.Artist.objects.all() - .order_by('name') .prefetch_related( 'albums__tracks__files', + 'albums__tracks__artist', 'albums__tracks__tags')) serializer_class = serializers.ArtistSerializerNested permission_classes = [ConditionalAuthentication] search_fields = ['name'] - ordering_fields = ('creation_date', 'name') filter_class = filters.ArtistFilter - + ordering_fields = ('id', 'name', 'creation_date') class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet): queryset = ( diff --git a/front/src/App.vue b/front/src/App.vue index f81d7d3daae22224f9d32450844a9677302c111e..c1c0874998b21af43407da9c87bb5ecb37ecd02b 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -1,7 +1,7 @@ <template> <div id="app"> <sidebar></sidebar> - <router-view></router-view> + <router-view :key="$route.fullPath"></router-view> <div class="ui divider"></div> <div id="footer" class="ui vertical footer segment"> <div class="ui container"> diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue index 2f0fb0a9236197d725191d4d1106037f855d3955..c3e9f1d14534e8486b5d72f156a83b7cbb7cb214 100644 --- a/front/src/components/library/Artists.vue +++ b/front/src/components/library/Artists.vue @@ -1,11 +1,40 @@ <template> <div> - <div v-if="isLoading" class="ui vertical segment"> - <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> - </div> - <div v-if="result" class="ui vertical stripe segment"> + <div class="ui vertical stripe segment"> <h2 class="ui header">Browsing artists</h2> - <div class="ui stackable three column grid"> + <div :class="['ui', {'loading': isLoading}, 'form']"> + <div class="fields"> + <div class="field"> + <label>Search</label> + <input type="text" v-model="query" placeholder="Enter an artist name..."/> + </div> + <div class="field"> + <label>Ordering</label> + <select class="ui dropdown" v-model="ordering"> + <option v-for="option in orderingOptions" :value="option[0]"> + {{ option[1] }} + </option> + </select> + </div> + <div class="field"> + <label>Ordering direction</label> + <select class="ui dropdown" v-model="orderingDirection"> + <option value="">Ascending</option> + <option value="-">Descending</option> + </select> + </div> + <div class="field"> + <label>Results per page</label> + <select class="ui dropdown" v-model="paginateBy"> + <option :value="parseInt(12)">12</option> + <option :value="parseInt(25)">25</option> + <option :value="parseInt(50)">50</option> + </select> + </div> + </div> + </div> + <div class="ui hidden divider"></div> + <div v-if="result" class="ui stackable three column grid"> <div v-if="result.results.length > 0" v-for="artist in result.results" @@ -28,6 +57,8 @@ </template> <script> +import _ from 'lodash' +import $ from 'jquery' import config from '@/config' import backend from '@/audio/backend' @@ -38,31 +69,72 @@ import Pagination from '@/components/Pagination' const FETCH_URL = config.API_URL + 'artists/' export default { + props: { + defaultOrdering: {type: String, required: false, default: '-creation_date'}, + defaultQuery: {type: String, required: false, default: ''}, + defaultPage: {required: false, default: 1}, + defaultPaginateBy: {required: false, default: 12} + }, components: { ArtistCard, Pagination }, data () { + let defaultOrdering = this.getOrderingFromString(this.defaultOrdering) return { isLoading: true, result: null, - page: 1, - orderBy: 'name', - paginateBy: 12 + page: parseInt(this.defaultPage), + query: this.defaultQuery, + paginateBy: parseInt(this.defaultPaginateBy), + orderingDirection: defaultOrdering.direction, + ordering: defaultOrdering.field, + orderingOptions: [ + ['creation_date', 'Creation date'], + ['name', 'Name'] + ] } }, created () { this.fetchData() }, + mounted () { + $('.ui.dropdown').dropdown() + }, methods: { - fetchData () { + getOrderingFromString (s) { + let parts = s.split('-') + if (parts.length > 1) { + return { + direction: '-', + field: parts.slice(1).join('-') + } + } else { + return { + direction: '', + field: s + } + } + }, + updateQueryString: function () { + this.$router.replace({ + query: { + query: this.query, + page: this.page, + paginateBy: this.paginateBy, + ordering: [this.orderingDirection, this.ordering].join('') + } + }) + }, + fetchData: _.debounce(function () { var self = this this.isLoading = true let url = FETCH_URL let params = { page: this.page, page_size: this.paginateBy, - order_by: 'name' + name__icontains: this.query, + ordering: [this.orderingDirection, this.ordering].join('') } logger.default.debug('Fetching artists') this.$http.get(url, {params: params}).then((response) => { @@ -76,13 +148,30 @@ export default { }) self.isLoading = false }) - }, + }, 500), selectPage: function (page) { this.page = page } }, watch: { page () { + this.updateQueryString() + this.fetchData() + }, + paginateBy () { + this.updateQueryString() + this.fetchData() + }, + ordering () { + this.updateQueryString() + this.fetchData() + }, + orderingDirection () { + this.updateQueryString() + this.fetchData() + }, + query () { + this.updateQueryString() this.fetchData() } } diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue index da9ac19b3ad7cef127384b446296d76e9a39a638..e8b053b6d0124175fda02254e71a85a420401e5e 100644 --- a/front/src/components/library/Library.vue +++ b/front/src/components/library/Library.vue @@ -8,7 +8,7 @@ <router-link v-if="auth.user.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">Import batches</router-link> </div> </div> - <router-view></router-view> + <router-view :key="$route.fullPath"></router-view> </div> </template> diff --git a/front/src/router/index.js b/front/src/router/index.js index d727276fc7b58f58d288bf4ae42282341cc65ce4..f6653e73d5976514384696fab41acd52b16ec7e3 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -54,7 +54,17 @@ export default new Router({ component: Library, children: [ { path: '', component: LibraryHome }, - { path: 'artists/', name: 'library.artists.browse', component: LibraryArtists }, + { + path: 'artists/', + name: 'library.artists.browse', + component: LibraryArtists, + props: (route) => ({ + defaultOrdering: route.query.ordering, + defaultQuery: route.query.query, + defaultPaginateBy: route.query.paginateBy, + defaultPage: route.query.page + }) + }, { path: 'artists/:id', name: 'library.artists.detail', component: LibraryArtist, props: true }, { path: 'albums/:id', name: 'library.albums.detail', component: LibraryAlbum, props: true }, { path: 'tracks/:id', name: 'library.tracks.detail', component: LibraryTrack, props: true },