diff --git a/api/funkwhale_api/common/models.py b/api/funkwhale_api/common/models.py index 1b70304eddad9b0ddd013a3cb9d3aecd9c5db0e6..c10d8dd067d59d5a2e60f2bb598a200f62ff6f41 100644 --- a/api/funkwhale_api/common/models.py +++ b/api/funkwhale_api/common/models.py @@ -180,6 +180,7 @@ class AttachmentQuerySet(models.QuerySet): "mutation_attachment", "covered_track", "covered_artist", + "iconed_actor", ] query = None for field in related_fields: diff --git a/api/funkwhale_api/common/scripts/create_image_variations.py b/api/funkwhale_api/common/scripts/create_image_variations.py index 31bf0269c3602a917014d819b7dacf387f965052..10bef7a35179f7e27b744f16fd68f9ef684d6054 100644 --- a/api/funkwhale_api/common/scripts/create_image_variations.py +++ b/api/funkwhale_api/common/scripts/create_image_variations.py @@ -5,13 +5,9 @@ Compute different sizes of image used for Album covers and User avatars from versatileimagefield.image_warmer import VersatileImageFieldWarmer from funkwhale_api.common.models import Attachment -from funkwhale_api.music.models import Album -from funkwhale_api.users.models import User MODELS = [ - (Album, "cover", "square"), - (User, "avatar", "square"), (Attachment, "file", "attachment_square"), ] diff --git a/api/funkwhale_api/common/serializers.py b/api/funkwhale_api/common/serializers.py index 38be2b5bc14551d022a10469399df647b4cc92bd..c754540c924a276f61f7ab9aad5fecb237a5418e 100644 --- a/api/funkwhale_api/common/serializers.py +++ b/api/funkwhale_api/common/serializers.py @@ -24,6 +24,7 @@ class RelatedField(serializers.RelatedField): self.related_field_name = related_field_name self.serializer = serializer self.filters = kwargs.pop("filters", None) + self.queryset_filter = kwargs.pop("queryset_filter", None) try: kwargs["queryset"] = kwargs.pop("queryset") except KeyError: @@ -36,10 +37,16 @@ class RelatedField(serializers.RelatedField): filters.update(self.filters(self.context)) return filters + def filter_queryset(self, queryset): + if self.queryset_filter: + queryset = self.queryset_filter(queryset, self.context) + return queryset + def to_internal_value(self, data): try: queryset = self.get_queryset() filters = self.get_filters(data) + queryset = self.filter_queryset(queryset) return queryset.get(**filters) except ObjectDoesNotExist: self.fail( @@ -318,3 +325,16 @@ class ContentSerializer(serializers.Serializer): def get_html(self, o): return utils.render_html(o.text, o.content_type) + + +class NullToEmptDict(object): + def get_attribute(self, o): + attr = super().get_attribute(o) + if attr is None: + return {} + return attr + + def to_representation(self, v): + if not v: + return v + return super().to_representation(v) diff --git a/api/funkwhale_api/common/utils.py b/api/funkwhale_api/common/utils.py index 9fb9f64e2928eb693646d57736824c02d22bf947..d7135c4a0303c621b9bf34fb78268a0a5336f23f 100644 --- a/api/funkwhale_api/common/utils.py +++ b/api/funkwhale_api/common/utils.py @@ -327,8 +327,11 @@ def attach_file(obj, field, file_data, fetch=False): extensions = {"image/jpeg": "jpg", "image/png": "png", "image/gif": "gif"} extension = extensions.get(file_data["mimetype"], "jpg") attachment = models.Attachment(mimetype=file_data["mimetype"]) - - filename = "cover-{}.{}".format(obj.uuid, extension) + name_fields = ["uuid", "full_username", "pk"] + name = [getattr(obj, field) for field in name_fields if getattr(obj, field, None)][ + 0 + ] + filename = "{}-{}.{}".format(field, name, extension) if "url" in file_data: attachment.url = file_data["url"] else: diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py index 3e1f337593624bbc0b338512a66b04754d8f1e74..db0c909001edef6f854216a9a22895a2a42c0fb0 100644 --- a/api/funkwhale_api/favorites/views.py +++ b/api/funkwhale_api/favorites/views.py @@ -22,7 +22,9 @@ class TrackFavoriteViewSet( filterset_class = filters.TrackFavoriteFilter serializer_class = serializers.UserTrackFavoriteSerializer - queryset = models.TrackFavorite.objects.all().select_related("user__actor") + queryset = models.TrackFavorite.objects.all().select_related( + "user__actor__attachment_icon" + ) permission_classes = [ oauth_permissions.ScopePermission, permissions.OwnerPermission, diff --git a/api/funkwhale_api/federation/migrations/0024_actor_attachment_icon.py b/api/funkwhale_api/federation/migrations/0024_actor_attachment_icon.py new file mode 100644 index 0000000000000000000000000000000000000000..66a888e0f35c4a641cc27b2c7f010b5ee0ad9d87 --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0024_actor_attachment_icon.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.9 on 2020-01-23 13:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0007_auto_20200116_1610'), + ('federation', '0023_actor_summary_obj'), + ] + + operations = [ + migrations.AddField( + model_name='actor', + name='attachment_icon', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='iconed_actor', to='common.Attachment'), + ), + ] diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 211ac944717b396cfcfd53d86542ddaa4da70352..2592afb16cde62f394aac84f6adcfee651a6cdaa 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -205,6 +205,13 @@ class Actor(models.Model): through_fields=("target", "actor"), related_name="following", ) + attachment_icon = models.ForeignKey( + "common.Attachment", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="iconed_actor", + ) objects = ActorQuerySet.as_manager() diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 4883062b313b8e2d63026351b01eb0e95bdf22db..96ff2fcd37d71c4db5220ebe4ed19dec551bad90 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -1,9 +1,7 @@ import logging -import mimetypes import urllib.parse import uuid -from django.core.exceptions import ObjectDoesNotExist from django.core.paginator import Paginator from django.db import transaction @@ -97,6 +95,9 @@ class ActorSerializer(jsonld.JsonLdSerializer): following = serializers.URLField(max_length=500, required=False, allow_null=True) publicKey = PublicKeySerializer(required=False) endpoints = EndpointsSerializer(required=False) + icon = LinkSerializer( + allowed_mimetypes=["image/*"], allow_null=True, required=False + ) class Meta: jsonld_mapping = { @@ -113,6 +114,7 @@ class ActorSerializer(jsonld.JsonLdSerializer): ), "mediaType": jsonld.first_val(contexts.AS.mediaType), "endpoints": jsonld.first_obj(contexts.AS.endpoints), + "icon": jsonld.first_obj(contexts.AS.icon), } def to_representation(self, instance): @@ -143,17 +145,11 @@ class ActorSerializer(jsonld.JsonLdSerializer): "id": "{}#main-key".format(instance.fid), } ret["endpoints"] = {} + + include_image(ret, instance.attachment_icon, "icon") + if instance.shared_inbox_url: ret["endpoints"]["sharedInbox"] = instance.shared_inbox_url - try: - if instance.user.avatar: - ret["icon"] = { - "type": "Image", - "mediaType": mimetypes.guess_type(instance.user.avatar_path)[0], - "url": utils.full_url(instance.user.avatar.crop["400x400"].url), - } - except ObjectDoesNotExist: - pass return ret def prepare_missing_fields(self): @@ -201,6 +197,15 @@ class ActorSerializer(jsonld.JsonLdSerializer): common_utils.attach_content( actor, "summary_obj", self.validated_data["summary"] ) + if "icon" in self.validated_data: + new_value = self.validated_data["icon"] + common_utils.attach_file( + actor, + "attachment_icon", + {"url": new_value["href"], "mimetype": new_value["mediaType"]} + if new_value + else None, + ) return actor def validate(self, data): @@ -844,15 +849,15 @@ def include_content(repr, content_obj): repr["mediaType"] = "text/html" -def include_image(repr, attachment): +def include_image(repr, attachment, field="image"): if attachment: - repr["image"] = { + repr[field] = { "type": "Image", "href": attachment.download_url_original, "mediaType": attachment.mimetype or "image/jpeg", } else: - repr["image"] = None + repr[field] = None class MusicEntitySerializer(jsonld.JsonLdSerializer): diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py index 6cdbc8a80f848c074adde2dee2f04f4ff5eefec1..56afadf4046c2ca984605b1986ea6920d400d1de 100644 --- a/api/funkwhale_api/history/views.py +++ b/api/funkwhale_api/history/views.py @@ -19,7 +19,9 @@ class ListeningViewSet( ): serializer_class = serializers.ListeningSerializer - queryset = models.Listening.objects.all().select_related("user__actor") + queryset = models.Listening.objects.all().select_related( + "user__actor__attachment_icon" + ) permission_classes = [ oauth_permissions.ScopePermission, diff --git a/api/funkwhale_api/music/mutations.py b/api/funkwhale_api/music/mutations.py index c208d93a1fdd4be3b4b4d72ce144a030b5ddcb91..d95d3570250f82a1468542dc2d819c97042eb50f 100644 --- a/api/funkwhale_api/music/mutations.py +++ b/api/funkwhale_api/music/mutations.py @@ -61,7 +61,9 @@ class DescriptionMutation(mutations.UpdateMutationSerializer): class CoverMutation(mutations.UpdateMutationSerializer): cover = common_serializers.RelatedField( - "uuid", queryset=common_models.Attachment.objects.all().local(), serializer=None + "uuid", + queryset=common_models.Attachment.objects.all().local(), + serializer=None, ) def get_serialized_relations(self): diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 27cca51da17fbb74cbd0c21c575f8b8fb2679926..a6ca51f0aacc31b58e764572f810d66ee4af3de5 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -18,20 +18,9 @@ from funkwhale_api.tags import serializers as tags_serializers from . import filters, models, tasks -class NullToEmptDict(object): - def get_attribute(self, o): - attr = super().get_attribute(o) - if attr is None: - return {} - return attr - - def to_representation(self, v): - if not v: - return v - return super().to_representation(v) - - -class CoverField(NullToEmptDict, common_serializers.AttachmentSerializer): +class CoverField( + common_serializers.NullToEmptDict, common_serializers.AttachmentSerializer +): # XXX: BACKWARD COMPATIBILITY pass diff --git a/api/funkwhale_api/playlists/views.py b/api/funkwhale_api/playlists/views.py index f2ade81814411399d51d0674031f497af39f0490..2cc348f460b3180d7df8c4ca4e88e2eab19c92cb 100644 --- a/api/funkwhale_api/playlists/views.py +++ b/api/funkwhale_api/playlists/views.py @@ -23,7 +23,7 @@ class PlaylistViewSet( serializer_class = serializers.PlaylistSerializer queryset = ( models.Playlist.objects.all() - .select_related("user__actor") + .select_related("user__actor__attachment_icon") .annotate(tracks_count=Count("playlist_tracks", distinct=True)) .with_covers() .with_duration() diff --git a/api/funkwhale_api/users/migrations/0017_actor_avatar.py b/api/funkwhale_api/users/migrations/0017_actor_avatar.py new file mode 100644 index 0000000000000000000000000000000000000000..c97a5fe13fe3375530473e0f2ffe3fc61d71797f --- /dev/null +++ b/api/funkwhale_api/users/migrations/0017_actor_avatar.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +def create_attachments(apps, schema_editor): + Actor = apps.get_model("federation", "Actor") + User = apps.get_model("users", "User") + Attachment = apps.get_model("common", "Attachment") + + obj_attachment_mapping = {} + def get_mimetype(path): + if path.lower().endswith('.png'): + return "image/png" + return "image/jpeg" + qs = User.objects.filter(actor__attachment_icon=None).exclude(avatar="").exclude(avatar=None).exclude(actor=None).select_related('actor') + total = qs.count() + print('Creating attachments for {} user avatars, this may take a while…'.format(total)) + from django.core.files.storage import FileSystemStorage + for i, user in enumerate(qs): + if isinstance(user.avatar.storage._wrapped, FileSystemStorage): + try: + size = user.avatar.size + except FileNotFoundError: + # can occur when file isn't found on disk or S3 + print(" Warning: avatar file wasn't found in storage: {}".format(e.__class__)) + size = None + obj_attachment_mapping[user.actor] = Attachment( + file=user.avatar, + size=size, + mimetype=get_mimetype(user.avatar.name), + ) + print('Commiting changes…') + Attachment.objects.bulk_create(obj_attachment_mapping.values(), batch_size=2000) + # map each attachment to the corresponding obj + # and bulk save + for obj, attachment in obj_attachment_mapping.items(): + obj.attachment_icon = attachment + + Actor.objects.bulk_update(obj_attachment_mapping.keys(), fields=['attachment_icon'], batch_size=2000) + + +def rewind(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [("users", "0016_auto_20190920_0857"), ("federation", "0024_actor_attachment_icon")] + + operations = [migrations.RunPython(create_attachments, rewind)] diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index 25eb926c7e37b8810e0ddc384630697575cfa859..72ab2afd669ccf8d1ec18e6428f9abb99d40a087 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -21,7 +21,6 @@ from django_auth_ldap.backend import populate_user as ldap_populate_user from oauth2_provider import models as oauth2_models from oauth2_provider import validators as oauth2_validators from versatileimagefield.fields import VersatileImageField -from versatileimagefield.image_warmer import VersatileImageFieldWarmer from funkwhale_api.common import fields, preferences from funkwhale_api.common import utils as common_utils @@ -413,13 +412,3 @@ def create_actor(user): def init_ldap_user(sender, user, ldap_user, **kwargs): if not user.actor: user.actor = create_actor(user) - - -@receiver(models.signals.post_save, sender=User) -def warm_user_avatar(sender, instance, **kwargs): - if not instance.avatar or not settings.CREATE_IMAGE_THUMBNAILS: - 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() diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index 0d167b1a1b7fd8c181f1c3abd587973bb89af62c..59986fdaa84ac107ce7102a39f1de9c4217fe64a 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -7,9 +7,9 @@ from django.utils.translation import gettext_lazy as _ from rest_auth.serializers import PasswordResetSerializer as PRS from rest_auth.registration.serializers import RegisterSerializer as RS, get_adapter from rest_framework import serializers -from versatileimagefield.serializers import VersatileImageFieldSerializer from funkwhale_api.activity import serializers as activity_serializers +from funkwhale_api.common import models as common_models from funkwhale_api.common import serializers as common_serializers from funkwhale_api.common import utils as common_utils from funkwhale_api.federation import models as federation_models @@ -89,26 +89,30 @@ class UserActivitySerializer(activity_serializers.ModelSerializer): return "Person" -class AvatarField( - common_serializers.StripExifImageField, VersatileImageFieldSerializer -): - pass - - -avatar_field = AvatarField(allow_null=True, sizes="square") - - class UserBasicSerializer(serializers.ModelSerializer): - avatar = avatar_field + avatar = serializers.SerializerMethodField() class Meta: model = models.User fields = ["id", "username", "name", "date_joined", "avatar"] + def get_avatar(self, o): + return common_serializers.AttachmentSerializer( + o.actor.attachment_icon if o.actor else None + ).data + class UserWriteSerializer(serializers.ModelSerializer): - avatar = avatar_field summary = common_serializers.ContentSerializer(required=False, allow_null=True) + avatar = common_serializers.RelatedField( + "uuid", + queryset=common_models.Attachment.objects.all().local().attached(False), + serializer=None, + queryset_filter=lambda qs, context: qs.filter( + actor=context["request"].user.actor + ), + write_only=True, + ) class Meta: model = models.User @@ -125,19 +129,30 @@ class UserWriteSerializer(serializers.ModelSerializer): if not obj.actor: obj.create_actor() summary = validated_data.pop("summary", NOOP) + avatar = validated_data.pop("avatar", NOOP) + obj = super().update(obj, validated_data) if summary != NOOP: common_utils.attach_content(obj.actor, "summary_obj", summary) - + if avatar != NOOP: + obj.actor.attachment_icon = avatar + obj.actor.save(update_fields=["attachment_icon"]) return obj + def to_representation(self, obj): + repr = super().to_representation(obj) + repr["avatar"] = common_serializers.AttachmentSerializer( + obj.actor.attachment_icon + ).data + return repr + class UserReadSerializer(serializers.ModelSerializer): permissions = serializers.SerializerMethodField() full_username = serializers.SerializerMethodField() - avatar = avatar_field + avatar = serializers.SerializerMethodField() class Meta: model = models.User @@ -155,6 +170,9 @@ class UserReadSerializer(serializers.ModelSerializer): "avatar", ] + def get_avatar(self, o): + return common_serializers.AttachmentSerializer(o.actor.attachment_icon).data + def get_permissions(self, o): return o.get_permissions() diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py index 28189c4bc547e883ee4f5ee1227923fa7e07ca67..7e94f34a67807cdc4320d375edd642702810ba52 100644 --- a/api/funkwhale_api/users/views.py +++ b/api/funkwhale_api/users/views.py @@ -44,7 +44,7 @@ class PasswordResetConfirmView(rest_auth_views.PasswordResetConfirmView): class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): - queryset = models.User.objects.all() + queryset = models.User.objects.all().select_related("actor__attachment_icon") serializer_class = serializers.UserWriteSerializer lookup_field = "username" lookup_value_regex = r"[a-zA-Z0-9-_.]+" diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py index 93fd14ba17a928a57378c3332733c58514313dfe..555684e0750f73ebef199e0b2a03e5c4f9304f02 100644 --- a/api/tests/federation/test_serializers.py +++ b/api/tests/federation/test_serializers.py @@ -36,6 +36,11 @@ def test_actor_serializer_from_ap(db): "id": actor_url + "#main-key", }, "endpoints": {"sharedInbox": "https://noop.url/federation/shared/inbox"}, + "icon": { + "type": "Image", + "mediaType": "image/jpeg", + "href": "https://image.example/image.png", + }, } serializer = serializers.ActorSerializer(data=payload) @@ -60,6 +65,8 @@ def test_actor_serializer_from_ap(db): assert actor.private_key is None assert actor.public_key == payload["publicKey"]["publicKeyPem"] assert actor.domain_id == "test.federation" + assert actor.attachment_icon.url == payload["icon"]["href"] + assert actor.attachment_icon.mimetype == payload["icon"]["mediaType"] def test_actor_serializer_only_mandatory_field_from_ap(db): @@ -90,7 +97,7 @@ def test_actor_serializer_only_mandatory_field_from_ap(db): assert actor.manually_approves_followers is None -def test_actor_serializer_to_ap(db): +def test_actor_serializer_to_ap(factories): expected = { "@context": jsonld.get_default_context(), "id": "https://test.federation/user", @@ -122,12 +129,18 @@ def test_actor_serializer_to_ap(db): domain=models.Domain.objects.create(pk="test.federation"), type="Person", manually_approves_followers=False, + attachment_icon=factories["common.Attachment"](), ) content = common_utils.attach_content( ac, "summary_obj", {"text": "hello world", "content_type": "text/markdown"} ) expected["summary"] = content.rendered + expected["icon"] = { + "type": "Image", + "mediaType": "image/jpeg", + "href": utils.full_url(ac.attachment_icon.file.url), + } serializer = serializers.ActorSerializer(ac) assert serializer.data == expected @@ -1133,6 +1146,7 @@ def test_local_actor_serializer_to_ap(factories): domain=models.Domain.objects.create(pk="test.federation"), type="Person", manually_approves_followers=False, + attachment_icon=factories["common.Attachment"](), ) content = common_utils.attach_content( ac, "summary_obj", {"text": "hello world", "content_type": "text/markdown"} @@ -1145,7 +1159,7 @@ def test_local_actor_serializer_to_ap(factories): expected["icon"] = { "type": "Image", "mediaType": "image/jpeg", - "url": utils.full_url(user.avatar.crop["400x400"].url), + "href": utils.full_url(ac.attachment_icon.file.url), } serializer = serializers.ActorSerializer(ac) diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py index d01252621570aff72de0510d2cce051fb5ea1fc5..3b5cf4cd369ee7edbb076c80d1c3480539c8ca85 100644 --- a/api/tests/users/test_views.py +++ b/api/tests/users/test_views.py @@ -110,7 +110,9 @@ def test_can_fetch_data_from_api(api_client, factories): user = factories["users.User"](permission_library=True, with_actor=True) summary = {"content_type": "text/plain", "text": "Hello"} summary_obj = common_utils.attach_content(user.actor, "summary_obj", summary) - + avatar = factories["common.Attachment"]() + user.actor.attachment_icon = avatar + user.actor.save() api_client.login(username=user.username, password="test") response = api_client.get(url) assert response.status_code == 200 @@ -120,6 +122,9 @@ def test_can_fetch_data_from_api(api_client, factories): assert response.data["email"] == user.email assert response.data["name"] == user.name assert response.data["permissions"] == user.get_permissions() + assert ( + response.data["avatar"] == common_serializers.AttachmentSerializer(avatar).data + ) assert ( response.data["summary"] == common_serializers.ContentSerializer(summary_obj).data @@ -301,18 +306,18 @@ def test_user_cannot_patch_another_user(method, logged_in_api_client, factories) assert response.status_code == 403 -def test_user_can_patch_their_own_avatar(logged_in_api_client, avatar): +def test_user_can_patch_their_own_avatar(logged_in_api_client, factories): user = logged_in_api_client.user + actor = user.create_actor() + attachment = factories["common.Attachment"](actor=actor) url = reverse("api:v1:users:users-detail", kwargs={"username": user.username}) - content = avatar.read() - avatar.seek(0) - payload = {"avatar": avatar} + payload = {"avatar": attachment.uuid} response = logged_in_api_client.patch(url, payload) assert response.status_code == 200 user.refresh_from_db() - assert user.avatar.read() == content + assert user.actor.attachment_icon == attachment def test_creating_user_creates_actor_as_well( diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index da693b257c7e7b543893304c30561cefbf98b579..fdf36f88a36f75c08654f3cc407dd76ca09ac795 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -74,7 +74,7 @@ </router-link> <div class="item"> <div class="ui user-dropdown dropdown" > - <img class="ui avatar image" v-if="$store.state.auth.profile.avatar.square_crop" v-lazy="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.square_crop)" /> + <img class="ui avatar image" v-if="$store.state.auth.profile.avatar.square_crop" :src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.square_crop)" /> <actor-avatar v-else :actor="{preferred_username: $store.state.auth.username, full_username: $store.state.auth.username}" /> <div class="menu"> <router-link class="item" :to="{name: 'profile', params: {username: $store.state.auth.username}}"><translate translate-context="*/*/*/Noun">Profile</translate></router-link> diff --git a/front/src/components/auth/Settings.vue b/front/src/components/auth/Settings.vue index 795a5c45e92d599d8d83eccd1474dcd86046702b..269e7ac29d2d183b4aa8d5502c866ced9aa66e1b 100644 --- a/front/src/components/auth/Settings.vue +++ b/front/src/components/auth/Settings.vue @@ -41,25 +41,13 @@ <li v-for="error in avatarErrors">{{ error }}</li> </ul> </div> - <div class="ui stackable grid"> - <div class="ui ten wide column"> - <h3 class="ui header"><translate translate-context="Content/Settings/Title/Verb">Upload a new avatar</translate></h3> - <p><translate translate-context="Content/Settings/Paragraph">PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px.</translate></p> - <input class="ui input" ref="avatar" type="file" /> - <div class="ui hidden divider"></div> - <button @click="submitAvatar" :class="['ui', {'loading': isLoadingAvatar}, 'button']"> - <translate translate-context="Content/Settings/Button.Label/Verb">Update avatar</translate> - </button> - </div> - <div class="ui six wide column"> - <h3 class="ui header"><translate translate-context="Content/Settings/Title/Noun">Current avatar</translate></h3> - <img class="ui circular image" v-if="currentAvatar && currentAvatar.square_crop" v-lazy="$store.getters['instance/absoluteUrl'](currentAvatar.medium_square_crop)" /> - <div class="ui hidden divider"></div> - <button @click="removeAvatar" v-if="currentAvatar && currentAvatar.square_crop" :class="['ui', {'loading': isLoadingAvatar}, ,'yellow', 'button']"> - <translate translate-context="Content/Settings/Button.Label/Verb">Remove avatar</translate> - </button> - </div> - </div> + {{ }} + <attachment-input + :value="avatar.uuid" + @input="submitAvatar($event)" + :initial-value="initialAvatar" + :required="false" + @delete="avatar = {uuid: null}"></attachment-input> </div> </section> @@ -315,12 +303,14 @@ import logger from "@/logging" import PasswordInput from "@/components/forms/PasswordInput" import SubsonicTokenForm from "@/components/auth/SubsonicTokenForm" import TranslationsMixin from "@/components/mixins/Translations" +import AttachmentInput from '@/components/common/AttachmentInput' export default { mixins: [TranslationsMixin], components: { PasswordInput, - SubsonicTokenForm + SubsonicTokenForm, + AttachmentInput }, data() { let d = { @@ -328,7 +318,7 @@ export default { // properties that will be used in it old_password: "", new_password: "", - currentAvatar: this.$store.state.auth.profile.avatar, + avatar: {...(this.$store.state.auth.profile.avatar || {uuid: null})}, passwordError: "", password: "", isLoading: false, @@ -336,7 +326,6 @@ export default { isDeletingAccount: false, accountDeleteErrors: [], avatarErrors: [], - avatar: null, apps: [], ownedApps: [], settings: { @@ -352,6 +341,7 @@ export default { } } } + d.initialAvatar = d.avatar.uuid d.settings.order.forEach(id => { d.settings.fields[id].value = d.settings.fields[id].initial d.settings.fields[id].id = id @@ -437,44 +427,17 @@ export default { } ) }, - submitAvatar() { + submitAvatar(uuid) { this.isLoadingAvatar = true this.avatarErrors = [] 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, { - headers: { - "Content-Type": "multipart/form-data" - } - }) - .then( - response => { - this.isLoadingAvatar = false - self.currentAvatar = response.data.avatar - 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 - }) + .patch(`users/users/${this.$store.state.auth.username}/`, {avatar: uuid}) .then( response => { this.isLoadingAvatar = false - self.currentAvatar = {} - self.$store.commit("auth/avatar", self.currentAvatar) + self.avatar = response.data.avatar + self.$store.commit("auth/avatar", response.data.avatar) }, error => { self.isLoadingAvatar = false