Use cropped covers/avatars to reduce bandwidth use
This commit is contained in:
parent
63df2e29cb
commit
979c554b4a
|
@ -467,3 +467,13 @@ MUSIC_DIRECTORY_SERVE_PATH = env(
|
||||||
USERS_INVITATION_EXPIRATION_DAYS = env.int(
|
USERS_INVITATION_EXPIRATION_DAYS = env.int(
|
||||||
"USERS_INVITATION_EXPIRATION_DAYS", default=14
|
"USERS_INVITATION_EXPIRATION_DAYS", default=14
|
||||||
)
|
)
|
||||||
|
|
||||||
|
VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = {
|
||||||
|
"square": [
|
||||||
|
("original", "url"),
|
||||||
|
("square_crop", "crop__400x400"),
|
||||||
|
("medium_square_crop", "crop__200x200"),
|
||||||
|
("small_square_crop", "crop__50x50"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
VERSATILEIMAGEFIELD_SETTINGS = {"create_images_on_demand": False}
|
||||||
|
|
|
@ -19,7 +19,7 @@ class Command(BaseCommand):
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
name = options["script_name"]
|
name = options["script_name"]
|
||||||
if not name:
|
if not name:
|
||||||
self.show_help()
|
return self.show_help()
|
||||||
|
|
||||||
available_scripts = self.get_scripts()
|
available_scripts = self.get_scripts()
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
from . import create_image_variations
|
||||||
|
from . import django_permissions_to_user_permissions
|
||||||
|
from . import test
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["create_image_variations", "django_permissions_to_user_permissions", "test"]
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""
|
||||||
|
Compute different sizes of image used for Album covers and User avatars
|
||||||
|
"""
|
||||||
|
|
||||||
|
from versatileimagefield.image_warmer import VersatileImageFieldWarmer
|
||||||
|
|
||||||
|
from funkwhale_api.music.models import Album
|
||||||
|
from funkwhale_api.users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
MODELS = [(Album, "cover", "square"), (User, "avatar", "square")]
|
||||||
|
|
||||||
|
|
||||||
|
def main(command, **kwargs):
|
||||||
|
for model, attribute, key_set in MODELS:
|
||||||
|
qs = model.objects.exclude(**{"{}__isnull".format(attribute): True})
|
||||||
|
qs = qs.exclude(**{attribute: ""})
|
||||||
|
warmer = VersatileImageFieldWarmer(
|
||||||
|
instance_or_queryset=qs,
|
||||||
|
rendition_key_set=key_set,
|
||||||
|
image_attr=attribute,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
|
command.stdout.write(
|
||||||
|
"Creating images for {} / {}".format(model.__name__, attribute)
|
||||||
|
)
|
||||||
|
num_created, failed_to_create = warmer.warm()
|
||||||
|
command.stdout.write(
|
||||||
|
" {} created, {} in error".format(num_created, len(failed_to_create))
|
||||||
|
)
|
|
@ -18,7 +18,11 @@ class TrackFavoriteViewSet(
|
||||||
):
|
):
|
||||||
|
|
||||||
serializer_class = serializers.UserTrackFavoriteSerializer
|
serializer_class = serializers.UserTrackFavoriteSerializer
|
||||||
queryset = models.TrackFavorite.objects.all()
|
queryset = (
|
||||||
|
models.TrackFavorite.objects.all()
|
||||||
|
.select_related("track__artist", "track__album__artist", "user")
|
||||||
|
.prefetch_related("track__files")
|
||||||
|
)
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
permissions.ConditionalAuthentication,
|
permissions.ConditionalAuthentication,
|
||||||
permissions.OwnerPermission,
|
permissions.OwnerPermission,
|
||||||
|
|
|
@ -15,7 +15,9 @@ from django.dispatch import receiver
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from versatileimagefield.fields import VersatileImageField
|
from versatileimagefield.fields import VersatileImageField
|
||||||
|
from versatileimagefield.image_warmer import VersatileImageFieldWarmer
|
||||||
|
|
||||||
from funkwhale_api import downloader, musicbrainz
|
from funkwhale_api import downloader, musicbrainz
|
||||||
from funkwhale_api.federation import utils as federation_utils
|
from funkwhale_api.federation import utils as federation_utils
|
||||||
|
@ -641,3 +643,13 @@ def update_request_status(sender, instance, created, **kwargs):
|
||||||
# let's mark the request as imported since the import is over
|
# let's mark the request as imported since the import is over
|
||||||
instance.import_request.status = "imported"
|
instance.import_request.status = "imported"
|
||||||
return instance.import_request.save(update_fields=["status"])
|
return instance.import_request.save(update_fields=["status"])
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.post_save, sender=Album)
|
||||||
|
def warm_album_covers(sender, instance, **kwargs):
|
||||||
|
if not instance.cover:
|
||||||
|
return
|
||||||
|
album_covers_warmer = VersatileImageFieldWarmer(
|
||||||
|
instance_or_queryset=instance, rendition_key_set="square", image_attr="cover"
|
||||||
|
)
|
||||||
|
num_created, failed_to_create = album_covers_warmer.warm()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
|
from versatileimagefield.serializers import VersatileImageFieldSerializer
|
||||||
|
|
||||||
from funkwhale_api.activity import serializers as activity_serializers
|
from funkwhale_api.activity import serializers as activity_serializers
|
||||||
from funkwhale_api.users.serializers import UserBasicSerializer
|
from funkwhale_api.users.serializers import UserBasicSerializer
|
||||||
|
@ -8,8 +9,12 @@ from funkwhale_api.users.serializers import UserBasicSerializer
|
||||||
from . import models, tasks
|
from . import models, tasks
|
||||||
|
|
||||||
|
|
||||||
|
cover_field = VersatileImageFieldSerializer(allow_null=True, sizes="square")
|
||||||
|
|
||||||
|
|
||||||
class ArtistAlbumSerializer(serializers.ModelSerializer):
|
class ArtistAlbumSerializer(serializers.ModelSerializer):
|
||||||
tracks_count = serializers.SerializerMethodField()
|
tracks_count = serializers.SerializerMethodField()
|
||||||
|
cover = cover_field
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Album
|
model = models.Album
|
||||||
|
@ -87,6 +92,7 @@ class AlbumTrackSerializer(serializers.ModelSerializer):
|
||||||
class AlbumSerializer(serializers.ModelSerializer):
|
class AlbumSerializer(serializers.ModelSerializer):
|
||||||
tracks = serializers.SerializerMethodField()
|
tracks = serializers.SerializerMethodField()
|
||||||
artist = ArtistSimpleSerializer(read_only=True)
|
artist = ArtistSimpleSerializer(read_only=True)
|
||||||
|
cover = cover_field
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Album
|
model = models.Album
|
||||||
|
@ -111,6 +117,7 @@ class AlbumSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class TrackAlbumSerializer(serializers.ModelSerializer):
|
class TrackAlbumSerializer(serializers.ModelSerializer):
|
||||||
artist = ArtistSimpleSerializer(read_only=True)
|
artist = ArtistSimpleSerializer(read_only=True)
|
||||||
|
cover = cover_field
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Album
|
model = models.Album
|
||||||
|
@ -156,6 +163,8 @@ class TagSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class SimpleAlbumSerializer(serializers.ModelSerializer):
|
class SimpleAlbumSerializer(serializers.ModelSerializer):
|
||||||
|
cover = cover_field
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Album
|
model = models.Album
|
||||||
fields = ("id", "mbid", "title", "release_date", "cover")
|
fields = ("id", "mbid", "title", "release_date", "cover")
|
||||||
|
|
|
@ -107,7 +107,7 @@ class PlaylistSerializer(serializers.ModelSerializer):
|
||||||
covers = []
|
covers = []
|
||||||
max_covers = 5
|
max_covers = 5
|
||||||
for plt in plts:
|
for plt in plts:
|
||||||
url = plt.track.album.cover.url
|
url = plt.track.album.cover.crop["200x200"].url
|
||||||
if url in covers:
|
if url in covers:
|
||||||
continue
|
continue
|
||||||
covers.append(url)
|
covers.append(url)
|
||||||
|
|
|
@ -11,12 +11,14 @@ import uuid
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.dispatch import receiver
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from versatileimagefield.fields import VersatileImageField
|
from versatileimagefield.fields import VersatileImageField
|
||||||
|
from versatileimagefield.image_warmer import VersatileImageFieldWarmer
|
||||||
|
|
||||||
from funkwhale_api.common import fields, preferences
|
from funkwhale_api.common import fields, preferences
|
||||||
from funkwhale_api.common import utils as common_utils
|
from funkwhale_api.common import utils as common_utils
|
||||||
|
@ -205,3 +207,13 @@ class Invitation(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().save(**kwargs)
|
return super().save(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.post_save, sender=User)
|
||||||
|
def warm_user_avatar(sender, instance, **kwargs):
|
||||||
|
if not instance.avatar:
|
||||||
|
return
|
||||||
|
user_avatar_warmer = VersatileImageFieldWarmer(
|
||||||
|
instance_or_queryset=instance, rendition_key_set="square", image_attr="avatar"
|
||||||
|
)
|
||||||
|
num_created, failed_to_create = user_avatar_warmer.warm()
|
||||||
|
|
|
@ -45,15 +45,7 @@ class UserActivitySerializer(activity_serializers.ModelSerializer):
|
||||||
return "Person"
|
return "Person"
|
||||||
|
|
||||||
|
|
||||||
avatar_field = VersatileImageFieldSerializer(
|
avatar_field = VersatileImageFieldSerializer(allow_null=True, sizes="square")
|
||||||
allow_null=True,
|
|
||||||
sizes=[
|
|
||||||
("original", "url"),
|
|
||||||
("square_crop", "crop__400x400"),
|
|
||||||
("medium_square_crop", "crop__200x200"),
|
|
||||||
("small_square_crop", "crop__50x50"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserBasicSerializer(serializers.ModelSerializer):
|
class UserBasicSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
@ -12,7 +12,12 @@ def test_artist_album_serializer(factories, to_api_date):
|
||||||
"artist": album.artist.id,
|
"artist": album.artist.id,
|
||||||
"creation_date": to_api_date(album.creation_date),
|
"creation_date": to_api_date(album.creation_date),
|
||||||
"tracks_count": 1,
|
"tracks_count": 1,
|
||||||
"cover": album.cover.url,
|
"cover": {
|
||||||
|
"original": album.cover.url,
|
||||||
|
"square_crop": album.cover.crop["400x400"].url,
|
||||||
|
"medium_square_crop": album.cover.crop["200x200"].url,
|
||||||
|
"small_square_crop": album.cover.crop["50x50"].url,
|
||||||
|
},
|
||||||
"release_date": to_api_date(album.release_date),
|
"release_date": to_api_date(album.release_date),
|
||||||
}
|
}
|
||||||
serializer = serializers.ArtistAlbumSerializer(album)
|
serializer = serializers.ArtistAlbumSerializer(album)
|
||||||
|
@ -83,7 +88,12 @@ def test_album_serializer(factories, to_api_date):
|
||||||
"title": album.title,
|
"title": album.title,
|
||||||
"artist": serializers.ArtistSimpleSerializer(album.artist).data,
|
"artist": serializers.ArtistSimpleSerializer(album.artist).data,
|
||||||
"creation_date": to_api_date(album.creation_date),
|
"creation_date": to_api_date(album.creation_date),
|
||||||
"cover": album.cover.url,
|
"cover": {
|
||||||
|
"original": album.cover.url,
|
||||||
|
"square_crop": album.cover.crop["400x400"].url,
|
||||||
|
"medium_square_crop": album.cover.crop["200x200"].url,
|
||||||
|
"small_square_crop": album.cover.crop["50x50"].url,
|
||||||
|
},
|
||||||
"release_date": to_api_date(album.release_date),
|
"release_date": to_api_date(album.release_date),
|
||||||
"tracks": serializers.AlbumTrackSerializer([track2, track1], many=True).data,
|
"tracks": serializers.AlbumTrackSerializer([track2, track1], many=True).data,
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,11 +80,11 @@ def test_playlist_serializer_include_covers(factories, api_request):
|
||||||
qs = playlist.__class__.objects.with_covers().with_tracks_count()
|
qs = playlist.__class__.objects.with_covers().with_tracks_count()
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
request.build_absolute_uri(t1.album.cover.url),
|
request.build_absolute_uri(t1.album.cover.crop["200x200"].url),
|
||||||
request.build_absolute_uri(t2.album.cover.url),
|
request.build_absolute_uri(t2.album.cover.crop["200x200"].url),
|
||||||
request.build_absolute_uri(t4.album.cover.url),
|
request.build_absolute_uri(t4.album.cover.crop["200x200"].url),
|
||||||
request.build_absolute_uri(t5.album.cover.url),
|
request.build_absolute_uri(t5.album.cover.crop["200x200"].url),
|
||||||
request.build_absolute_uri(t6.album.cover.url),
|
request.build_absolute_uri(t6.album.cover.crop["200x200"].url),
|
||||||
]
|
]
|
||||||
|
|
||||||
serializer = serializers.PlaylistSerializer(qs.get(), context={"request": request})
|
serializer = serializers.PlaylistSerializer(qs.get(), context={"request": request})
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
Use thumbnails for avatars and covers to reduce bandwidth
|
||||||
|
|
||||||
|
|
||||||
|
Image thumbnails [Manual action required]
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
To reduce bandwidth usage on slow or limited connexions and improve performance
|
||||||
|
in general, we now use smaller images in the front-end. For instance, if you have
|
||||||
|
an album cover with a 1000x1000 pixel size, we will create smaller
|
||||||
|
versions of this image (50x50, 200x200, 400x400) and reference those resized version
|
||||||
|
when we don't actually need the original image.
|
||||||
|
|
||||||
|
Thumbnail will be created automatically for new objects, however, you have
|
||||||
|
to launch a manual command to deal with existing ones.
|
||||||
|
|
||||||
|
On docker setups::
|
||||||
|
|
||||||
|
docker-compose run --rm api python manage.py script create_image_variations --no-input
|
||||||
|
|
||||||
|
On non-docker setups::
|
||||||
|
|
||||||
|
python manage.py script create_image_variations --no-input
|
||||||
|
|
||||||
|
This should be quite fast but may take up to a few minutes depending on the number
|
||||||
|
of albums you have in database. It is safe to interrupt the process or rerun it multiple times.
|
|
@ -133,7 +133,7 @@
|
||||||
<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="right aligned">{{ index + 1}}</td>
|
||||||
<td class="center aligned">
|
<td class="center aligned">
|
||||||
<img class="ui mini image" v-if="track.album.cover" :src="$store.getters['instance/absoluteUrl'](track.album.cover)">
|
<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">
|
<img class="ui mini image" v-else src="../assets/audio/default-cover.png">
|
||||||
</td>
|
</td>
|
||||||
<td colspan="4">
|
<td colspan="4">
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<div v-if="currentTrack" class="track-area ui unstackable items">
|
<div v-if="currentTrack" class="track-area ui unstackable items">
|
||||||
<div class="ui inverted item">
|
<div class="ui inverted item">
|
||||||
<div class="ui tiny image">
|
<div class="ui tiny image">
|
||||||
<img ref="cover" @load="updateBackground" v-if="currentTrack.album.cover" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover)">
|
<img ref="cover" @load="updateBackground" v-if="currentTrack.album.cover && currentTrack.album.cover.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.medium_square_crop)">
|
||||||
<img v-else src="../../assets/audio/default-cover.png">
|
<img v-else src="../../assets/audio/default-cover.png">
|
||||||
</div>
|
</div>
|
||||||
<div class="middle aligned content">
|
<div class="middle aligned content">
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="ui card">
|
<div class="ui card">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="right floated tiny ui image">
|
<div class="right floated tiny ui image">
|
||||||
<img v-if="album.cover" v-lazy="$store.getters['instance/absoluteUrl'](album.cover)">
|
<img v-if="album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](album.cover.square_crop)">
|
||||||
<img v-else src="../../../assets/audio/default-cover.png">
|
<img v-else src="../../../assets/audio/default-cover.png">
|
||||||
</div>
|
</div>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
<h3 class="ui header">
|
<h3 class="ui header">
|
||||||
<slot name="title"></slot>
|
<slot name="title"></slot>
|
||||||
</h3>
|
</h3>
|
||||||
<i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'large', 'angle left', 'icon']">
|
<i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'medium', 'angle left', 'icon']">
|
||||||
</i>
|
</i>
|
||||||
<i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'large', 'angle right', 'icon']">
|
<i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'medium', 'angle right', 'icon']">
|
||||||
</i>
|
</i>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider"></div>
|
||||||
<div class="ui five cards">
|
<div class="ui five cards">
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
<div class="ui loader"></div>
|
<div class="ui loader"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card" v-for="album in albums" :key="album.id">
|
<div class="card" v-for="album in albums" :key="album.id">
|
||||||
<div :class="['ui', 'image', 'with-overlay', {'default-cover': !album.cover}]" :style="getImageStyle(album)">
|
<div :class="['ui', 'image', 'with-overlay', {'default-cover': !album.cover.original}]" :style="getImageStyle(album)">
|
||||||
<play-button class="play-overlay" :icon-only="true" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :album="album.id"></play-button>
|
<play-button class="play-overlay" :icon-only="true" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :album="album.id"></play-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -92,8 +92,8 @@ export default {
|
||||||
getImageStyle (album) {
|
getImageStyle (album) {
|
||||||
let url = '../../../assets/audio/default-cover.png'
|
let url = '../../../assets/audio/default-cover.png'
|
||||||
|
|
||||||
if (album.cover) {
|
if (album.cover.original) {
|
||||||
url = this.$store.getters['instance/absoluteUrl'](album.cover)
|
url = this.$store.getters['instance/absoluteUrl'](album.cover.medium_square_crop)
|
||||||
} else {
|
} else {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="album in albums">
|
<tr v-for="album in albums">
|
||||||
<td>
|
<td>
|
||||||
<img class="ui mini image" v-if="album.cover" :src="$store.getters['instance/absoluteUrl'](album.cover)">
|
<img class="ui mini image" v-if="album.cover.original" :src="$store.getters['instance/absoluteUrl'](album.cover.small_square_crop)">
|
||||||
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
|
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
|
||||||
</td>
|
</td>
|
||||||
<td colspan="4">
|
<td colspan="4">
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<play-button class="basic icon" :discrete="true" :track="track"></play-button>
|
<play-button class="basic icon" :discrete="true" :track="track"></play-button>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<img class="ui mini image" v-if="track.album.cover" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover)">
|
<img class="ui mini image" v-if="track.album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.small_square_crop)">
|
||||||
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
|
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
|
||||||
</td>
|
</td>
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
<h3 class="ui header">
|
<h3 class="ui header">
|
||||||
<slot name="title"></slot>
|
<slot name="title"></slot>
|
||||||
</h3>
|
</h3>
|
||||||
<i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'large', 'angle up', 'icon']">
|
<i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'medium', 'angle up', 'icon']">
|
||||||
</i>
|
</i>
|
||||||
<i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'large', 'angle down', 'icon']">
|
<i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'medium', 'angle down', 'icon']">
|
||||||
|
</i>
|
||||||
|
<i @click="fetchData(url)" :class="['ui', 'circular', 'medium', 'refresh', 'icon']">
|
||||||
</i>
|
</i>
|
||||||
<div class="ui divided unstackable items">
|
<div class="ui divided unstackable items">
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||||
|
@ -13,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="item" v-for="object in objects" :key="object.id">
|
<div class="item" v-for="object in objects" :key="object.id">
|
||||||
<div class="ui tiny image">
|
<div class="ui tiny image">
|
||||||
<img v-if="object.track.album.cover" v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover)">
|
<img v-if="object.track.album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.medium_square_crop)">
|
||||||
<img v-else src="../../../assets/audio/default-cover.png">
|
<img v-else src="../../../assets/audio/default-cover.png">
|
||||||
<play-button class="play-overlay" :icon-only="true" :button-classes="['ui', 'circular', 'tiny', 'orange', 'icon', 'button']" :track="object.track"></play-button>
|
<play-button class="play-overlay" :icon-only="true" :button-classes="['ui', 'circular', 'tiny', 'orange', 'icon', 'button']" :track="object.track"></play-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -121,4 +123,7 @@ export default {
|
||||||
left: 2.5em;
|
left: 2.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.refresh.icon {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="album">
|
<template v-if="album">
|
||||||
<div :class="['ui', 'head', {'with-background': album.cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="album.title">
|
<div :class="['ui', 'head', {'with-background': album.cover.original}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="album.title">
|
||||||
<div class="segment-content">
|
<div class="segment-content">
|
||||||
<h2 class="ui center aligned icon header">
|
<h2 class="ui center aligned icon header">
|
||||||
<i class="circular inverted sound yellow icon"></i>
|
<i class="circular inverted sound yellow icon"></i>
|
||||||
|
@ -98,10 +98,10 @@ export default {
|
||||||
return 'https://musicbrainz.org/release/' + this.album.mbid
|
return 'https://musicbrainz.org/release/' + this.album.mbid
|
||||||
},
|
},
|
||||||
headerStyle () {
|
headerStyle () {
|
||||||
if (!this.album.cover) {
|
if (!this.album.cover.original) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.album.cover) + ')'
|
return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.album.cover.original) + ')'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -158,10 +158,10 @@ export default {
|
||||||
})[0]
|
})[0]
|
||||||
},
|
},
|
||||||
headerStyle () {
|
headerStyle () {
|
||||||
if (!this.cover) {
|
if (!this.cover.original) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.cover) + ')'
|
return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.cover.original) + ')'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
<h3 class="ui header">
|
<h3 class="ui header">
|
||||||
<slot name="title"></slot>
|
<slot name="title"></slot>
|
||||||
</h3>
|
</h3>
|
||||||
<i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'large', 'angle up', 'icon']">
|
<i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'medium', 'angle up', 'icon']">
|
||||||
</i>
|
</i>
|
||||||
<i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'large', 'angle down', 'icon']">
|
<i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'medium', 'angle down', 'icon']">
|
||||||
|
</i>
|
||||||
|
<i @click="fetchData(url)" :class="['ui', 'circular', 'medium', 'refresh', 'icon']">
|
||||||
</i>
|
</i>
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||||
<div class="ui loader"></div>
|
<div class="ui loader"></div>
|
||||||
|
@ -75,3 +77,8 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.refresh.icon {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue