-<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 }}
@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;
styleUrls: [ './my-account-settings.component.scss' ]
})
export class MyAccountSettingsComponent implements OnInit {
- @ViewChild('avatarfileInput') avatarfileInput
-
user: User = null
userVideoQuota = '0'
userVideoQuotaUsed = 0
.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 => {
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(',')
- }
}
+<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>
margin-bottom: 20px;
}
+my-actor-avatar-info {
+ display: block;
+ margin-bottom: 20px;
+}
+
input[type=text] {
@include peertube-input-text(340px);
-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'
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'
styleUrls: [ './my-account-video-channel-edit.component.scss' ]
})
export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy {
+ @ViewChild('avatarfileInput') avatarfileInput
+
error: string
private videoChannelToUpdate: VideoChannel
private router: Router,
private route: ActivatedRoute,
private videoChannelService: VideoChannelService,
- private i18n: I18n
+ private i18n: I18n,
+ private serverService: ServerService
) {
super()
}
)
}
+ 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
}
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: [
MyAccountVideosComponent,
MyAccountVideoChannelsComponent,
MyAccountVideoChannelCreateComponent,
- MyAccountVideoChannelUpdateComponent
+ MyAccountVideoChannelUpdateComponent,
+ ActorAvatarInfoComponent
],
exports: [
--- /dev/null
+<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>
\ No newline at end of file
--- /dev/null
+@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;
+}
\ No newline at end of file
--- /dev/null
+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(',')
+ }
+}
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)
}
}
account: Account
videoChannels: VideoChannel[]
createdAt: Date
- accountAvatarUrl: string
constructor (hash: UserConstructorHash) {
this.id = hash.id
if (hash.createdAt !== undefined) {
this.createdAt = hash.createdAt
}
+ }
+
+ get accountAvatarUrl () {
+ if (!this.account) return ''
- this.updateComputedAttributes()
+ return this.account.avatarUrl
}
hasRight (right: UserRight) {
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)
}
}
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 {
)
}
+ 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(
export class Video implements VideoServerModel {
by: string
accountAvatarUrl: string
+ videoChannelAvatarUrl: string
createdAt: Date
updatedAt: Date
publishedAt: Date
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)
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) {
<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>
&:hover {
opacity: 0.8;
}
+
+ img {
+ @include avatar(18px);
+
+ margin: -2px 2px 0 5px;
+ }
}
my-help {
img {
@include avatar(18px);
+ margin-top: -2px;
margin-left: 7px;
}
}
attributes: [ 'host' ],
model: () => ServerModel.unscoped(),
required: false
+ },
+ {
+ model: () => AvatarModel.unscoped(),
+ required: false
}
]
},
killallServers,
makeGetRequest,
makePostBodyRequest,
- makePutBodyRequest, makeUploadRequest,
+ makePutBodyRequest,
+ makeUploadRequest,
runServer,
ServerInfo,
setAccessTokensToServers,
} 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