mirror of https://github.com/Chocobozzz/PeerTube
Client: Add ability to update video channel avatar
parent
4bbfc6c606
commit
52d9f792b3
|
@ -1,20 +1,4 @@
|
|||
<div class="user">
|
||||
<img [src]="user.accountAvatarUrl" alt="Avatar" />
|
||||
|
||||
<div class="user-info">
|
||||
<div class="user-info-names">
|
||||
<div class="user-info-display-name">{{ user.account?.displayName }}</div>
|
||||
<div class="user-info-username">{{ user.username }}</div>
|
||||
</div>
|
||||
<div i18n class="user-info-followers">{{ user.account?.followersCount }} subscribers</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-file">
|
||||
<span i18n>Change your avatar</span>
|
||||
<input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="changeAvatar()" />
|
||||
</div>
|
||||
<div i18n class="file-max-size">(extensions: {{ avatarExtensions }}, max size: {{ maxAvatarSize | bytes }})</div>
|
||||
<my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)"></my-actor-avatar-info>
|
||||
|
||||
<div class="user-quota">
|
||||
<span i18n class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ userVideoQuota }}
|
||||
|
|
|
@ -1,55 +1,6 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
@include avatar(50px);
|
||||
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
.user-info-names {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.user-info-display-name {
|
||||
font-size: 20px;
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
.user-info-username {
|
||||
margin-left: 7px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: 14px;
|
||||
color: #777272;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info-followers {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-file {
|
||||
@include peertube-button-file(160px);
|
||||
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.file-max-size {
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
.user-quota {
|
||||
font-size: 15px;
|
||||
margin-top: 20px;
|
||||
|
|
|
@ -13,8 +13,6 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
|
|||
styleUrls: [ './my-account-settings.component.scss' ]
|
||||
})
|
||||
export class MyAccountSettingsComponent implements OnInit {
|
||||
@ViewChild('avatarfileInput') avatarfileInput
|
||||
|
||||
user: User = null
|
||||
userVideoQuota = '0'
|
||||
userVideoQuotaUsed = 0
|
||||
|
@ -48,16 +46,7 @@ export class MyAccountSettingsComponent implements OnInit {
|
|||
.subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
|
||||
}
|
||||
|
||||
changeAvatar () {
|
||||
const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ]
|
||||
if (avatarfile.size > this.maxAvatarSize) {
|
||||
this.notificationsService.error('Error', 'This image is too large.')
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('avatarfile', avatarfile)
|
||||
|
||||
onAvatarChange (formData: FormData) {
|
||||
this.userService.changeAvatar(formData)
|
||||
.subscribe(
|
||||
data => {
|
||||
|
@ -69,12 +58,4 @@ export class MyAccountSettingsComponent implements OnInit {
|
|||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
}
|
||||
|
||||
get maxAvatarSize () {
|
||||
return this.serverService.getConfig().avatar.file.size.max
|
||||
}
|
||||
|
||||
get avatarExtensions () {
|
||||
return this.serverService.getConfig().avatar.file.extensions.join(',')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<my-actor-avatar-info
|
||||
*ngIf="isCreation() === false && videoChannelToUpdate"
|
||||
[actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)"
|
||||
></my-actor-avatar-info>
|
||||
|
||||
<div i18n class="form-sub-title" *ngIf="isCreation() === true">Create a video channel</div>
|
||||
<div i18n class="form-sub-title" *ngIf="isCreation() === false">Update {{ videoChannel?.displayName }}</div>
|
||||
|
||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
my-actor-avatar-info {
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
@include peertube-input-text(340px);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit'
|
||||
|
@ -6,7 +6,7 @@ import { VideoChannelUpdate } from '../../../../../shared/models/videos'
|
|||
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||
import { AuthService } from '@app/core'
|
||||
import { AuthService, ServerService } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||
import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service'
|
||||
|
@ -17,6 +17,8 @@ import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators
|
|||
styleUrls: [ './my-account-video-channel-edit.component.scss' ]
|
||||
})
|
||||
export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy {
|
||||
@ViewChild('avatarfileInput') avatarfileInput
|
||||
|
||||
error: string
|
||||
|
||||
private videoChannelToUpdate: VideoChannel
|
||||
|
@ -30,7 +32,8 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
|
|||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private videoChannelService: VideoChannelService,
|
||||
private i18n: I18n
|
||||
private i18n: I18n,
|
||||
private serverService: ServerService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
@ -89,6 +92,27 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
|
|||
)
|
||||
}
|
||||
|
||||
onAvatarChange (formData: FormData) {
|
||||
this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.uuid, formData)
|
||||
.subscribe(
|
||||
data => {
|
||||
this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.'))
|
||||
|
||||
this.videoChannelToUpdate.updateAvatar(data.avatar)
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
}
|
||||
|
||||
get maxAvatarSize () {
|
||||
return this.serverService.getConfig().avatar.file.size.max
|
||||
}
|
||||
|
||||
get avatarExtensions () {
|
||||
return this.serverService.getConfig().avatar.file.extensions.join(',')
|
||||
}
|
||||
|
||||
isCreation () {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { MyAccountProfileComponent } from '@app/+my-account/my-account-settings/
|
|||
import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channels.component'
|
||||
import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component'
|
||||
import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component'
|
||||
import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -26,7 +27,8 @@ import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-accoun
|
|||
MyAccountVideosComponent,
|
||||
MyAccountVideoChannelsComponent,
|
||||
MyAccountVideoChannelCreateComponent,
|
||||
MyAccountVideoChannelUpdateComponent
|
||||
MyAccountVideoChannelUpdateComponent,
|
||||
ActorAvatarInfoComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<ng-container *ngIf="actor">
|
||||
<div class="actor">
|
||||
<img [src]="actor.avatarUrl" alt="Avatar" />
|
||||
|
||||
<div class="actor-info">
|
||||
<div class="actor-info-names">
|
||||
<div class="actor-info-display-name">{{ actor.displayName }}</div>
|
||||
<div class="actor-info-username">{{ actor.name }}</div>
|
||||
</div>
|
||||
<div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-file">
|
||||
<span i18n>Change the avatar</span>
|
||||
<input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange()" />
|
||||
</div>
|
||||
<div i18n class="file-max-size">(extensions: {{ avatarExtensions }}, max size: {{ maxAvatarSize | bytes }})</div>
|
||||
</ng-container>
|
|
@ -0,0 +1,51 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.actor {
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
@include avatar(50px);
|
||||
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.actor-info {
|
||||
.actor-info-names {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.actor-info-display-name {
|
||||
font-size: 20px;
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
.actor-info-username {
|
||||
margin-left: 7px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: 14px;
|
||||
color: #777272;
|
||||
}
|
||||
}
|
||||
|
||||
.actor-info-followers {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-file {
|
||||
@include peertube-button-file(160px);
|
||||
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.file-max-size {
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
|
||||
import { AuthService } from '../../core'
|
||||
import { ServerService } from '../../core/server'
|
||||
import { UserService } from '../../shared/users'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||
import { Account } from '@app/shared/account/account.model'
|
||||
|
||||
@Component({
|
||||
selector: 'my-actor-avatar-info',
|
||||
templateUrl: './actor-avatar-info.component.html',
|
||||
styleUrls: [ './actor-avatar-info.component.scss' ]
|
||||
})
|
||||
export class ActorAvatarInfoComponent {
|
||||
@ViewChild('avatarfileInput') avatarfileInput
|
||||
|
||||
@Input() actor: VideoChannel | Account
|
||||
|
||||
@Output() avatarChange = new EventEmitter<FormData>()
|
||||
|
||||
constructor (
|
||||
private userService: UserService,
|
||||
private authService: AuthService,
|
||||
private serverService: ServerService,
|
||||
private notificationsService: NotificationsService
|
||||
) {}
|
||||
|
||||
onAvatarChange () {
|
||||
const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ]
|
||||
if (avatarfile.size > this.maxAvatarSize) {
|
||||
this.notificationsService.error('Error', 'This image is too large.')
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('avatarfile', avatarfile)
|
||||
|
||||
this.avatarChange.emit(formData)
|
||||
}
|
||||
|
||||
get maxAvatarSize () {
|
||||
return this.serverService.getConfig().avatar.file.size.max
|
||||
}
|
||||
|
||||
get avatarExtensions () {
|
||||
return this.serverService.getConfig().avatar.file.extensions.join(',')
|
||||
}
|
||||
}
|
|
@ -45,6 +45,16 @@ export abstract class Actor implements ActorServer {
|
|||
this.updatedAt = new Date(hash.updatedAt.toString())
|
||||
this.avatar = hash.avatar
|
||||
|
||||
this.updateComputedAttributes()
|
||||
}
|
||||
|
||||
updateAvatar (newAvatar: Avatar) {
|
||||
this.avatar = newAvatar
|
||||
|
||||
this.updateComputedAttributes()
|
||||
}
|
||||
|
||||
private updateComputedAttributes () {
|
||||
this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ export class User implements UserServerModel {
|
|||
account: Account
|
||||
videoChannels: VideoChannel[]
|
||||
createdAt: Date
|
||||
accountAvatarUrl: string
|
||||
|
||||
constructor (hash: UserConstructorHash) {
|
||||
this.id = hash.id
|
||||
|
@ -65,8 +64,12 @@ export class User implements UserServerModel {
|
|||
if (hash.createdAt !== undefined) {
|
||||
this.createdAt = hash.createdAt
|
||||
}
|
||||
}
|
||||
|
||||
this.updateComputedAttributes()
|
||||
get accountAvatarUrl () {
|
||||
if (!this.account) return ''
|
||||
|
||||
return this.account.avatarUrl
|
||||
}
|
||||
|
||||
hasRight (right: UserRight) {
|
||||
|
@ -81,17 +84,9 @@ export class User implements UserServerModel {
|
|||
if (obj.account !== undefined) {
|
||||
this.account = new Account(obj.account)
|
||||
}
|
||||
|
||||
this.updateComputedAttributes()
|
||||
}
|
||||
|
||||
updateAccountAvatar (newAccountAvatar: Avatar) {
|
||||
this.account.avatar = newAccountAvatar
|
||||
|
||||
this.updateComputedAttributes()
|
||||
}
|
||||
|
||||
private updateComputedAttributes () {
|
||||
this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account)
|
||||
this.account.updateAvatar(newAccountAvatar)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ResultList } from '../../../../../shared'
|
|||
import { VideoChannel } from './video-channel.model'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { Account } from '@app/shared/account/account.model'
|
||||
import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
|
||||
|
||||
@Injectable()
|
||||
export class VideoChannelService {
|
||||
|
@ -54,6 +55,13 @@ export class VideoChannelService {
|
|||
)
|
||||
}
|
||||
|
||||
changeVideoChannelAvatar (videoChannelUUID: string, avatarForm: FormData) {
|
||||
const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelUUID + '/avatar/pick'
|
||||
|
||||
return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm)
|
||||
.pipe(catchError(this.restExtractor.handleError))
|
||||
}
|
||||
|
||||
removeVideoChannel (videoChannel: VideoChannel) {
|
||||
return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.uuid)
|
||||
.pipe(
|
||||
|
|
|
@ -11,6 +11,7 @@ import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-s
|
|||
export class Video implements VideoServerModel {
|
||||
by: string
|
||||
accountAvatarUrl: string
|
||||
videoChannelAvatarUrl: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
publishedAt: Date
|
||||
|
@ -102,9 +103,11 @@ export class Video implements VideoServerModel {
|
|||
this.dislikes = hash.dislikes
|
||||
this.nsfw = hash.nsfw
|
||||
this.account = hash.account
|
||||
this.channel = hash.channel
|
||||
|
||||
this.by = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host)
|
||||
this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account)
|
||||
this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.channel)
|
||||
|
||||
this.category.label = peertubeTranslate(this.category.label, translations)
|
||||
this.licence.label = peertubeTranslate(this.licence.label, translations)
|
||||
|
|
|
@ -132,10 +132,10 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
|
|||
if (!videofile) return
|
||||
|
||||
// Cannot upload videos > 4GB for now
|
||||
if (videofile.size > 4 * 1024 * 1024 * 1024) {
|
||||
this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 4GB'))
|
||||
return
|
||||
}
|
||||
// if (videofile.size > 4 * 1024 * 1024 * 1024) {
|
||||
// this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 4GB'))
|
||||
// return
|
||||
// }
|
||||
|
||||
const videoQuota = this.authService.getUser().videoQuota
|
||||
if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
<div class="video-info-channel">
|
||||
<a [routerLink]="[ '/video-channels', video.channel.id ]" i18n-title title="Go the channel page">
|
||||
{{ video.channel.displayName }}
|
||||
|
||||
<img [src]="video.videoChannelAvatarUrl" alt="Video channel avatar" />
|
||||
</a>
|
||||
<!-- Here will be the subscribe button -->
|
||||
<my-help helpType="custom" i18n-customHtml customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.name}}@{{video.account.host}}</strong> and subscribe there. Subscription as a PeerTube user is being worked on in <a href='https://github.com/Chocobozzz/PeerTube/issues/470'>#470</a>."></my-help>
|
||||
|
|
|
@ -84,6 +84,12 @@
|
|||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
img {
|
||||
@include avatar(18px);
|
||||
|
||||
margin: -2px 2px 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
my-help {
|
||||
|
@ -106,6 +112,7 @@
|
|||
img {
|
||||
@include avatar(18px);
|
||||
|
||||
margin-top: -2px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -251,6 +251,10 @@ export enum ScopeNames {
|
|||
attributes: [ 'host' ],
|
||||
model: () => ServerModel.unscoped(),
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: () => AvatarModel.unscoped(),
|
||||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -14,7 +14,8 @@ import {
|
|||
killallServers,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
makePutBodyRequest, makeUploadRequest,
|
||||
makePutBodyRequest,
|
||||
makeUploadRequest,
|
||||
runServer,
|
||||
ServerInfo,
|
||||
setAccessTokensToServers,
|
||||
|
@ -22,7 +23,7 @@ import {
|
|||
} from '../../utils'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
|
||||
import { User } from '../../../../shared/models/users'
|
||||
import { join } from "path"
|
||||
import { join } from 'path'
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
|
|
Loading…
Reference in New Issue