avatar upload works

This commit is contained in:
Henry Jameson 2025-08-04 03:35:09 +03:00
commit b305748a92
10 changed files with 239 additions and 207 deletions

View file

@ -15,6 +15,8 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
import Modal from 'src/components/modal/modal.vue'
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
import localeService from 'src/services/locale/locale.service.js'
import suggestor from 'src/components/emoji_input/suggestor.js'
@ -34,7 +36,8 @@ import {
faTimes,
faExpandAlt,
faBirthdayCake,
faSave
faSave,
faChevronRight
} from '@fortawesome/free-solid-svg-icons'
import { useMediaViewerStore } from '../../stores/media_viewer'
@ -49,7 +52,8 @@ library.add(
faEdit,
faTimes,
faExpandAlt,
faBirthdayCake
faBirthdayCake,
faChevronRight
)
export default {
@ -66,6 +70,7 @@ export default {
'hasNoteEditor'
],
components: {
Modal,
UserAvatar,
Checkbox,
RemoteFollow,
@ -79,7 +84,8 @@ export default {
UserNote,
UserTimedFilterModal,
ColorInput,
EmojiInput
EmojiInput,
ImageCropper
},
data () {
const user = this.$store.state.users.currentUser
@ -93,6 +99,7 @@ export default {
newName: user.name_unescaped,
editingName: false,
newActorType: user.actor_type,
editImage: false,
newBio: unescape(user.description),
editingBio: false,
newBirthday: user.birthday,
@ -223,6 +230,29 @@ export default {
},
// Editable stuff
defaultAvatar () {
return this.$store.state.instance.server + this.$store.state.instance.defaultAvatar
},
defaultBanner () {
return this.$store.state.instance.server + this.$store.state.instance.defaultBanner
},
isDefaultAvatar () {
const baseAvatar = this.$store.state.instance.defaultAvatar
return !(this.$store.state.users.currentUser.profile_image_url) ||
this.$store.state.users.currentUser.profile_image_url.includes(baseAvatar)
},
isDefaultBanner () {
const baseBanner = this.$store.state.instance.defaultBanner
return !(this.$store.state.users.currentUser.cover_photo) ||
this.$store.state.users.currentUser.cover_photo.includes(baseBanner)
},
isDefaultBackground () {
return !(this.$store.state.users.currentUser.background_image)
},
avatarImgSrc () {
const src = this.$store.state.users.currentUser.profile_image_url_original
return (!src) ? this.defaultAvatar : src
},
fieldsLimits () {
return this.$store.state.instance.fieldsLimits
},
@ -303,6 +333,52 @@ export default {
},
// Editable stuff
changeAvatar () {
this.editImage = 'avatar'
},
changeBanner () {
this.editImage = 'banner'
},
resetAvatar () {
const confirmed = window.confirm(this.$t('settings.reset_avatar_confirm'))
if (confirmed) {
this.submitAvatar(undefined, '')
}
},
resetBanner () {
const confirmed = window.confirm(this.$t('settings.reset_banner_confirm'))
if (confirmed) {
this.submitBanner('')
}
},
resetBackground () {
const confirmed = window.confirm(this.$t('settings.reset_background_confirm'))
if (confirmed) {
this.submitBackground('')
}
},
submitImage ({ canvas, file }) {
const reqData = {}
if (this.editImage === 'avatar') {
if (canvas) {
return canvas.toBlob((data) => this.submitImage({ canvas: null, file: data }))
}
reqData.avatar = file
reqData.avatarName = file.name
} else {
reqData.banner = file
}
return this.$store.state.api.backendInteractor.updateProfileImages(reqData)
.then((user) => {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
this.editImage = false
})
.catch((error) => {
this.displayUploadError(error)
})
},
addField () {
if (this.newFields.length < this.maxFields) {
this.newFields.push({ name: '', value: '' })
@ -316,6 +392,9 @@ export default {
propsToNative (props) {
return propsToNative(props)
},
cancelImageText () {
return
},
updateProfile () {
const params = {
note: this.newBio,

View file

@ -21,6 +21,22 @@
padding-bottom: 0;
}
&-bio {
text-align: center;
color: var(--lightText);
display: block;
line-height: 1.3;
padding: 0.6em;
margin: 0 0.6em;
img {
object-fit: contain;
vertical-align: middle;
max-width: 100%;
max-height: 400px;
}
}
.user-card-bio {
margin: 0.6em;
@ -101,22 +117,6 @@
z-index: -2;
}
&-bio {
text-align: center;
color: var(--lightText);
display: block;
line-height: 1.3;
padding: 0.6em;
margin: 0 0.6em;
img {
object-fit: contain;
vertical-align: middle;
max-width: 100%;
max-height: 400px;
}
}
&.-rounded-t {
border-top-left-radius: var(--roundness);
border-top-right-radius: var(--roundness);
@ -535,3 +535,44 @@
}
}
}
.edit-image {
.panel-body {
text-align: center;
}
.current-avatar {
object-fit: contain;
}
.images-container {
display: grid;
margin: 1em;
grid-template-columns: 1fr 5em 1fr;
grid-template-rows: 20em;
gap: 0.5em;
.new-image {
display: flex;
flex-direction: column;
}
.cropper {
flex: 1;
}
> * {
flex: 1 0 10em;
max-height: 100%;
aspect-ratio: 1;
}
.separator {
min-width: 1.1em;
font-size: 500%;
align-self: center;
flex: 0 1 5em;
aspect-ratio: unset;
}
}
}

View file

@ -31,7 +31,7 @@
v-else-if="editable"
class="user-info-avatar button-unstyled -link"
:class="{ '-editable': editable }"
@click="editAvatar"
@click="changeAvatar"
>
<UserAvatar :user="user" />
<div class="user-info-avatar -link -overlay">
@ -63,7 +63,7 @@
v-if="editable"
:disabled="newName && newName.length === 0"
class="btn button-unstyled edit-banner-button"
@click="updateProfile"
@click="changeBanner"
>
{{ $t('settings.change_banner') }}
<FAIcon
@ -591,6 +591,85 @@
:is-mute="true"
/>
</teleport>
<teleport to="#modal">
<Modal
v-if="editImage"
:is-mute="true"
class="edit-image"
@backdrop-clicked="editImage = false"
>
<div class="panel">
<div class="panel-heading">
<h1 class="title">
{{ editImage === 'avatar' ? $t('settings.change_avatar') : $t('settings.change_banner') }}
</h1>
</div>
<div class="panel-body">
<div class="images-container">
<img
:src="editImage === 'avatar' ? user.profile_image_url_original : newBanner"
class="current-avatar"
/>
<FAIcon
class="separator"
icon="chevron-right"
/>
<image-cropper
ref="cropper"
class="cropper"
@submit="submitImage"
/>
</div>
<button
id="pick-image"
class="button-default btn"
type="button"
@click="() => this.$refs.cropper.pickImage()"
>
{{ $t('settings.upload_picture') }}
</button>
<p class="visibility-notice">
{{ $t('settings.avatar_size_instruction') }}
</p>
<button
:title="editImage === 'avatar' ? $t('settings.reset_avatar') : $t('settings.reset_banner')"
class="button-unstyled reset-button"
@click="resetImage"
>
</button>
<div class="panel-footer">
<div/>
<button
class="button-default btn"
type="button"
@click="destroy"
>
{{ this.$t('image_cropper.cancel') }}
</button>
<button
class="button-default btn"
type="button"
@click="this.$refs.cropper.submit(true)"
>
{{ $t('image_cropper.save') }}
</button>
<button
class="button-default btn"
type="button"
@click="this.$refs.cropper.submit(false)"
>
{{ $t('image_cropper.save_without_cropping') }}
</button>
<FAIcon
v-if="submitting"
spin
icon="circle-notch"
/>
</div>
</div>
</div>
</Modal>
</teleport>
</div>
</template>