+++ /dev/null
-import { NgModule } from '@angular/core'
-import { RouterModule, Routes } from '@angular/router'
-
-import { MetaGuard } from '@ngx-meta/core'
-
-import { LoginGuard } from '../core'
-import { AccountComponent } from './account.component'
-import { AccountSettingsComponent } from './account-settings/account-settings.component'
-import { AccountVideosComponent } from './account-videos/account-videos.component'
-
-const accountRoutes: Routes = [
- {
- path: 'account',
- component: AccountComponent,
- canActivateChild: [ MetaGuard, LoginGuard ],
- children: [
- {
- path: 'settings',
- component: AccountSettingsComponent,
- data: {
- meta: {
- title: 'Account settings'
- }
- }
- },
- {
- path: 'videos',
- component: AccountVideosComponent,
- data: {
- meta: {
- title: 'Account videos'
- }
- }
- }
- ]
- }
-]
-
-@NgModule({
- imports: [ RouterModule.forChild(accountRoutes) ],
- exports: [ RouterModule ]
-})
-export class AccountRoutingModule {}
+++ /dev/null
-<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
-
-<form role="form" (ngSubmit)="changePassword()" [formGroup]="form">
-
- <label for="new-password">Change password</label>
- <input
- type="password" id="new-password" placeholder="New password"
- formControlName="new-password" [ngClass]="{ 'input-error': formErrors['new-password'] }"
- >
- <div *ngIf="formErrors['new-password']" class="form-error">
- {{ formErrors['new-password'] }}
- </div>
-
- <input
- type="password" id="new-confirmed-password" placeholder="Confirm new password"
- formControlName="new-confirmed-password"
- >
-
- <input type="submit" value="Change password" [disabled]="!form.valid">
-</form>
+++ /dev/null
-@import '_variables';
-@import '_mixins';
-
-input[type=password] {
- @include peertube-input-text(340px);
- display: block;
-
- &#new-confirmed-password {
- margin-top: 15px;
- }
-}
-
-input[type=submit] {
- @include peertube-button;
- @include orange-button;
-
- margin-top: 15px;
-}
-
+++ /dev/null
-import { Component, OnInit } from '@angular/core'
-import { FormBuilder, FormGroup } from '@angular/forms'
-import { NotificationsService } from 'angular2-notifications'
-import { FormReactive, USER_PASSWORD, UserService } from '../../../shared'
-
-@Component({
- selector: 'my-account-change-password',
- templateUrl: './account-change-password.component.html',
- styleUrls: [ './account-change-password.component.scss' ]
-})
-export class AccountChangePasswordComponent extends FormReactive implements OnInit {
- error: string = null
-
- form: FormGroup
- formErrors = {
- 'new-password': '',
- 'new-confirmed-password': ''
- }
- validationMessages = {
- 'new-password': USER_PASSWORD.MESSAGES,
- 'new-confirmed-password': USER_PASSWORD.MESSAGES
- }
-
- constructor (
- private formBuilder: FormBuilder,
- private notificationsService: NotificationsService,
- private userService: UserService
- ) {
- super()
- }
-
- buildForm () {
- this.form = this.formBuilder.group({
- 'new-password': [ '', USER_PASSWORD.VALIDATORS ],
- 'new-confirmed-password': [ '', USER_PASSWORD.VALIDATORS ]
- })
-
- this.form.valueChanges.subscribe(data => this.onValueChanged(data))
- }
-
- ngOnInit () {
- this.buildForm()
- }
-
- changePassword () {
- const newPassword = this.form.value['new-password']
- const newConfirmedPassword = this.form.value['new-confirmed-password']
-
- this.error = null
-
- if (newPassword !== newConfirmedPassword) {
- this.error = 'The new password and the confirmed password do not correspond.'
- return
- }
-
- this.userService.changePassword(newPassword).subscribe(
- () => this.notificationsService.success('Success', 'Password updated.'),
-
- err => this.error = err.message
- )
- }
-}
+++ /dev/null
-export * from './account-change-password.component'
+++ /dev/null
-<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
- <div class="form-group">
- <label for="nsfwPolicy">Default policy on videos containing sensitive content</label>
- <my-help helpType="custom" customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."></my-help>
-
- <div class="peertube-select-container">
- <select id="nsfwPolicy" formControlName="nsfwPolicy">
- <option value="do_not_list">Do not list</option>
- <option value="blur">Blur thumbnails</option>
- <option value="display">Display</option>
- </select>
- </div>
- </div>
-
- <div class="form-group">
- <input
- type="checkbox" id="autoPlayVideo"
- formControlName="autoPlayVideo"
- >
- <label for="autoPlayVideo"></label>
- <label for="autoPlayVideo">Automatically plays video</label>
- </div>
-
- <input type="submit" value="Save" [disabled]="!form.valid">
-</form>
+++ /dev/null
-@import '_variables';
-@import '_mixins';
-
-input[type=checkbox] {
- @include peertube-checkbox(1px);
-}
-
-input[type=submit] {
- @include peertube-button;
- @include orange-button;
-
- display: block;
- margin-top: 15px;
-}
-
-.peertube-select-container {
- @include peertube-select-container(340px);
-
- margin-bottom: 30px;
-}
\ No newline at end of file
+++ /dev/null
-import { Component, Input, OnInit } from '@angular/core'
-import { FormBuilder, FormGroup } from '@angular/forms'
-import { NotificationsService } from 'angular2-notifications'
-import { UserUpdateMe } from '../../../../../../shared'
-import { AuthService } from '../../../core'
-import { FormReactive, User, UserService } from '../../../shared'
-
-@Component({
- selector: 'my-account-details',
- templateUrl: './account-details.component.html',
- styleUrls: [ './account-details.component.scss' ]
-})
-
-export class AccountDetailsComponent extends FormReactive implements OnInit {
- @Input() user: User = null
-
- form: FormGroup
- formErrors = {}
- validationMessages = {}
-
- constructor (
- private authService: AuthService,
- private formBuilder: FormBuilder,
- private notificationsService: NotificationsService,
- private userService: UserService
- ) {
- super()
- }
-
- buildForm () {
- this.form = this.formBuilder.group({
- nsfwPolicy: [ this.user.nsfwPolicy ],
- autoPlayVideo: [ this.user.autoPlayVideo ]
- })
-
- this.form.valueChanges.subscribe(data => this.onValueChanged(data))
- }
-
- ngOnInit () {
- this.buildForm()
- }
-
- updateDetails () {
- const nsfwPolicy = this.form.value['nsfwPolicy']
- const autoPlayVideo = this.form.value['autoPlayVideo']
- const details: UserUpdateMe = {
- nsfwPolicy,
- autoPlayVideo
- }
-
- this.userService.updateMyDetails(details).subscribe(
- () => {
- this.notificationsService.success('Success', 'Information updated.')
-
- this.authService.refreshUserInformation()
- },
-
- err => this.notificationsService.error('Error', err.message)
- )
- }
-}
+++ /dev/null
-export * from './account-details.component'
+++ /dev/null
-<div class="user">
- <img [src]="getAvatarUrl()" alt="Avatar" />
-
- <div class="user-info">
- <div class="user-info-username">{{ user.username }}</div>
- <div class="user-info-followers">{{ user.account?.followersCount }} subscribers</div>
- </div>
-</div>
-
-<div class="button-file">
- <span>Change your avatar</span>
- <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="changeAvatar()" />
-</div>
-<div class="file-max-size">(extensions: {{ avatarExtensions }}, max size: {{ maxAvatarSize | bytes }})</div>
-
-<div class="user-quota">
- <span class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ userVideoQuota }}
-</div>
-
-<div class="account-title">Account settings</div>
-<my-account-change-password></my-account-change-password>
-
-<div class="account-title">Video settings</div>
-<my-account-details [user]="user"></my-account-details>
+++ /dev/null
-@import '_variables';
-@import '_mixins';
-
-.user {
- display: flex;
-
- img {
- @include avatar(50px);
-
- margin-right: 15px;
- }
-
- .user-info {
- .user-info-username {
- font-size: 20px;
- font-weight: $font-bold;
- }
-
- .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;
-
- .user-quota-label {
- font-weight: $font-semibold;
- }
-}
-
-.account-title {
- text-transform: uppercase;
- color: $orange-color;
- font-weight: $font-bold;
- font-size: 13px;
- margin-top: 55px;
- margin-bottom: 30px;
-}
+++ /dev/null
-import { Component, OnInit, ViewChild } from '@angular/core'
-import { NotificationsService } from 'angular2-notifications'
-import { BytesPipe } from 'ngx-pipes'
-import { AuthService } from '../../core'
-import { ServerService } from '../../core/server'
-import { User } from '../../shared'
-import { UserService } from '../../shared/users'
-
-@Component({
- selector: 'my-account-settings',
- templateUrl: './account-settings.component.html',
- styleUrls: [ './account-settings.component.scss' ]
-})
-export class AccountSettingsComponent implements OnInit {
- @ViewChild('avatarfileInput') avatarfileInput
-
- user: User = null
- userVideoQuota = '0'
- userVideoQuotaUsed = 0
-
- constructor (
- private userService: UserService,
- private authService: AuthService,
- private serverService: ServerService,
- private notificationsService: NotificationsService
- ) {}
-
- ngOnInit () {
- this.user = this.authService.getUser()
-
- this.authService.userInformationLoaded.subscribe(
- () => {
- if (this.user.videoQuota !== -1) {
- this.userVideoQuota = new BytesPipe().transform(this.user.videoQuota, 0).toString()
- } else {
- this.userVideoQuota = 'Unlimited'
- }
- }
- )
-
- this.userService.getMyVideoQuotaUsed()
- .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
- }
-
- getAvatarUrl () {
- return this.user.getAvatarUrl()
- }
-
- changeAvatar () {
- const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ]
-
- const formData = new FormData()
- formData.append('avatarfile', avatarfile)
-
- this.userService.changeAvatar(formData)
- .subscribe(
- data => {
- this.notificationsService.success('Success', 'Avatar changed.')
-
- this.user.account.avatar = data.avatar
- },
-
- err => this.notificationsService.error('Error', err.message)
- )
- }
-
- get maxAvatarSize () {
- return this.serverService.getConfig().avatar.file.size.max
- }
-
- get avatarExtensions () {
- return this.serverService.getConfig().avatar.file.extensions.join(',')
- }
-}
+++ /dev/null
-<div *ngIf="pagination.totalItems === 0">No results.</div>
-
-<div
- myInfiniteScroller
- [pageHeight]="pageHeight"
- (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
- class="videos" #videosElement
->
- <div *ngFor="let videos of videoPages; let i = index" class="videos-page">
- <div class="video" *ngFor="let video of videos; let j = index">
- <div class="checkbox-container">
- <input [id]="'video-check-' + video.id" type="checkbox" [(ngModel)]="checkedVideos[video.id]" />
- <label [for]="'video-check-' + video.id"></label>
- </div>
-
- <my-video-thumbnail [video]="video"></my-video-thumbnail>
-
- <div class="video-info">
- <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
- <span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
- <div class="video-info-private">{{ video.privacy.label }}</div>
- </div>
-
- <!-- Display only once -->
- <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
- <div class="action-selection-mode-child">
- <span class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
- Cancel
- </span>
-
- <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
- <span class="icon icon-delete-white"></span>
- Delete
- </span>
- </div>
- </div>
-
- <div class="video-buttons" *ngIf="isInSelectionMode() === false">
- <my-delete-button (click)="deleteVideo(video)"></my-delete-button>
-
- <my-edit-button [routerLink]="[ '/videos', 'edit', video.uuid ]"></my-edit-button>
- </div>
- </div>
- </div>
-</div>
+++ /dev/null
-@import '_variables';
-@import '_mixins';
-
-.action-selection-mode {
- width: 174px;
- display: flex;
- justify-content: flex-end;
-
- .action-selection-mode-child {
- position: fixed;
-
- .action-button {
- display: inline-block;
- }
-
- .action-button-cancel-selection {
- @include peertube-button;
- @include grey-button;
-
- margin-right: 10px;
- }
-
- .action-button-delete-selection {
- @include peertube-button;
- @include orange-button;
- }
-
- .icon.icon-delete-white {
- @include icon(21px);
-
- position: relative;
- top: -2px;
- background-image: url('../../../assets/images/global/delete-white.svg');
- }
- }
-}
-
-/deep/ .action-button {
- &.action-button-delete {
- margin-right: 10px;
- }
-}
-
-.video {
- display: flex;
- min-height: 130px;
- padding-bottom: 20px;
- margin-bottom: 20px;
- border-bottom: 1px solid #C6C6C6;
-
- &:first-child {
- margin-top: 47px;
- }
-
- .checkbox-container {
- display: flex;
- align-items: center;
- margin-right: 20px;
- margin-left: 12px;
-
- input[type=checkbox] {
- @include peertube-checkbox(2px);
- }
- }
-
- my-video-thumbnail {
- margin-right: 10px;
- }
-
- .video-info {
- flex-grow: 1;
-
- .video-info-name {
- @include disable-default-a-behaviour;
-
- color: #000;
- display: block;
- font-size: 16px;
- font-weight: $font-semibold;
- }
-
- .video-info-date-views, .video-info-private {
- font-size: 13px;
-
- &.video-info-private {
- font-weight: $font-semibold;
- }
- }
- }
-
- .video-buttons {
- min-width: 190px;
- }
-}
-
-@media screen and (max-width: 800px) {
- .video {
- flex-direction: column;
- height: auto;
- text-align: center;
-
- input[type=checkbox] {
- display: none;
- }
-
- my-video-thumbnail {
- margin-right: 0;
- }
-
- .video-buttons {
- margin-top: 10px;
- }
- }
-}
+++ /dev/null
-import { Component, OnInit, OnDestroy } from '@angular/core'
-import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
-import { immutableAssign } from '@app/shared/misc/utils'
-import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
-import { NotificationsService } from 'angular2-notifications'
-import 'rxjs/add/observable/from'
-import 'rxjs/add/operator/concatAll'
-import { Observable } from 'rxjs/Observable'
-import { AuthService } from '../../core/auth'
-import { ConfirmService } from '../../core/confirm'
-import { AbstractVideoList } from '../../shared/video/abstract-video-list'
-import { Video } from '../../shared/video/video.model'
-import { VideoService } from '../../shared/video/video.service'
-
-@Component({
- selector: 'my-account-videos',
- templateUrl: './account-videos.component.html',
- styleUrls: [ './account-videos.component.scss' ]
-})
-export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
- titlePage = 'My videos'
- currentRoute = '/account/videos'
- checkedVideos: { [ id: number ]: boolean } = {}
- pagination: ComponentPagination = {
- currentPage: 1,
- itemsPerPage: 5,
- totalItems: null
- }
-
- protected baseVideoWidth = -1
- protected baseVideoHeight = 155
-
- constructor (protected router: Router,
- protected route: ActivatedRoute,
- protected authService: AuthService,
- protected notificationsService: NotificationsService,
- protected confirmService: ConfirmService,
- protected location: Location,
- private videoService: VideoService) {
- super()
- }
-
- ngOnInit () {
- super.ngOnInit()
-
- // this.generateSyndicationList()
- }
-
- ngOnDestroy () {
- super.ngOnDestroy()
- }
-
- abortSelectionMode () {
- this.checkedVideos = {}
- }
-
- isInSelectionMode () {
- return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
- }
-
- getVideosObservable (page: number) {
- const newPagination = immutableAssign(this.pagination, { currentPage: page })
-
- return this.videoService.getMyVideos(newPagination, this.sort)
- }
-
- generateSyndicationList () {
- throw new Error('Method not implemented.')
- }
-
- async deleteSelectedVideos () {
- const toDeleteVideosIds = Object.keys(this.checkedVideos)
- .filter(k => this.checkedVideos[k] === true)
- .map(k => parseInt(k, 10))
-
- const res = await this.confirmService.confirm(`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 'Delete')
- if (res === false) return
-
- const observables: Observable<any>[] = []
- for (const videoId of toDeleteVideosIds) {
- const o = this.videoService
- .removeVideo(videoId)
- .do(() => this.spliceVideosById(videoId))
-
- observables.push(o)
- }
-
- Observable.from(observables)
- .concatAll()
- .subscribe(
- res => {
- this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`)
- this.buildVideoPages()
- },
-
- err => this.notificationsService.error('Error', err.message)
- )
- }
-
- async deleteVideo (video: Video) {
- const res = await this.confirmService.confirm(`Do you really want to delete ${video.name}?`, 'Delete')
- if (res === false) return
-
- this.videoService.removeVideo(video.id)
- .subscribe(
- status => {
- this.notificationsService.success('Success', `Video ${video.name} deleted.`)
- this.spliceVideosById(video.id)
- this.buildVideoPages()
- },
-
- error => this.notificationsService.error('Error', error.message)
- )
- }
-
- protected buildVideoHeight () {
- // In account videos, the video height is fixed
- return this.baseVideoHeight
- }
-
- private spliceVideosById (id: number) {
- for (const key of Object.keys(this.loadedPages)) {
- const videos = this.loadedPages[key]
- const index = videos.findIndex(v => v.id === id)
-
- if (index !== -1) {
- videos.splice(index, 1)
- return
- }
- }
- }
-}
+++ /dev/null
-<div class="row">
- <div class="sub-menu">
- <a routerLink="/account/settings" routerLinkActive="active" class="title-page">My account</a>
-
- <a routerLink="/account/videos" routerLinkActive="active" class="title-page">My videos</a>
- </div>
-
- <div class="margin-content">
- <router-outlet></router-outlet>
- </div>
-</div>
+++ /dev/null
-import { Component } from '@angular/core'
-
-@Component({
- selector: 'my-account',
- templateUrl: './account.component.html',
- styleUrls: [ './account.component.scss' ]
-})
-export class AccountComponent {}
+++ /dev/null
-import { NgModule } from '@angular/core'
-import { SharedModule } from '../shared'
-import { AccountRoutingModule } from './account-routing.module'
-import { AccountChangePasswordComponent } from './account-settings/account-change-password/account-change-password.component'
-import { AccountDetailsComponent } from './account-settings/account-details/account-details.component'
-import { AccountSettingsComponent } from './account-settings/account-settings.component'
-import { AccountComponent } from './account.component'
-import { AccountVideosComponent } from './account-videos/account-videos.component'
-
-@NgModule({
- imports: [
- AccountRoutingModule,
- SharedModule
- ],
-
- declarations: [
- AccountComponent,
- AccountSettingsComponent,
- AccountChangePasswordComponent,
- AccountDetailsComponent,
- AccountVideosComponent
- ],
-
- exports: [
- AccountComponent
- ],
-
- providers: []
-})
-export class AccountModule { }
+++ /dev/null
-export * from './account-routing.module'
-export * from './account.component'
-export * from './account.module'
import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
-import { AccountModule } from './account'
+import { MyAccountModule } from './my-account'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
AppRoutingModule,
- AccountModule,
+ MyAccountModule,
CoreModule,
LoginModule,
ResetPasswordModule,
</a>
<div class="logged-in-info">
- <a routerLink="/account/settings" class="logged-in-username">{{ user.username }}</a>
+ <a routerLink="/my-account/settings" class="logged-in-username">{{ user.username }}</a>
<div class="logged-in-email">{{ user.email }}</div>
</div>
<ul *dropdownMenu class="dropdown-menu">
<li>
- <a i18n routerLink="/account/settings" class="dropdown-item" title="My account">
+ <a i18n routerLink="/my-account/settings" class="dropdown-item" title="My account">
My account
</a>
--- /dev/null
+export * from './my-account-routing.module'
+export * from './my-account.component'
+export * from './my-account.module'
--- /dev/null
+import { NgModule } from '@angular/core'
+import { RouterModule, Routes } from '@angular/router'
+import { MetaGuard } from '@ngx-meta/core'
+import { LoginGuard } from '../core'
+import { MyAccountComponent } from './my-account.component'
+import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
+import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
+
+const myAccountRoutes: Routes = [
+ {
+ path: 'my-account',
+ component: MyAccountComponent,
+ canActivateChild: [ MetaGuard, LoginGuard ],
+ children: [
+ {
+ path: 'settings',
+ component: MyAccountSettingsComponent,
+ data: {
+ meta: {
+ title: 'Account settings'
+ }
+ }
+ },
+ {
+ path: 'videos',
+ component: MyAccountVideosComponent,
+ data: {
+ meta: {
+ title: 'Account videos'
+ }
+ }
+ }
+ ]
+ }
+]
+
+@NgModule({
+ imports: [ RouterModule.forChild(myAccountRoutes) ],
+ exports: [ RouterModule ]
+})
+export class MyAccountRoutingModule {}
--- /dev/null
+export * from './my-account-change-password.component'
--- /dev/null
+<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+<form role="form" (ngSubmit)="changePassword()" [formGroup]="form">
+
+ <label for="new-password">Change password</label>
+ <input
+ type="password" id="new-password" placeholder="New password"
+ formControlName="new-password" [ngClass]="{ 'input-error': formErrors['new-password'] }"
+ >
+ <div *ngIf="formErrors['new-password']" class="form-error">
+ {{ formErrors['new-password'] }}
+ </div>
+
+ <input
+ type="password" id="new-confirmed-password" placeholder="Confirm new password"
+ formControlName="new-confirmed-password"
+ >
+
+ <input type="submit" value="Change password" [disabled]="!form.valid">
+</form>
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+input[type=password] {
+ @include peertube-input-text(340px);
+ display: block;
+
+ &#new-confirmed-password {
+ margin-top: 15px;
+ }
+}
+
+input[type=submit] {
+ @include peertube-button;
+ @include orange-button;
+
+ margin-top: 15px;
+}
+
--- /dev/null
+import { Component, OnInit } from '@angular/core'
+import { FormBuilder, FormGroup } from '@angular/forms'
+import { NotificationsService } from 'angular2-notifications'
+import { FormReactive, USER_PASSWORD, UserService } from '../../../shared'
+
+@Component({
+ selector: 'my-account-change-password',
+ templateUrl: './my-account-change-password.component.html',
+ styleUrls: [ './my-account-change-password.component.scss' ]
+})
+export class MyAccountChangePasswordComponent extends FormReactive implements OnInit {
+ error: string = null
+
+ form: FormGroup
+ formErrors = {
+ 'new-password': '',
+ 'new-confirmed-password': ''
+ }
+ validationMessages = {
+ 'new-password': USER_PASSWORD.MESSAGES,
+ 'new-confirmed-password': USER_PASSWORD.MESSAGES
+ }
+
+ constructor (
+ private formBuilder: FormBuilder,
+ private notificationsService: NotificationsService,
+ private userService: UserService
+ ) {
+ super()
+ }
+
+ buildForm () {
+ this.form = this.formBuilder.group({
+ 'new-password': [ '', USER_PASSWORD.VALIDATORS ],
+ 'new-confirmed-password': [ '', USER_PASSWORD.VALIDATORS ]
+ })
+
+ this.form.valueChanges.subscribe(data => this.onValueChanged(data))
+ }
+
+ ngOnInit () {
+ this.buildForm()
+ }
+
+ changePassword () {
+ const newPassword = this.form.value['new-password']
+ const newConfirmedPassword = this.form.value['new-confirmed-password']
+
+ this.error = null
+
+ if (newPassword !== newConfirmedPassword) {
+ this.error = 'The new password and the confirmed password do not correspond.'
+ return
+ }
+
+ this.userService.changePassword(newPassword).subscribe(
+ () => this.notificationsService.success('Success', 'Password updated.'),
+
+ err => this.error = err.message
+ )
+ }
+}
--- /dev/null
+export * from './my-account-details.component'
--- /dev/null
+<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
+ <div class="form-group">
+ <label for="nsfwPolicy">Default policy on videos containing sensitive content</label>
+ <my-help helpType="custom" customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."></my-help>
+
+ <div class="peertube-select-container">
+ <select id="nsfwPolicy" formControlName="nsfwPolicy">
+ <option value="do_not_list">Do not list</option>
+ <option value="blur">Blur thumbnails</option>
+ <option value="display">Display</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group">
+ <input
+ type="checkbox" id="autoPlayVideo"
+ formControlName="autoPlayVideo"
+ >
+ <label for="autoPlayVideo"></label>
+ <label for="autoPlayVideo">Automatically plays video</label>
+ </div>
+
+ <input type="submit" value="Save" [disabled]="!form.valid">
+</form>
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+input[type=checkbox] {
+ @include peertube-checkbox(1px);
+}
+
+input[type=submit] {
+ @include peertube-button;
+ @include orange-button;
+
+ display: block;
+ margin-top: 15px;
+}
+
+.peertube-select-container {
+ @include peertube-select-container(340px);
+
+ margin-bottom: 30px;
+}
\ No newline at end of file
--- /dev/null
+import { Component, Input, OnInit } from '@angular/core'
+import { FormBuilder, FormGroup } from '@angular/forms'
+import { NotificationsService } from 'angular2-notifications'
+import { UserUpdateMe } from '../../../../../../shared'
+import { AuthService } from '../../../core'
+import { FormReactive, User, UserService } from '../../../shared'
+
+@Component({
+ selector: 'my-account-details',
+ templateUrl: './my-account-details.component.html',
+ styleUrls: [ './my-account-details.component.scss' ]
+})
+export class MyAccountDetailsComponent extends FormReactive implements OnInit {
+ @Input() user: User = null
+
+ form: FormGroup
+ formErrors = {}
+ validationMessages = {}
+
+ constructor (
+ private authService: AuthService,
+ private formBuilder: FormBuilder,
+ private notificationsService: NotificationsService,
+ private userService: UserService
+ ) {
+ super()
+ }
+
+ buildForm () {
+ this.form = this.formBuilder.group({
+ nsfwPolicy: [ this.user.nsfwPolicy ],
+ autoPlayVideo: [ this.user.autoPlayVideo ]
+ })
+
+ this.form.valueChanges.subscribe(data => this.onValueChanged(data))
+ }
+
+ ngOnInit () {
+ this.buildForm()
+ }
+
+ updateDetails () {
+ const nsfwPolicy = this.form.value['nsfwPolicy']
+ const autoPlayVideo = this.form.value['autoPlayVideo']
+ const details: UserUpdateMe = {
+ nsfwPolicy,
+ autoPlayVideo
+ }
+
+ this.userService.updateMyDetails(details).subscribe(
+ () => {
+ this.notificationsService.success('Success', 'Information updated.')
+
+ this.authService.refreshUserInformation()
+ },
+
+ err => this.notificationsService.error('Error', err.message)
+ )
+ }
+}
--- /dev/null
+<div class="user">
+ <img [src]="getAvatarUrl()" alt="Avatar" />
+
+ <div class="user-info">
+ <div class="user-info-username">{{ user.username }}</div>
+ <div class="user-info-followers">{{ user.account?.followersCount }} subscribers</div>
+ </div>
+</div>
+
+<div class="button-file">
+ <span>Change your avatar</span>
+ <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="changeAvatar()" />
+</div>
+<div class="file-max-size">(extensions: {{ avatarExtensions }}, max size: {{ maxAvatarSize | bytes }})</div>
+
+<div class="user-quota">
+ <span class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ userVideoQuota }}
+</div>
+
+<div class="account-title">Account settings</div>
+<my-account-change-password></my-account-change-password>
+
+<div class="account-title">Video settings</div>
+<my-account-details [user]="user"></my-account-details>
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+.user {
+ display: flex;
+
+ img {
+ @include avatar(50px);
+
+ margin-right: 15px;
+ }
+
+ .user-info {
+ .user-info-username {
+ font-size: 20px;
+ font-weight: $font-bold;
+ }
+
+ .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;
+
+ .user-quota-label {
+ font-weight: $font-semibold;
+ }
+}
+
+.account-title {
+ text-transform: uppercase;
+ color: $orange-color;
+ font-weight: $font-bold;
+ font-size: 13px;
+ margin-top: 55px;
+ margin-bottom: 30px;
+}
--- /dev/null
+import { Component, OnInit, ViewChild } from '@angular/core'
+import { NotificationsService } from 'angular2-notifications'
+import { BytesPipe } from 'ngx-pipes'
+import { AuthService } from '../../core'
+import { ServerService } from '../../core/server'
+import { User } from '../../shared'
+import { UserService } from '../../shared/users'
+
+@Component({
+ selector: 'my-account-settings',
+ templateUrl: './my-account-settings.component.html',
+ styleUrls: [ './my-account-settings.component.scss' ]
+})
+export class MyAccountSettingsComponent implements OnInit {
+ @ViewChild('avatarfileInput') avatarfileInput
+
+ user: User = null
+ userVideoQuota = '0'
+ userVideoQuotaUsed = 0
+
+ constructor (
+ private userService: UserService,
+ private authService: AuthService,
+ private serverService: ServerService,
+ private notificationsService: NotificationsService
+ ) {}
+
+ ngOnInit () {
+ this.user = this.authService.getUser()
+
+ this.authService.userInformationLoaded.subscribe(
+ () => {
+ if (this.user.videoQuota !== -1) {
+ this.userVideoQuota = new BytesPipe().transform(this.user.videoQuota, 0).toString()
+ } else {
+ this.userVideoQuota = 'Unlimited'
+ }
+ }
+ )
+
+ this.userService.getMyVideoQuotaUsed()
+ .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
+ }
+
+ getAvatarUrl () {
+ return this.user.getAvatarUrl()
+ }
+
+ changeAvatar () {
+ const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ]
+
+ const formData = new FormData()
+ formData.append('avatarfile', avatarfile)
+
+ this.userService.changeAvatar(formData)
+ .subscribe(
+ data => {
+ this.notificationsService.success('Success', 'Avatar changed.')
+
+ this.user.account.avatar = data.avatar
+ },
+
+ err => this.notificationsService.error('Error', err.message)
+ )
+ }
+
+ get maxAvatarSize () {
+ return this.serverService.getConfig().avatar.file.size.max
+ }
+
+ get avatarExtensions () {
+ return this.serverService.getConfig().avatar.file.extensions.join(',')
+ }
+}
--- /dev/null
+<div *ngIf="pagination.totalItems === 0">No results.</div>
+
+<div
+ myInfiniteScroller
+ [pageHeight]="pageHeight"
+ (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
+ class="videos" #videosElement
+>
+ <div *ngFor="let videos of videoPages; let i = index" class="videos-page">
+ <div class="video" *ngFor="let video of videos; let j = index">
+ <div class="checkbox-container">
+ <input [id]="'video-check-' + video.id" type="checkbox" [(ngModel)]="checkedVideos[video.id]" />
+ <label [for]="'video-check-' + video.id"></label>
+ </div>
+
+ <my-video-thumbnail [video]="video"></my-video-thumbnail>
+
+ <div class="video-info">
+ <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
+ <span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
+ <div class="video-info-private">{{ video.privacy.label }}</div>
+ </div>
+
+ <!-- Display only once -->
+ <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
+ <div class="action-selection-mode-child">
+ <span class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
+ Cancel
+ </span>
+
+ <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
+ <span class="icon icon-delete-white"></span>
+ Delete
+ </span>
+ </div>
+ </div>
+
+ <div class="video-buttons" *ngIf="isInSelectionMode() === false">
+ <my-delete-button (click)="deleteVideo(video)"></my-delete-button>
+
+ <my-edit-button [routerLink]="[ '/videos', 'edit', video.uuid ]"></my-edit-button>
+ </div>
+ </div>
+ </div>
+</div>
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+.action-selection-mode {
+ width: 174px;
+ display: flex;
+ justify-content: flex-end;
+
+ .action-selection-mode-child {
+ position: fixed;
+
+ .action-button {
+ display: inline-block;
+ }
+
+ .action-button-cancel-selection {
+ @include peertube-button;
+ @include grey-button;
+
+ margin-right: 10px;
+ }
+
+ .action-button-delete-selection {
+ @include peertube-button;
+ @include orange-button;
+ }
+
+ .icon.icon-delete-white {
+ @include icon(21px);
+
+ position: relative;
+ top: -2px;
+ background-image: url('../../../assets/images/global/delete-white.svg');
+ }
+ }
+}
+
+/deep/ .action-button {
+ &.action-button-delete {
+ margin-right: 10px;
+ }
+}
+
+.video {
+ display: flex;
+ min-height: 130px;
+ padding-bottom: 20px;
+ margin-bottom: 20px;
+ border-bottom: 1px solid #C6C6C6;
+
+ &:first-child {
+ margin-top: 47px;
+ }
+
+ .checkbox-container {
+ display: flex;
+ align-items: center;
+ margin-right: 20px;
+ margin-left: 12px;
+
+ input[type=checkbox] {
+ @include peertube-checkbox(2px);
+ }
+ }
+
+ my-video-thumbnail {
+ margin-right: 10px;
+ }
+
+ .video-info {
+ flex-grow: 1;
+
+ .video-info-name {
+ @include disable-default-a-behaviour;
+
+ color: #000;
+ display: block;
+ font-size: 16px;
+ font-weight: $font-semibold;
+ }
+
+ .video-info-date-views, .video-info-private {
+ font-size: 13px;
+
+ &.video-info-private {
+ font-weight: $font-semibold;
+ }
+ }
+ }
+
+ .video-buttons {
+ min-width: 190px;
+ }
+}
+
+@media screen and (max-width: 800px) {
+ .video {
+ flex-direction: column;
+ height: auto;
+ text-align: center;
+
+ input[type=checkbox] {
+ display: none;
+ }
+
+ my-video-thumbnail {
+ margin-right: 0;
+ }
+
+ .video-buttons {
+ margin-top: 10px;
+ }
+ }
+}
--- /dev/null
+import { Component, OnInit, OnDestroy } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { Location } from '@angular/common'
+import { immutableAssign } from '@app/shared/misc/utils'
+import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
+import { NotificationsService } from 'angular2-notifications'
+import 'rxjs/add/observable/from'
+import 'rxjs/add/operator/concatAll'
+import { Observable } from 'rxjs/Observable'
+import { AuthService } from '../../core/auth'
+import { ConfirmService } from '../../core/confirm'
+import { AbstractVideoList } from '../../shared/video/abstract-video-list'
+import { Video } from '../../shared/video/video.model'
+import { VideoService } from '../../shared/video/video.service'
+
+@Component({
+ selector: 'my-account-videos',
+ templateUrl: './my-account-videos.component.html',
+ styleUrls: [ './my-account-videos.component.scss' ]
+})
+export class MyAccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
+ titlePage = 'My videos'
+ currentRoute = '/my-account/videos'
+ checkedVideos: { [ id: number ]: boolean } = {}
+ pagination: ComponentPagination = {
+ currentPage: 1,
+ itemsPerPage: 5,
+ totalItems: null
+ }
+
+ protected baseVideoWidth = -1
+ protected baseVideoHeight = 155
+
+ constructor (protected router: Router,
+ protected route: ActivatedRoute,
+ protected authService: AuthService,
+ protected notificationsService: NotificationsService,
+ protected confirmService: ConfirmService,
+ protected location: Location,
+ private videoService: VideoService) {
+ super()
+ }
+
+ ngOnInit () {
+ super.ngOnInit()
+
+ // this.generateSyndicationList()
+ }
+
+ ngOnDestroy () {
+ super.ngOnDestroy()
+ }
+
+ abortSelectionMode () {
+ this.checkedVideos = {}
+ }
+
+ isInSelectionMode () {
+ return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
+ }
+
+ getVideosObservable (page: number) {
+ const newPagination = immutableAssign(this.pagination, { currentPage: page })
+
+ return this.videoService.getMyVideos(newPagination, this.sort)
+ }
+
+ generateSyndicationList () {
+ throw new Error('Method not implemented.')
+ }
+
+ async deleteSelectedVideos () {
+ const toDeleteVideosIds = Object.keys(this.checkedVideos)
+ .filter(k => this.checkedVideos[k] === true)
+ .map(k => parseInt(k, 10))
+
+ const res = await this.confirmService.confirm(`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 'Delete')
+ if (res === false) return
+
+ const observables: Observable<any>[] = []
+ for (const videoId of toDeleteVideosIds) {
+ const o = this.videoService
+ .removeVideo(videoId)
+ .do(() => this.spliceVideosById(videoId))
+
+ observables.push(o)
+ }
+
+ Observable.from(observables)
+ .concatAll()
+ .subscribe(
+ res => {
+ this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`)
+ this.buildVideoPages()
+ },
+
+ err => this.notificationsService.error('Error', err.message)
+ )
+ }
+
+ async deleteVideo (video: Video) {
+ const res = await this.confirmService.confirm(`Do you really want to delete ${video.name}?`, 'Delete')
+ if (res === false) return
+
+ this.videoService.removeVideo(video.id)
+ .subscribe(
+ status => {
+ this.notificationsService.success('Success', `Video ${video.name} deleted.`)
+ this.spliceVideosById(video.id)
+ this.buildVideoPages()
+ },
+
+ error => this.notificationsService.error('Error', error.message)
+ )
+ }
+
+ protected buildVideoHeight () {
+ // In account videos, the video height is fixed
+ return this.baseVideoHeight
+ }
+
+ private spliceVideosById (id: number) {
+ for (const key of Object.keys(this.loadedPages)) {
+ const videos = this.loadedPages[key]
+ const index = videos.findIndex(v => v.id === id)
+
+ if (index !== -1) {
+ videos.splice(index, 1)
+ return
+ }
+ }
+ }
+}
--- /dev/null
+<div class="row">
+ <div class="sub-menu">
+ <a routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My account</a>
+
+ <a routerLink="/my-account/videos" routerLinkActive="active" class="title-page">My videos</a>
+ </div>
+
+ <div class="margin-content">
+ <router-outlet></router-outlet>
+ </div>
+</div>
--- /dev/null
+import { Component } from '@angular/core'
+
+@Component({
+ selector: 'my-account',
+ templateUrl: './my-account.component.html'
+})
+export class MyAccountComponent {}
--- /dev/null
+import { NgModule } from '@angular/core'
+import { SharedModule } from '../shared'
+import { MyAccountRoutingModule } from './my-account-routing.module'
+import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component'
+import { MyAccountDetailsComponent } from './my-account-settings/my-account-details/my-account-details.component'
+import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
+import { MyAccountComponent } from './my-account.component'
+import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
+
+@NgModule({
+ imports: [
+ MyAccountRoutingModule,
+ SharedModule
+ ],
+
+ declarations: [
+ MyAccountComponent,
+ MyAccountSettingsComponent,
+ MyAccountChangePasswordComponent,
+ MyAccountDetailsComponent,
+ MyAccountVideosComponent
+ ],
+
+ exports: [
+ MyAccountComponent
+ ],
+
+ providers: []
+})
+export class MyAccountModule { }