Client: Add ability to update video channel avatar

pull/758/head
Chocobozzz 2018-06-29 14:34:04 +02:00
parent 4bbfc6c606
commit 52d9f792b3
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
19 changed files with 207 additions and 108 deletions

View File

@ -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 }}

View File

@ -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;

View File

@ -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(',')
}
}

View File

@ -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>

View File

@ -5,6 +5,11 @@
margin-bottom: 20px;
}
my-actor-avatar-info {
display: block;
margin-bottom: 20px;
}
input[type=text] {
@include peertube-input-text(340px);

View File

@ -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
}

View File

@ -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: [

View File

@ -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>

View File

@ -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;
}

View File

@ -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(',')
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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(

View File

@ -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)

View File

@ -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) {

View File

@ -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>

View File

@ -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;
}
}

View File

@ -251,6 +251,10 @@ export enum ScopeNames {
attributes: [ 'host' ],
model: () => ServerModel.unscoped(),
required: false
},
{
model: () => AvatarModel.unscoped(),
required: false
}
]
},

View File

@ -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