diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 3f7cc7503385fae3cbf5b030386c510844ed74e0..6a2299bb4c619175082d1490ead7eeffd088fbb5 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -54,6 +54,7 @@ THIRD_PARTY_APPS = ( 'rest_auth.registration', 'mptt', 'dynamic_preferences', + 'django_filters', ) # Apps specific for this project go here. @@ -298,7 +299,7 @@ REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ), - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'DEFAULT_PAGINATION_CLASS': 'funkwhale_api.common.pagination.FunkwhalePagination', 'PAGE_SIZE': 25, 'DEFAULT_AUTHENTICATION_CLASSES': ( @@ -309,6 +310,7 @@ REST_FRAMEWORK = { ), 'DEFAULT_FILTER_BACKENDS': ( 'rest_framework.filters.OrderingFilter', + 'django_filters.rest_framework.DjangoFilterBackend', ) } diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py index 0168bc25af3889c3fd06ef2ae9ba55e813da09a9..d961973d34db055563899a8dc956514abec7f967 100644 --- a/api/funkwhale_api/__init__.py +++ b/api/funkwhale_api/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = '0.2.2' +__version__ = '0.2.3' __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) diff --git a/api/funkwhale_api/common/pagination.py b/api/funkwhale_api/common/pagination.py new file mode 100644 index 0000000000000000000000000000000000000000..224c470dc1e8abb6de2d279eb85b174e1a2c76ef --- /dev/null +++ b/api/funkwhale_api/common/pagination.py @@ -0,0 +1,6 @@ +from rest_framework.pagination import PageNumberPagination + + +class FunkwhalePagination(PageNumberPagination): + page_size_query_param = 'page_size' + max_page_size = 25 diff --git a/api/funkwhale_api/music/fake_data.py b/api/funkwhale_api/music/fake_data.py new file mode 100644 index 0000000000000000000000000000000000000000..ef5eaf4be811f580d82378c17ab838694df52ea7 --- /dev/null +++ b/api/funkwhale_api/music/fake_data.py @@ -0,0 +1,22 @@ +""" +Populates the database with fake data +""" +import random + +from funkwhale_api.music import models +from funkwhale_api.music.tests import factories + + +def create_data(count=25): + artists = factories.ArtistFactory.create_batch(size=count) + for artist in artists: + print('Creating data for', artist) + albums = factories.AlbumFactory.create_batch( + artist=artist, size=random.randint(1, 5)) + for album in albums: + factories.TrackFileFactory.create_batch( + track__album=album, size=random.randint(3, 18)) + + +if __name__ == '__main__': + main() diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..ba3fa453d77443eb75247b3987849db972963e1c --- /dev/null +++ b/api/funkwhale_api/music/filters.py @@ -0,0 +1,12 @@ +import django_filters + +from . import models + + +class ArtistFilter(django_filters.FilterSet): + + class Meta: + model = models.Artist + fields = { + 'name': ['exact', 'iexact', 'startswith'] + } diff --git a/api/funkwhale_api/music/tests/factories.py b/api/funkwhale_api/music/tests/factories.py index d4ec048101984419b5b1459943be5998f43f261a..ea680b3bd5d4da58e63f5ee4c861fec7972117f4 100644 --- a/api/funkwhale_api/music/tests/factories.py +++ b/api/funkwhale_api/music/tests/factories.py @@ -5,7 +5,7 @@ SAMPLES_PATH = os.path.dirname(os.path.abspath(__file__)) class ArtistFactory(factory.django.DjangoModelFactory): - name = factory.Sequence(lambda n: 'artist-{0}'.format(n)) + name = factory.Faker('name') mbid = factory.Faker('uuid4') class Meta: @@ -13,7 +13,7 @@ class ArtistFactory(factory.django.DjangoModelFactory): class AlbumFactory(factory.django.DjangoModelFactory): - title = factory.Sequence(lambda n: 'album-{0}'.format(n)) + title = factory.Faker('sentence', nb_words=3) mbid = factory.Faker('uuid4') release_date = factory.Faker('date') cover = factory.django.ImageField() @@ -24,7 +24,7 @@ class AlbumFactory(factory.django.DjangoModelFactory): class TrackFactory(factory.django.DjangoModelFactory): - title = factory.Sequence(lambda n: 'track-{0}'.format(n)) + title = factory.Faker('sentence', nb_words=3) mbid = factory.Faker('uuid4') album = factory.SubFactory(AlbumFactory) artist = factory.SelfAttribute('album.artist') diff --git a/api/funkwhale_api/music/tests/test_api.py b/api/funkwhale_api/music/tests/test_api.py index 21a567084e25a4d3ade7a27fe878d227839f4bbf..b7c25424f322605bc47662743d921e0419ed3d3e 100644 --- a/api/funkwhale_api/music/tests/test_api.py +++ b/api/funkwhale_api/music/tests/test_api.py @@ -182,6 +182,21 @@ class TestAPI(TMPDirTestCaseMixin, TestCase): self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8'))) + def test_can_search_artist_by_name_start(self): + artist1 = factories.ArtistFactory(name='alpha') + artist2 = factories.ArtistFactory(name='beta') + results = { + 'next': None, + 'previous': None, + 'count': 1, + 'results': [serializers.ArtistSerializerNested(artist1).data] + } + expected = json.dumps(results) + url = self.reverse('api:v1:artists-list') + response = self.client.get(url, {'name__startswith': 'a'}) + + self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8'))) + def test_can_search_tracks(self): artist1 = models.Artist.objects.create(name='Test1') artist2 = models.Artist.objects.create(name='Test2') diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 98319255270bfdae84f70e3335fc4917f650885b..d149b5d1b9b937955d89debc33ecf2689d64c9e5 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -21,8 +21,10 @@ from taggit.models import Tag from . import models from . import serializers from . import importers +from . import filters from . import utils + class SearchMixin(object): search_fields = [] @@ -52,7 +54,8 @@ class ArtistViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet): serializer_class = serializers.ArtistSerializerNested permission_classes = [ConditionalAuthentication] search_fields = ['name'] - ordering_fields = ('creation_date',) + ordering_fields = ('creation_date', 'name') + filter_class = filters.ArtistFilter class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet): diff --git a/api/requirements/base.txt b/api/requirements/base.txt index e7bc870cfd85efeff22033e71f583536592b7ab9..3a11afadf509cfa04aae62a9ca566d23f28f2646 100644 --- a/api/requirements/base.txt +++ b/api/requirements/base.txt @@ -45,6 +45,7 @@ django-taggit==0.22.1 persisting-theory==0.2.1 django-versatileimagefield==1.7.1 django-cachalot==1.5.0 +django-filter==1.1 django-rest-auth==0.9.1 beautifulsoup4==4.6.0 Markdown==2.6.8 diff --git a/front/package.json b/front/package.json index 55e773fba7b52c6d61655d4e1e80c71349203457..bad90430f4144be99d4b17ac11e82073a6705ff0 100644 --- a/front/package.json +++ b/front/package.json @@ -16,9 +16,10 @@ "dependencies": { "dateformat": "^2.0.0", "js-logger": "^1.3.0", + "lodash": "^4.17.4", "semantic-ui-css": "^2.2.10", "vue": "^2.3.3", - "vue-global-events": "^1.0.2", + "vue-lazyload": "^1.1.4", "vue-resource": "^1.3.4", "vue-router": "^2.3.1", "vuedraggable": "^2.14.1" diff --git a/front/src/components/Pagination.vue b/front/src/components/Pagination.vue new file mode 100644 index 0000000000000000000000000000000000000000..3ac7c59af5817413c13a97e569065ca48cb95b91 --- /dev/null +++ b/front/src/components/Pagination.vue @@ -0,0 +1,52 @@ +<template> + <div class="ui pagination borderless menu"> + <a + @click="selectPage(1)" + :class="[{'disabled': current === 1}, 'item']"><i class="angle double left icon"></i></a> + <a + @click="selectPage(current - 1)" + :class="[{'disabled': current - 1 < 1}, 'item']"><i class="angle left icon"></i></a> + <a + v-for="page in pages" + @click="selectPage(page)" + :class="[{'active': page === current}, 'item']"> + {{ page }} + </a> + <a + @click="selectPage(current + 1)" + :class="[{'disabled': current + 1 > maxPage}, 'item']"><i class="angle right icon"></i></a> + <a + @click="selectPage(maxPage)" + :class="[{'disabled': current === maxPage}, 'item']"><i class="angle double right icon"></i></a> + </div> +</template> + +<script> +import _ from 'lodash' + +export default { + props: { + current: {type: Number, default: 1}, + paginateBy: {type: Number, default: 25}, + total: {type: Number} + }, + computed: { + pages: function () { + return _.range(1, this.maxPage + 1) + }, + maxPage: function () { + return Math.ceil(this.total / this.paginateBy) + } + }, + methods: { + selectPage: function (page) { + if (this.current !== page) { + this.$emit('page-changed', page) + } + } + } +} +</script> + +<style scoped> +</style> diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index 0ce16a75f94a6b597a2ea0e3d120c336533232c7..68927a37b09a5e302a9dfb7ce9be44b9e7750b96 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -94,7 +94,7 @@ </template> <script> -import GlobalEvents from 'vue-global-events' +import GlobalEvents from '@/components/utils/global-events' import Player from '@/components/audio/Player' import favoriteTracks from '@/favorites/tracks' diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index 92ea9d0a48297ac1be4f65a894c584fd801e6282..423c9d12f2d5cf89abf5b1f876a595925ee21370 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -7,14 +7,14 @@ <img v-else src="../../assets/audio/default-cover.png"> </div> <div class="middle aligned content"> - <router-link class="small header discrete link track" :to="{name: 'library.track', params: {id: queue.currentTrack.id }}"> + <router-link class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: queue.currentTrack.id }}"> {{ queue.currentTrack.title }} </router-link> <div class="meta"> - <router-link class="artist" :to="{name: 'library.artist', params: {id: queue.currentTrack.artist.id }}"> + <router-link class="artist" :to="{name: 'library.artists.detail', params: {id: queue.currentTrack.artist.id }}"> {{ queue.currentTrack.artist.name }} </router-link> / - <router-link class="album" :to="{name: 'library.album', params: {id: queue.currentTrack.album.id }}"> + <router-link class="album" :to="{name: 'library.albums.detail', params: {id: queue.currentTrack.album.id }}"> {{ queue.currentTrack.album.title }} </router-link> </div> @@ -68,7 +68,7 @@ </template> <script> -import GlobalEvents from 'vue-global-events' +import GlobalEvents from '@/components/utils/global-events' import queue from '@/audio/queue' import Track from '@/audio/track' diff --git a/front/src/components/audio/SearchBar.vue b/front/src/components/audio/SearchBar.vue index 6f4629296d4eecfc16b1ba802669bafd01135378..2324c88392f258f95b7915cce04ea1b9166d5e80 100644 --- a/front/src/components/audio/SearchBar.vue +++ b/front/src/components/audio/SearchBar.vue @@ -35,7 +35,7 @@ export default { let categories = [ { code: 'artists', - route: 'library.artist', + route: 'library.artists.detail', name: 'Artist', getTitle (r) { return r.name @@ -46,7 +46,7 @@ export default { }, { code: 'albums', - route: 'library.album', + route: 'library.albums.detail', name: 'Album', getTitle (r) { return r.title @@ -57,7 +57,7 @@ export default { }, { code: 'tracks', - route: 'library.track', + route: 'library.tracks.detail', name: 'Track', getTitle (r) { return r.title diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue index 7fd60d963b512ae637faa7c7dc8acbb2f9021d80..ce5e832e2794a4288a673460244434f30945615f 100644 --- a/front/src/components/audio/album/Card.vue +++ b/front/src/components/audio/album/Card.vue @@ -2,14 +2,14 @@ <div class="ui card"> <div class="content"> <div class="right floated tiny ui image"> - <img v-if="album.cover" :src="backend.absoluteUrl(album.cover)"> + <img v-if="album.cover" v-lazy="backend.absoluteUrl(album.cover)"> <img v-else src="../../../assets/audio/default-cover.png"> </div> <div class="header"> - <router-link class="discrete link" :to="{name: 'library.album', params: {id: album.id }}">{{ album.title }}</router-link> + <router-link class="discrete link" :to="{name: 'library.albums.detail', params: {id: album.id }}">{{ album.title }}</router-link> </div> <div class="meta"> - By <router-link :to="{name: 'library.artist', params: {id: album.artist.id }}"> + By <router-link :to="{name: 'library.artists.detail', params: {id: album.artist.id }}"> {{ album.artist.name }} </router-link> </div> @@ -21,7 +21,7 @@ <play-button class="basic icon" :track="track" :discrete="true"></play-button> </td> <td colspan="6"> - <router-link class="track discrete link" :to="{name: 'library.track', params: {id: track.id }}"> + <router-link class="track discrete link" :to="{name: 'library.tracks.detail', params: {id: track.id }}"> <template v-if="track.position"> {{ track.position }}. </template> diff --git a/front/src/components/audio/artist/Card.vue b/front/src/components/audio/artist/Card.vue index a9701c07ebadda7e7003e01c05fd09a9e19c9743..8a02163fbb17258badb78fe6f4d133e5096f6749 100644 --- a/front/src/components/audio/artist/Card.vue +++ b/front/src/components/audio/artist/Card.vue @@ -2,7 +2,7 @@ <div class="ui card"> <div class="content"> <div class="header"> - <router-link class="discrete link" :to="{name: 'library.artist', params: {id: artist.id }}"> + <router-link class="discrete link" :to="{name: 'library.artists.detail', params: {id: artist.id }}"> {{ artist.name }} </router-link> </div> @@ -15,7 +15,7 @@ <img class="ui mini image" v-else src="../../../assets/audio/default-cover.png"> </td> <td colspan="4"> - <router-link class="discrete link":to="{name: 'library.album', params: {id: album.id }}"> + <router-link class="discrete link":to="{name: 'library.albums.detail', params: {id: album.id }}"> <strong>{{ album.title }}</strong> </router-link><br /> {{ album.tracks.length }} tracks diff --git a/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue index e9beaa05a5ac8f0bb6285597a9316eb8afab4e4f..055c986d2fdf416d01215fa328ba59bea3a34c07 100644 --- a/front/src/components/audio/track/Table.vue +++ b/front/src/components/audio/track/Table.vue @@ -16,11 +16,11 @@ <play-button class="basic icon" :discrete="true" :track="track"></play-button> </td> <td> - <img class="ui mini image" v-if="track.album.cover" :src="backend.absoluteUrl(track.album.cover)"> + <img class="ui mini image" v-if="track.album.cover" v-lazy="backend.absoluteUrl(track.album.cover)"> <img class="ui mini image" v-else src="../../..//assets/audio/default-cover.png"> </td> <td colspan="6"> - <router-link class="track" :to="{name: 'library.track', params: {id: track.id }}"> + <router-link class="track" :to="{name: 'library.tracks.detail', params: {id: track.id }}"> <template v-if="displayPosition && track.position"> {{ track.position }}. </template> @@ -28,12 +28,12 @@ </router-link> </td> <td colspan="6"> - <router-link class="artist discrete link" :to="{name: 'library.artist', params: {id: track.artist.id }}"> + <router-link class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}"> {{ track.artist.name }} </router-link> </td> <td colspan="6"> - <router-link class="album discrete link" :to="{name: 'library.album', params: {id: track.album.id }}"> + <router-link class="album discrete link" :to="{name: 'library.albums.detail', params: {id: track.album.id }}"> {{ track.album.title }} </router-link> </td> diff --git a/front/src/components/favorites/List.vue b/front/src/components/favorites/List.vue index f7972a07c9243863e025af3f2103e0af6e47c891..63c3ba79d18c7360cdef97f8739614cfb1f1f163 100644 --- a/front/src/components/favorites/List.vue +++ b/front/src/components/favorites/List.vue @@ -12,11 +12,16 @@ </div> <div class="ui vertical stripe segment"> - <button class="ui left floated labeled icon button" @click="fetchFavorites(previousLink)" :disabled="!previousLink"><i class="left arrow icon"></i> Previous</button> - <button class="ui right floated right labeled icon button" @click="fetchFavorites(nextLink)" :disabled="!nextLink">Next <i class="right arrow icon"></i></button> - <div class="ui hidden clearing divider"></div> - <div class="ui hidden clearing divider"></div> <track-table v-if="results" :tracks="results.results"></track-table> + <div class="ui center aligned basic segment"> + <pagination + v-if="results && results.count > 0" + @page-changed="selectPage" + :current="page" + :paginate-by="paginateBy" + :total="results.count" + ></pagination> + </div> </div> </div> </template> @@ -28,13 +33,15 @@ import config from '@/config' import favoriteTracks from '@/favorites/tracks' import TrackTable from '@/components/audio/track/Table' import RadioButton from '@/components/radios/Button' +import Pagination from '@/components/Pagination' const FAVORITES_URL = config.API_URL + 'tracks/' export default { components: { TrackTable, - RadioButton + RadioButton, + Pagination }, data () { return { @@ -42,6 +49,8 @@ export default { isLoading: false, nextLink: null, previousLink: null, + page: 1, + paginateBy: 25, favoriteTracks } }, @@ -53,7 +62,9 @@ export default { var self = this this.isLoading = true let params = { - favorites: 'true' + favorites: 'true', + page: this.page, + page_size: this.paginateBy } logger.default.time('Loading user favorites') this.$http.get(url, {params: params}).then((response) => { @@ -68,6 +79,14 @@ export default { logger.default.timeEnd('Loading user favorites') self.isLoading = false }) + }, + selectPage: function (page) { + this.page = page + } + }, + watch: { + page: function () { + this.fetchFavorites(FAVORITES_URL) } } } diff --git a/front/src/components/library/Album.vue b/front/src/components/library/Album.vue index cf3403400b8a07afc56cd0ca0edd7874ca8bacf6..dcfea560095064e20384dd1e77dc37707c7f42e3 100644 --- a/front/src/components/library/Album.vue +++ b/front/src/components/library/Album.vue @@ -12,7 +12,7 @@ {{ album.title }} <div class="sub header"> Album containing {{ album.tracks.length }} tracks, - by <router-link :to="{name: 'library.artist', params: {id: album.artist.id }}"> + by <router-link :to="{name: 'library.artists.detail', params: {id: album.artist.id }}"> {{ album.artist.name }} </router-link> </div> diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue new file mode 100644 index 0000000000000000000000000000000000000000..07727a0595efcc550f61ec0738b9d7070d0b146c --- /dev/null +++ b/front/src/components/library/Artists.vue @@ -0,0 +1,86 @@ +<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"> + <h2 class="ui header">Browsing artists</h2> + <div class="ui stackable three column grid"> + <div + v-if="result.results.length > 0" + v-for="artist in result.results" + :key="artist.id" + class="column"> + <artist-card class="fluid" :artist="artist"></artist-card> + </div> + </div> + <div class="ui center aligned basic segment"> + <pagination + v-if="result && result.results.length > 0" + @page-changed="selectPage" + :current="page" + :paginate-by="paginateBy" + :total="result.count" + ></pagination> + </div> + </div> + </div> +</template> + +<script> + +import config from '@/config' +import logger from '@/logging' +import ArtistCard from '@/components/audio/artist/Card' +import Pagination from '@/components/Pagination' + +const FETCH_URL = config.API_URL + 'artists/' + +export default { + components: { + ArtistCard, + Pagination + }, + data () { + return { + isLoading: true, + result: null, + page: 1, + orderBy: 'name', + paginateBy: 12 + } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + var self = this + this.isLoading = true + let url = FETCH_URL + let params = { + page: this.page, + page_size: this.paginateBy, + order_by: 'name' + } + logger.default.debug('Fetching artists') + this.$http.get(url, {params: params}).then((response) => { + self.result = response.data + self.isLoading = false + }) + }, + selectPage: function (page) { + this.page = page + } + }, + watch: { + page () { + this.fetchData() + } + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> +</style> diff --git a/front/src/components/library/Home.vue b/front/src/components/library/Home.vue index 651f7cb63bb90a0c66b1b9b97bf983d469c40f58..624da62c567e75fee65577726b5aaa0d5b2f32bd 100644 --- a/front/src/components/library/Home.vue +++ b/front/src/components/library/Home.vue @@ -8,7 +8,7 @@ <div class="column"> <h2 class="ui header">Latest artists</h2> <div :class="['ui', {'active': isLoadingArtists}, 'inline', 'loader']"></div> - <div v-if="artists.length > 0" v-for="artist in artists.slice(0, 3)" :key="artist" class="ui cards"> + <div v-if="artists.length > 0" v-for="artist in artists.slice(0, 3)" :key="artist.id" class="ui cards"> <artist-card :artist="artist"></artist-card> </div> </div> diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue index 56b750a4afcfe2870a5d7a7961c259f9da62105a..da9ac19b3ad7cef127384b446296d76e9a39a638 100644 --- a/front/src/components/library/Library.vue +++ b/front/src/components/library/Library.vue @@ -2,8 +2,11 @@ <div class="main library pusher"> <div class="ui secondary pointing menu"> <router-link class="ui item" to="/library" exact>Browse</router-link> - <router-link v-if="auth.user.availablePermissions['import.launch']" class="ui item" to="/library/import/launch" exact>Import</router-link> - <router-link v-if="auth.user.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">Import batches</router-link> + <router-link class="ui item" to="/library/artists" exact>Artists</router-link> + <div class="ui secondary right menu"> + <router-link v-if="auth.user.availablePermissions['import.launch']" class="ui item" to="/library/import/launch" exact>Import</router-link> + <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> </div> diff --git a/front/src/components/library/Track.vue b/front/src/components/library/Track.vue index 3c627c13c257d6c71d2ac03f80ff9f573c5bff58..36a76e822c2ac0218d8c23a1fb2cca8f21db4f29 100644 --- a/front/src/components/library/Track.vue +++ b/front/src/components/library/Track.vue @@ -12,10 +12,10 @@ {{ track.title }} <div class="sub header"> From album - <router-link :to="{name: 'library.album', params: {id: track.album.id }}"> + <router-link :to="{name: 'library.albums.detail', params: {id: track.album.id }}"> {{ track.album.title }} </router-link> - by <router-link :to="{name: 'library.artist', params: {id: track.artist.id }}"> + by <router-link :to="{name: 'library.artists.detail', params: {id: track.artist.id }}"> {{ track.artist.name }} </router-link> </div> diff --git a/front/src/components/utils/global-events.vue b/front/src/components/utils/global-events.vue new file mode 100644 index 0000000000000000000000000000000000000000..2905e3a7d337dddc3b66ecc528e0d4f9c607dd9f --- /dev/null +++ b/front/src/components/utils/global-events.vue @@ -0,0 +1,52 @@ +<script> +import $ from 'jquery' + +const modifiersRE = /^[~!&]*/ +const nonEventNameCharsRE = /\W+/ +const names = { + '!': 'capture', + '~': 'once', + '&': 'passive' +} + +function extractEventOptions (eventDescriptor) { + const [modifiers] = eventDescriptor.match(modifiersRE) + return modifiers.split('').reduce((options, modifier) => { + options[names[modifier]] = true + return options + }, {}) +} + +export default { + render: h => h(), + + mounted () { + this._listeners = Object.create(null) + Object.keys(this.$listeners).forEach(event => { + const handler = this.$listeners[event] + let wrapper = function (event) { + // we check here the event is not triggered from an input + // to avoid collisions + if (!$(event.target).is(':input, [contenteditable]')) { + handler(event) + } + } + document.addEventListener( + event.replace(nonEventNameCharsRE, ''), + wrapper, + extractEventOptions(event) + ) + this._listeners[event] = handler + }) + }, + + beforeDestroy () { + for (const event in this._listeners) { + document.removeEventListener( + event.replace(nonEventNameCharsRE, ''), + this._listeners[event] + ) + } + } +} +</script> diff --git a/front/src/main.js b/front/src/main.js index 4ffbdb2f7b787a0dacb6c53eba914f190a0640fa..a214c3881e1535178a46a6b1101e2fac17685706 100644 --- a/front/src/main.js +++ b/front/src/main.js @@ -10,6 +10,7 @@ import App from './App' import router from './router' import VueResource from 'vue-resource' import auth from './auth' +import VueLazyload from 'vue-lazyload' window.$ = window.jQuery = require('jquery') @@ -19,6 +20,7 @@ window.$ = window.jQuery = require('jquery') require('semantic-ui-css/semantic.js') Vue.use(VueResource) +Vue.use(VueLazyload) Vue.config.productionTip = false Vue.http.interceptors.push(function (request, next) { diff --git a/front/src/router/index.js b/front/src/router/index.js index b3d90731f41cf11f614612b1a3cc954625fd975a..e546172b5f058e0c2ae880be68c82296fc0f41a3 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -7,6 +7,7 @@ import Logout from '@/components/auth/Logout' import Library from '@/components/library/Library' import LibraryHome from '@/components/library/Home' import LibraryArtist from '@/components/library/Artist' +import LibraryArtists from '@/components/library/Artists' import LibraryAlbum from '@/components/library/Album' import LibraryTrack from '@/components/library/Track' import LibraryImport from '@/components/library/import/Main' @@ -51,9 +52,10 @@ export default new Router({ component: Library, children: [ { path: '', component: LibraryHome }, - { path: 'artist/:id', name: 'library.artist', component: LibraryArtist, props: true }, - { path: 'album/:id', name: 'library.album', component: LibraryAlbum, props: true }, - { path: 'track/:id', name: 'library.track', component: LibraryTrack, props: true }, + { path: 'artists/', name: 'library.artists.browse', component: LibraryArtists }, + { 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 }, { path: 'import/launch', name: 'library.import.launch',