aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.html22
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.scss14
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.ts103
-rw-r--r--client/src/app/+about/about-routing.module.ts10
-rw-r--r--client/src/app/+about/about.component.html4
-rw-r--r--client/src/app/+about/about.module.ts2
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.html32
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.scss31
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.ts71
-rw-r--r--client/src/app/+accounts/account-videos/account-videos.component.ts4
-rw-r--r--client/src/app/+accounts/accounts-routing.module.ts2
-rw-r--r--client/src/app/+accounts/accounts.component.html4
-rw-r--r--client/src/app/+admin/admin.module.ts3
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html8
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts1
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.ts2
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.ts2
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts2
-rw-r--r--client/src/app/+admin/follows/index.ts1
-rw-r--r--client/src/app/+admin/follows/shared/index.ts1
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts5
-rw-r--r--client/src/app/+my-account/my-account-history/my-account-history.component.ts2
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html7
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts5
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts15
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts3
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html10
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts4
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts47
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.html2
-rw-r--r--client/src/app/+signup/+register/custom-stepper.component.html25
-rw-r--r--client/src/app/+signup/+register/custom-stepper.component.scss66
-rw-r--r--client/src/app/+signup/+register/custom-stepper.component.ts19
-rw-r--r--client/src/app/+signup/+register/register-routing.module.ts (renamed from client/src/app/signup/signup-routing.module.ts)17
-rw-r--r--client/src/app/+signup/+register/register-step-channel.component.html50
-rw-r--r--client/src/app/+signup/+register/register-step-channel.component.ts40
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.html54
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.ts37
-rw-r--r--client/src/app/+signup/+register/register.component.html41
-rw-r--r--client/src/app/+signup/+register/register.component.scss (renamed from client/src/app/signup/signup.component.scss)33
-rw-r--r--client/src/app/+signup/+register/register.component.ts89
-rw-r--r--client/src/app/+signup/+register/register.module.ts33
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html (renamed from client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html)0
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss (renamed from client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss)0
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts (renamed from client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts)0
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html (renamed from client/src/app/+verify-account/verify-account-email/verify-account-email.component.html)6
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts (renamed from client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts)3
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-routing.module.ts (renamed from client/src/app/+verify-account/verify-account-routing.module.ts)8
-rw-r--r--client/src/app/+signup/+verify-account/verify-account.module.ts25
-rw-r--r--client/src/app/+signup/shared/signup-shared.module.ts21
-rw-r--r--client/src/app/+signup/shared/signup-success.component.html16
-rw-r--r--client/src/app/+signup/shared/signup-success.component.scss76
-rw-r--r--client/src/app/+signup/shared/signup-success.component.ts10
-rw-r--r--client/src/app/+verify-account/index.ts2
-rw-r--r--client/src/app/+verify-account/verify-account.module.ts27
-rw-r--r--client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts5
-rw-r--r--client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts2
-rw-r--r--client/src/app/app-routing.module.ts6
-rw-r--r--client/src/app/app.module.ts2
-rw-r--r--client/src/app/core/core.module.ts3
-rw-r--r--client/src/app/core/routing/redirect.service.ts9
-rw-r--r--client/src/app/core/routing/unlogged-guard.service.ts25
-rw-r--r--client/src/app/shared/actor/actor.model.ts2
-rw-r--r--client/src/app/shared/buttons/button.component.scss13
-rw-r--r--client/src/app/shared/buttons/button.component.ts2
-rw-r--r--client/src/app/shared/buttons/delete-button.component.html2
-rw-r--r--client/src/app/shared/buttons/edit-button.component.html2
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.scss3
-rw-r--r--client/src/app/shared/forms/reactive-file.component.html7
-rw-r--r--client/src/app/shared/forms/reactive-file.component.scss10
-rw-r--r--client/src/app/shared/forms/reactive-file.component.ts2
-rw-r--r--client/src/app/shared/images/image-upload.component.html9
-rw-r--r--client/src/app/shared/images/image-upload.component.scss18
-rw-r--r--client/src/app/shared/images/preview-upload.component.html13
-rw-r--r--client/src/app/shared/images/preview-upload.component.scss27
-rw-r--r--client/src/app/shared/images/preview-upload.component.ts (renamed from client/src/app/shared/images/image-upload.component.ts)17
-rw-r--r--client/src/app/shared/instance/follow.service.ts (renamed from client/src/app/+admin/follows/shared/follow.service.ts)8
-rw-r--r--client/src/app/shared/shared.module.ts9
-rw-r--r--client/src/app/shared/users/user.service.ts3
-rw-r--r--client/src/app/shared/video-channel/video-channel.service.ts17
-rw-r--r--client/src/app/shared/video/abstract-video-list.html20
-rw-r--r--client/src/app/shared/video/abstract-video-list.scss36
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts80
-rw-r--r--client/src/app/shared/video/modals/video-download.component.ts8
-rw-r--r--client/src/app/shared/video/video-details.model.ts3
-rw-r--r--client/src/app/shared/video/video-edit.model.ts5
-rw-r--r--client/src/app/shared/video/video.model.ts2
-rw-r--r--client/src/app/shared/video/videos-selection.component.ts2
-rw-r--r--client/src/app/signup/index.ts3
-rw-r--r--client/src/app/signup/signup.component.html72
-rw-r--r--client/src/app/signup/signup.component.ts78
-rw-r--r--client/src/app/signup/signup.module.ts24
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html14
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts8
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts1
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-send.ts1
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.html23
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss17
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts108
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts15
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts77
-rw-r--r--client/src/app/videos/video-list/video-local.component.ts2
-rw-r--r--client/src/app/videos/video-list/video-overview.component.html6
-rw-r--r--client/src/app/videos/video-list/video-overview.component.scss60
-rw-r--r--client/src/app/videos/video-list/video-recently-added.component.ts3
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts2
-rw-r--r--client/src/app/videos/video-list/video-user-subscriptions.component.ts3
-rw-r--r--client/src/assets/player/peertube-player-manager.ts13
-rw-r--r--client/src/sass/include/_miniature.scss97
-rw-r--r--client/src/sass/include/_mixins.scss7
113 files changed, 1506 insertions, 623 deletions
diff --git a/client/src/app/+about/about-follows/about-follows.component.html b/client/src/app/+about/about-follows/about-follows.component.html
new file mode 100644
index 000000000..18689bbf7
--- /dev/null
+++ b/client/src/app/+about/about-follows/about-follows.component.html
@@ -0,0 +1,22 @@
1<div class="row" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()">
2 <div class="col-xl-6 col-md-12">
3 <div i18n class="subtitle">Followers</div>
4
5 <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">This instance does not have followers.</div>
6
7 <a *ngFor="let follower of followers" [href]="buildLink(follower)" target="_blank" rel="noopener noreferrer">
8 {{ follower }}
9 </a>
10 </div>
11
12 <div class="col-xl-6 col-md-12">
13 <div i18n class="subtitle">Followings</div>
14
15 <div i18n class="no-results" *ngIf="followingsPagination.totalItems === 0">This instance does not have followings.</div>
16
17 <a *ngFor="let following of followings" [href]="buildLink(following)" target="_blank" rel="noopener noreferrer">
18 {{ following }}
19 </a>
20 </div>
21
22</div>
diff --git a/client/src/app/+about/about-follows/about-follows.component.scss b/client/src/app/+about/about-follows/about-follows.component.scss
new file mode 100644
index 000000000..e0d597a96
--- /dev/null
+++ b/client/src/app/+about/about-follows/about-follows.component.scss
@@ -0,0 +1,14 @@
1@import '_variables';
2@import '_mixins';
3
4.subtitle {
5 font-size: 18px;
6 font-weight: $font-semibold;
7 margin-bottom: 20px;
8}
9
10a {
11 display: block;
12 width: fit-content;
13 margin-top: 3px;
14}
diff --git a/client/src/app/+about/about-follows/about-follows.component.ts b/client/src/app/+about/about-follows/about-follows.component.ts
new file mode 100644
index 000000000..f0e1375d6
--- /dev/null
+++ b/client/src/app/+about/about-follows/about-follows.component.ts
@@ -0,0 +1,103 @@
1import { Component, OnInit } from '@angular/core'
2import { FollowService } from '@app/shared/instance/follow.service'
3import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model'
4import { Notifier } from '@app/core'
5import { RestService } from '@app/shared'
6import { SortMeta } from 'primeng/api'
7
8@Component({
9 selector: 'my-about-follows',
10 templateUrl: './about-follows.component.html',
11 styleUrls: [ './about-follows.component.scss' ]
12})
13
14export class AboutFollowsComponent implements OnInit {
15 followers: string[] = []
16 followings: string[] = []
17
18 followersPagination: ComponentPagination = {
19 currentPage: 1,
20 itemsPerPage: 40,
21 totalItems: null
22 }
23
24 followingsPagination: ComponentPagination = {
25 currentPage: 1,
26 itemsPerPage: 40,
27 totalItems: null
28 }
29
30 sort: SortMeta = {
31 field: 'createdAt',
32 order: -1
33 }
34
35 constructor (
36 private restService: RestService,
37 private notifier: Notifier,
38 private followService: FollowService
39 ) { }
40
41 ngOnInit () {
42 this.loadMoreFollowers()
43
44 this.loadMoreFollowings()
45 }
46
47 onNearOfBottom () {
48 this.onNearOfFollowersBottom()
49
50 this.onNearOfFollowingsBottom()
51 }
52
53 onNearOfFollowersBottom () {
54 if (!hasMoreItems(this.followersPagination)) return
55
56 this.followersPagination.currentPage += 1
57 this.loadMoreFollowers()
58 }
59
60 onNearOfFollowingsBottom () {
61 if (!hasMoreItems(this.followingsPagination)) return
62
63 this.followingsPagination.currentPage += 1
64 this.loadMoreFollowings()
65 }
66
67 buildLink (host: string) {
68 return window.location.protocol + '//' + host
69 }
70
71 private loadMoreFollowers () {
72 const pagination = this.restService.componentPaginationToRestPagination(this.followersPagination)
73
74 this.followService.getFollowers(pagination, this.sort)
75 .subscribe(
76 resultList => {
77 const newFollowers = resultList.data.map(r => r.follower.host)
78 this.followers = this.followers.concat(newFollowers)
79
80 this.followersPagination.totalItems = resultList.total
81 },
82
83 err => this.notifier.error(err.message)
84 )
85 }
86
87 private loadMoreFollowings () {
88 const pagination = this.restService.componentPaginationToRestPagination(this.followingsPagination)
89
90 this.followService.getFollowing(pagination, this.sort)
91 .subscribe(
92 resultList => {
93 const newFollowings = resultList.data.map(r => r.following.host)
94 this.followings = this.followings.concat(newFollowings)
95
96 this.followingsPagination.totalItems = resultList.total
97 },
98
99 err => this.notifier.error(err.message)
100 )
101 }
102
103}
diff --git a/client/src/app/+about/about-routing.module.ts b/client/src/app/+about/about-routing.module.ts
index c83c62c7f..33e5070cb 100644
--- a/client/src/app/+about/about-routing.module.ts
+++ b/client/src/app/+about/about-routing.module.ts
@@ -4,6 +4,7 @@ import { MetaGuard } from '@ngx-meta/core'
4import { AboutComponent } from './about.component' 4import { AboutComponent } from './about.component'
5import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' 5import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
6import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' 6import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
7import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
7 8
8const aboutRoutes: Routes = [ 9const aboutRoutes: Routes = [
9 { 10 {
@@ -33,6 +34,15 @@ const aboutRoutes: Routes = [
33 title: 'About PeerTube' 34 title: 'About PeerTube'
34 } 35 }
35 } 36 }
37 },
38 {
39 path: 'follows',
40 component: AboutFollowsComponent,
41 data: {
42 meta: {
43 title: 'About follows'
44 }
45 }
36 } 46 }
37 ] 47 ]
38 } 48 }
diff --git a/client/src/app/+about/about.component.html b/client/src/app/+about/about.component.html
index 8c50835c1..0c4a5156d 100644
--- a/client/src/app/+about/about.component.html
+++ b/client/src/app/+about/about.component.html
@@ -5,10 +5,12 @@
5 <a i18n routerLink="instance" routerLinkActive="active" class="title-page">Instance</a> 5 <a i18n routerLink="instance" routerLinkActive="active" class="title-page">Instance</a>
6 6
7 <a i18n routerLink="peertube" routerLinkActive="active" class="title-page">PeerTube</a> 7 <a i18n routerLink="peertube" routerLinkActive="active" class="title-page">PeerTube</a>
8
9 <a i18n routerLink="follows" routerLinkActive="active" class="title-page">Follows</a>
8 </div> 10 </div>
9 </div> 11 </div>
10 12
11 <div class="margin-content"> 13 <div class="margin-content">
12 <router-outlet></router-outlet> 14 <router-outlet></router-outlet>
13 </div> 15 </div>
14</div> \ No newline at end of file 16</div>
diff --git a/client/src/app/+about/about.module.ts b/client/src/app/+about/about.module.ts
index 9c6b29740..49a7a52f8 100644
--- a/client/src/app/+about/about.module.ts
+++ b/client/src/app/+about/about.module.ts
@@ -6,6 +6,7 @@ import { SharedModule } from '../shared'
6import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' 6import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
7import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' 7import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
8import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' 8import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
9import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
9 10
10@NgModule({ 11@NgModule({
11 imports: [ 12 imports: [
@@ -17,6 +18,7 @@ import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-a
17 AboutComponent, 18 AboutComponent,
18 AboutInstanceComponent, 19 AboutInstanceComponent,
19 AboutPeertubeComponent, 20 AboutPeertubeComponent,
21 AboutFollowsComponent,
20 ContactAdminModalComponent 22 ContactAdminModalComponent
21 ], 23 ],
22 24
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
index c3ef1d894..43dbbebb3 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
@@ -1,11 +1,21 @@
1<div *ngIf="account" class="row"> 1<div class="margin-content">
2 <a 2
3 *ngFor="let videoChannel of videoChannels" [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" 3 <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div>
4 class="video-channel" i18n-title title="See this video channel" 4
5 > 5 <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true">
6 <img [src]="videoChannel.avatarUrl" alt="Avatar" /> 6 <div class="section channel" *ngFor="let videoChannel of videoChannels">
7 7 <div class="section-title">
8 <div class="video-channel-display-name">{{ videoChannel.displayName }}</div> 8 <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" i18n-title title="See this video channel">
9 <div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div> 9 <img [src]="videoChannel.avatarUrl" alt="Avatar" />
10 </a> 10
11</div> \ No newline at end of file 11 <div>{{ videoChannel.displayName }}</div>
12 <div i18n class="followers">{{ videoChannel.followersCount }} subscribers</div>
13 </a>
14
15 <my-subscribe-button [videoChannel]="videoChannel"></my-subscribe-button>
16 </div>
17
18 <my-video-miniature *ngFor="let video of getVideosOf(videoChannel)" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature>
19 </div>
20 </div>
21</div>
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
index 0c6de2efa..d9f78bdcd 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
@@ -1,30 +1,17 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3@import '_miniature';
3 4
4.row { 5.margin-content {
5 justify-content: center; 6 @include adapt-margin-content-width;
6} 7}
7 8
8a.video-channel { 9.section {
9 @include disable-default-a-behaviour; 10 @include miniature-rows;
10 11
11 display: inline-block; 12 padding-top: 0 !important;
12 text-align: center;
13 color: var(--mainForegroundColor);
14 margin: 10px 30px;
15 13
16 img { 14 .section-title {
17 @include avatar(80px); 15 align-items: center;
18
19 margin-bottom: 10px;
20 }
21
22 .video-channel-display-name {
23 font-size: 20px;
24 font-weight: $font-bold;
25 } 16 }
26 17}
27 .video-channel-followers {
28 font-size: 15px;
29 }
30} \ No newline at end of file
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
index 44f5626bb..ee3b5f8e4 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
@@ -3,9 +3,14 @@ import { ActivatedRoute } from '@angular/router'
3import { Account } from '@app/shared/account/account.model' 3import { Account } from '@app/shared/account/account.model'
4import { AccountService } from '@app/shared/account/account.service' 4import { AccountService } from '@app/shared/account/account.service'
5import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' 5import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
6import { flatMap, map, tap } from 'rxjs/operators' 6import { concatMap, map, switchMap, tap } from 'rxjs/operators'
7import { Subscription } from 'rxjs' 7import { from, Subscription } from 'rxjs'
8import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 8import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
9import { Video } from '@app/shared/video/video.model'
10import { AuthService } from '@app/core'
11import { VideoService } from '@app/shared/video/video.service'
12import { VideoSortField } from '@app/shared/video/sort-field.type'
13import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model'
9 14
10@Component({ 15@Component({
11 selector: 'my-account-video-channels', 16 selector: 'my-account-video-channels',
@@ -15,27 +20,73 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
15export class AccountVideoChannelsComponent implements OnInit, OnDestroy { 20export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
16 account: Account 21 account: Account
17 videoChannels: VideoChannel[] = [] 22 videoChannels: VideoChannel[] = []
23 videos: { [id: number]: Video[] } = {}
24
25 channelPagination: ComponentPagination = {
26 currentPage: 1,
27 itemsPerPage: 2
28 }
29
30 videosPagination: ComponentPagination = {
31 currentPage: 1,
32 itemsPerPage: 12
33 }
34 videosSort: VideoSortField = '-publishedAt'
18 35
19 private accountSub: Subscription 36 private accountSub: Subscription
20 37
21 constructor ( 38 constructor (
22 protected route: ActivatedRoute, 39 private route: ActivatedRoute,
40 private authService: AuthService,
23 private accountService: AccountService, 41 private accountService: AccountService,
24 private videoChannelService: VideoChannelService 42 private videoChannelService: VideoChannelService,
43 private videoService: VideoService
25 ) { } 44 ) { }
26 45
46 get user () {
47 return this.authService.getUser()
48 }
49
27 ngOnInit () { 50 ngOnInit () {
28 // Parent get the account for us 51 // Parent get the account for us
29 this.accountSub = this.accountService.accountLoaded 52 this.accountSub = this.accountService.accountLoaded
30 .pipe( 53 .subscribe(account => {
31 tap(account => this.account = account), 54 this.account = account
32 flatMap(account => this.videoChannelService.listAccountVideoChannels(account)), 55
33 map(res => res.data) 56 this.loadMoreChannels()
34 ) 57 })
35 .subscribe(videoChannels => this.videoChannels = videoChannels)
36 } 58 }
37 59
38 ngOnDestroy () { 60 ngOnDestroy () {
39 if (this.accountSub) this.accountSub.unsubscribe() 61 if (this.accountSub) this.accountSub.unsubscribe()
40 } 62 }
63
64 loadMoreChannels () {
65 this.videoChannelService.listAccountVideoChannels(this.account, this.channelPagination)
66 .pipe(
67 tap(res => this.channelPagination.totalItems = res.total),
68 switchMap(res => from(res.data)),
69 concatMap(videoChannel => {
70 return this.videoService.getVideoChannelVideos(videoChannel, this.videosPagination, this.videosSort)
71 .pipe(map(data => ({ videoChannel, videos: data.videos })))
72 })
73 )
74 .subscribe(({ videoChannel, videos }) => {
75 this.videoChannels.push(videoChannel)
76
77 this.videos[videoChannel.id] = videos
78 })
79 }
80
81 getVideosOf (videoChannel: VideoChannel) {
82 return this.videos[ videoChannel.id ] || []
83 }
84
85 onNearOfBottom () {
86 if (!hasMoreItems(this.channelPagination)) return
87
88 this.channelPagination.currentPage += 1
89
90 this.loadMoreChannels()
91 }
41} 92}
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts
index 0d579fa0c..5a99aadce 100644
--- a/client/src/app/+accounts/account-videos/account-videos.component.ts
+++ b/client/src/app/+accounts/account-videos/account-videos.component.ts
@@ -29,6 +29,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
29 private accountSub: Subscription 29 private accountSub: Subscription
30 30
31 constructor ( 31 constructor (
32 protected i18n: I18n,
32 protected router: Router, 33 protected router: Router,
33 protected serverService: ServerService, 34 protected serverService: ServerService,
34 protected route: ActivatedRoute, 35 protected route: ActivatedRoute,
@@ -36,13 +37,10 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
36 protected notifier: Notifier, 37 protected notifier: Notifier,
37 protected confirmService: ConfirmService, 38 protected confirmService: ConfirmService,
38 protected screenService: ScreenService, 39 protected screenService: ScreenService,
39 private i18n: I18n,
40 private accountService: AccountService, 40 private accountService: AccountService,
41 private videoService: VideoService 41 private videoService: VideoService
42 ) { 42 ) {
43 super() 43 super()
44
45 this.titlePage = this.i18n('Published videos')
46 } 44 }
47 45
48 ngOnInit () { 46 ngOnInit () {
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts
index 531d763c4..55bce351a 100644
--- a/client/src/app/+accounts/accounts-routing.module.ts
+++ b/client/src/app/+accounts/accounts-routing.module.ts
@@ -14,7 +14,7 @@ const accountsRoutes: Routes = [
14 children: [ 14 children: [
15 { 15 {
16 path: '', 16 path: '',
17 redirectTo: 'videos', 17 redirectTo: 'video-channels',
18 pathMatch: 'full' 18 pathMatch: 'full'
19 }, 19 },
20 { 20 {
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index c1377c1ea..038e18c4b 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -26,10 +26,10 @@
26 </div> 26 </div>
27 27
28 <div class="links"> 28 <div class="links">
29 <a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a>
30
31 <a i18n routerLink="video-channels" routerLinkActive="active" class="title-page">Video channels</a> 29 <a i18n routerLink="video-channels" routerLinkActive="active" class="title-page">Video channels</a>
32 30
31 <a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a>
32
33 <a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a> 33 <a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a>
34 </div> 34 </div>
35 </div> 35 </div>
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 71a4dfc4a..9ab883f60 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -5,7 +5,7 @@ import { TableModule } from 'primeng/table'
5import { SharedModule } from '../shared' 5import { SharedModule } from '../shared'
6import { AdminRoutingModule } from './admin-routing.module' 6import { AdminRoutingModule } from './admin-routing.module'
7import { AdminComponent } from './admin.component' 7import { AdminComponent } from './admin.component'
8import { FollowersListComponent, FollowingAddComponent, FollowsComponent, FollowService } from './follows' 8import { FollowersListComponent, FollowingAddComponent, FollowsComponent } from './follows'
9import { FollowingListComponent } from './follows/following-list/following-list.component' 9import { FollowingListComponent } from './follows/following-list/following-list.component'
10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' 10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users'
11import { 11import {
@@ -66,7 +66,6 @@ import { DebugComponent, DebugService } from '@app/+admin/system/debug'
66 ], 66 ],
67 67
68 providers: [ 68 providers: [
69 FollowService,
70 RedundancyService, 69 RedundancyService,
71 JobService, 70 JobService,
72 LogsService, 71 LogsService,
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 637484622..44fc6dc26 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -287,6 +287,14 @@
287 </div> 287 </div>
288 288
289 <div class="form-group"> 289 <div class="form-group">
290 <my-peertube-checkbox
291 inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles"
292 i18n-labelText labelText="Allow audio files upload"
293 i18n-helpHtml helpHtml="Allow your users to upload audio files that will be merged with the preview file on upload"
294 ></my-peertube-checkbox>
295 </div>
296
297 <div class="form-group">
290 <label i18n for="transcodingThreads">Transcoding threads</label> 298 <label i18n for="transcodingThreads">Transcoding threads</label>
291 <div class="peertube-select-container"> 299 <div class="peertube-select-container">
292 <select id="transcodingThreads" formControlName="threads"> 300 <select id="transcodingThreads" formControlName="threads">
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index e64750713..c238a6c81 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -116,6 +116,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
116 enabled: null, 116 enabled: null,
117 threads: this.customConfigValidatorsService.TRANSCODING_THREADS, 117 threads: this.customConfigValidatorsService.TRANSCODING_THREADS,
118 allowAdditionalExtensions: null, 118 allowAdditionalExtensions: null,
119 allowAudioFiles: null,
119 resolutions: {} 120 resolutions: {}
120 }, 121 },
121 autoBlacklist: { 122 autoBlacklist: {
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
index b78cdf656..e25d9ab66 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
@@ -3,7 +3,7 @@ import { ConfirmService, Notifier } from '@app/core'
3import { SortMeta } from 'primeng/primeng' 3import { SortMeta } from 'primeng/primeng'
4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' 4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
5import { RestPagination, RestTable } from '../../../shared' 5import { RestPagination, RestTable } from '../../../shared'
6import { FollowService } from '../shared' 6import { FollowService } from '@app/shared/instance/follow.service'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8 8
9@Component({ 9@Component({
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.ts b/client/src/app/+admin/follows/following-add/following-add.component.ts
index 2bb249746..308bbb0c5 100644
--- a/client/src/app/+admin/follows/following-add/following-add.component.ts
+++ b/client/src/app/+admin/follows/following-add/following-add.component.ts
@@ -3,7 +3,7 @@ import { Router } from '@angular/router'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { ConfirmService } from '../../../core' 4import { ConfirmService } from '../../../core'
5import { validateHost } from '../../../shared' 5import { validateHost } from '../../../shared'
6import { FollowService } from '../shared' 6import { FollowService } from '@app/shared/instance/follow.service'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8 8
9@Component({ 9@Component({
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts
index 4517a721e..ded616624 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.ts
+++ b/client/src/app/+admin/follows/following-list/following-list.component.ts
@@ -4,7 +4,7 @@ import { SortMeta } from 'primeng/primeng'
4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' 4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
5import { ConfirmService } from '../../../core/confirm/confirm.service' 5import { ConfirmService } from '../../../core/confirm/confirm.service'
6import { RestPagination, RestTable } from '../../../shared' 6import { RestPagination, RestTable } from '../../../shared'
7import { FollowService } from '../shared' 7import { FollowService } from '@app/shared/instance/follow.service'
8import { I18n } from '@ngx-translate/i18n-polyfill' 8import { I18n } from '@ngx-translate/i18n-polyfill'
9 9
10@Component({ 10@Component({
diff --git a/client/src/app/+admin/follows/index.ts b/client/src/app/+admin/follows/index.ts
index 7849a06e7..e94f33710 100644
--- a/client/src/app/+admin/follows/index.ts
+++ b/client/src/app/+admin/follows/index.ts
@@ -1,6 +1,5 @@
1export * from './following-add' 1export * from './following-add'
2export * from './followers-list' 2export * from './followers-list'
3export * from './following-list' 3export * from './following-list'
4export * from './shared'
5export * from './follows.component' 4export * from './follows.component'
6export * from './follows.routes' 5export * from './follows.routes'
diff --git a/client/src/app/+admin/follows/shared/index.ts b/client/src/app/+admin/follows/shared/index.ts
deleted file mode 100644
index 78d456def..000000000
--- a/client/src/app/+admin/follows/shared/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './follow.service'
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts
index adce1b2d4..ee6d2c489 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/users/user-edit/user-edit.ts
@@ -7,7 +7,8 @@ import { UserAdminFlag } from '@shared/models/users/user-flag.model'
7export abstract class UserEdit extends FormReactive { 7export abstract class UserEdit extends FormReactive {
8 videoQuotaOptions: { value: string, label: string }[] = [] 8 videoQuotaOptions: { value: string, label: string }[] = []
9 videoQuotaDailyOptions: { value: string, label: string }[] = [] 9 videoQuotaDailyOptions: { value: string, label: string }[] = []
10 roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) 10 roles = Object.keys(USER_ROLE_LABELS)
11 .map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
11 username: string 12 username: string
12 userId: number 13 userId: number
13 14
@@ -27,7 +28,7 @@ export abstract class UserEdit extends FormReactive {
27 const transcodingConfig = this.serverService.getConfig().transcoding 28 const transcodingConfig = this.serverService.getConfig().transcoding
28 29
29 const resolutions = transcodingConfig.enabledResolutions 30 const resolutions = transcodingConfig.enabledResolutions
30 const higherResolution = VideoResolution.H_1080P 31 const higherResolution = VideoResolution.H_4K
31 let multiplier = 0 32 let multiplier = 0
32 33
33 for (const resolution of resolutions) { 34 for (const resolution of resolutions) {
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.ts b/client/src/app/+my-account/my-account-history/my-account-history.component.ts
index 73340d21a..13607119e 100644
--- a/client/src/app/+my-account/my-account-history/my-account-history.component.ts
+++ b/client/src/app/+my-account/my-account-history/my-account-history.component.ts
@@ -27,6 +27,7 @@ export class MyAccountHistoryComponent extends AbstractVideoList implements OnIn
27 videosHistoryEnabled: boolean 27 videosHistoryEnabled: boolean
28 28
29 constructor ( 29 constructor (
30 protected i18n: I18n,
30 protected router: Router, 31 protected router: Router,
31 protected serverService: ServerService, 32 protected serverService: ServerService,
32 protected route: ActivatedRoute, 33 protected route: ActivatedRoute,
@@ -34,7 +35,6 @@ export class MyAccountHistoryComponent extends AbstractVideoList implements OnIn
34 protected userService: UserService, 35 protected userService: UserService,
35 protected notifier: Notifier, 36 protected notifier: Notifier,
36 protected screenService: ScreenService, 37 protected screenService: ScreenService,
37 protected i18n: I18n,
38 private confirmService: ConfirmService, 38 private confirmService: ConfirmService,
39 private videoService: VideoService, 39 private videoService: VideoService,
40 private userHistoryService: UserHistoryService 40 private userHistoryService: UserHistoryService
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html
index 81fb11f45..f87df87df 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html
@@ -61,5 +61,12 @@ When you will upload a video in this channel, the video support field will be au
61 </div> 61 </div>
62 </div> 62 </div>
63 63
64 <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()">
65 <my-peertube-checkbox
66 inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate"
67 i18n-labelText labelText="Overwrite support field of all videos of this channel"
68 ></my-peertube-checkbox>
69 </div>
70
64 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 71 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
65</form> 72</form>
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts
index 4dc65dd99..7479442d1 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts
@@ -11,4 +11,9 @@ export abstract class MyAccountVideoChannelEdit extends FormReactive {
11 11
12 // FIXME: We need this method so angular does not complain in the child template 12 // FIXME: We need this method so angular does not complain in the child template
13 onAvatarChange (formData: FormData) { /* empty */ } 13 onAvatarChange (formData: FormData) { /* empty */ }
14
15 // Should be implemented by the child
16 isBulkUpdateVideosDisplayed () {
17 return false
18 }
14} 19}
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts
index da4fb645a..081e956d2 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts
@@ -20,6 +20,7 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
20 videoChannelToUpdate: VideoChannel 20 videoChannelToUpdate: VideoChannel
21 21
22 private paramsSub: Subscription 22 private paramsSub: Subscription
23 private oldSupportField: string
23 24
24 constructor ( 25 constructor (
25 protected formValidatorService: FormValidatorService, 26 protected formValidatorService: FormValidatorService,
@@ -39,7 +40,8 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
39 this.buildForm({ 40 this.buildForm({
40 'display-name': this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME, 41 'display-name': this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME,
41 description: this.videoChannelValidatorsService.VIDEO_CHANNEL_DESCRIPTION, 42 description: this.videoChannelValidatorsService.VIDEO_CHANNEL_DESCRIPTION,
42 support: this.videoChannelValidatorsService.VIDEO_CHANNEL_SUPPORT 43 support: this.videoChannelValidatorsService.VIDEO_CHANNEL_SUPPORT,
44 bulkVideosSupportUpdate: null
43 }) 45 })
44 46
45 this.paramsSub = this.route.params.subscribe(routeParams => { 47 this.paramsSub = this.route.params.subscribe(routeParams => {
@@ -49,6 +51,8 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
49 videoChannelToUpdate => { 51 videoChannelToUpdate => {
50 this.videoChannelToUpdate = videoChannelToUpdate 52 this.videoChannelToUpdate = videoChannelToUpdate
51 53
54 this.oldSupportField = videoChannelToUpdate.support
55
52 this.form.patchValue({ 56 this.form.patchValue({
53 'display-name': videoChannelToUpdate.displayName, 57 'display-name': videoChannelToUpdate.displayName,
54 description: videoChannelToUpdate.description, 58 description: videoChannelToUpdate.description,
@@ -72,7 +76,8 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
72 const videoChannelUpdate: VideoChannelUpdate = { 76 const videoChannelUpdate: VideoChannelUpdate = {
73 displayName: body['display-name'], 77 displayName: body['display-name'],
74 description: body.description || null, 78 description: body.description || null,
75 support: body.support || null 79 support: body.support || null,
80 bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false
76 } 81 }
77 82
78 this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( 83 this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe(
@@ -118,4 +123,10 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
118 getFormButtonTitle () { 123 getFormButtonTitle () {
119 return this.i18n('Update') 124 return this.i18n('Update')
120 } 125 }
126
127 isBulkUpdateVideosDisplayed () {
128 if (this.oldSupportField === undefined) return false
129
130 return this.oldSupportField !== this.form.value['support']
131 }
121} 132}
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts
index 87a10961f..8aed8b513 100644
--- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts
@@ -7,7 +7,6 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val
7import { VideoPlaylistValidatorsService } from '@app/shared' 7import { VideoPlaylistValidatorsService } from '@app/shared'
8import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' 8import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
10import { VideoConstant } from '@shared/models'
11import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' 10import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
12import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' 11import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
13 12
@@ -18,7 +17,6 @@ import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
18}) 17})
19export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylistEdit implements OnInit { 18export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylistEdit implements OnInit {
20 error: string 19 error: string
21 videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = []
22 20
23 constructor ( 21 constructor (
24 protected formValidatorService: FormValidatorService, 22 protected formValidatorService: FormValidatorService,
@@ -47,6 +45,7 @@ export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylis
47 }) 45 })
48 46
49 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) 47 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
48 .catch(err => console.error('Cannot populate user video channels.', err))
50 49
51 this.serverService.videoPlaylistPrivaciesLoaded.subscribe( 50 this.serverService.videoPlaylistPrivaciesLoaded.subscribe(
52 () => { 51 () => {
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html
index 303fc46f7..82321459f 100644
--- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html
@@ -57,10 +57,12 @@
57 </div> 57 </div>
58 58
59 <div class="form-group"> 59 <div class="form-group">
60 <my-image-upload 60 <label i18n>Playlist thumbnail</label>
61 i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile" 61
62 previewWidth="200px" previewHeight="110px" 62 <my-preview-upload
63 ></my-image-upload> 63 i18n-inputLabel inputLabel="Edit" inputName="thumbnailfile" formControlName="thumbnailfile"
64 previewWidth="223px" previewHeight="122px"
65 ></my-preview-upload>
64 </div> 66 </div>
65 </div> 67 </div>
66 </div> 68 </div>
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts
index fbfb4c8f7..e94188786 100644
--- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts
@@ -1,12 +1,12 @@
1import { FormReactive } from '@app/shared' 1import { FormReactive } from '@app/shared'
2import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
3import { ServerService } from '@app/core'
4import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' 2import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model'
3import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models'
5 4
6export abstract class MyAccountVideoPlaylistEdit extends FormReactive { 5export abstract class MyAccountVideoPlaylistEdit extends FormReactive {
7 // Declare it here to avoid errors in create template 6 // Declare it here to avoid errors in create template
8 videoPlaylistToUpdate: VideoPlaylist 7 videoPlaylistToUpdate: VideoPlaylist
9 userVideoChannels: { id: number, label: string }[] = [] 8 userVideoChannels: { id: number, label: string }[] = []
9 videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = []
10 10
11 abstract isCreation (): boolean 11 abstract isCreation (): boolean
12 abstract getFormButtonTitle (): string 12 abstract getFormButtonTitle (): string
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts
index 4887fdfb4..917ad7258 100644
--- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts
@@ -9,9 +9,8 @@ import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
10import { VideoPlaylistValidatorsService } from '@app/shared' 10import { VideoPlaylistValidatorsService } from '@app/shared'
11import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model' 11import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model'
12import { VideoConstant } from '@shared/models'
13import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
14import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' 12import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
13import { delayWhen, map, switchMap } from 'rxjs/operators'
15 14
16@Component({ 15@Component({
17 selector: 'my-account-video-playlist-update', 16 selector: 'my-account-video-playlist-update',
@@ -21,7 +20,6 @@ import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
21export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylistEdit implements OnInit, OnDestroy { 20export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylistEdit implements OnInit, OnDestroy {
22 error: string 21 error: string
23 videoPlaylistToUpdate: VideoPlaylist 22 videoPlaylistToUpdate: VideoPlaylist
24 videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = []
25 23
26 private paramsSub: Subscription 24 private paramsSub: Subscription
27 25
@@ -53,31 +51,24 @@ export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylis
53 }) 51 })
54 52
55 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) 53 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
56 54 .catch(err => console.error('Cannot populate user video channels.', err))
57 this.paramsSub = this.route.params.subscribe(routeParams => { 55
58 const videoPlaylistId = routeParams['videoPlaylistId'] 56 this.paramsSub = this.route.params
59 57 .pipe(
60 this.videoPlaylistService.getVideoPlaylist(videoPlaylistId).subscribe( 58 map(routeParams => routeParams['videoPlaylistId']),
61 videoPlaylistToUpdate => { 59 switchMap(videoPlaylistId => this.videoPlaylistService.getVideoPlaylist(videoPlaylistId)),
62 this.videoPlaylistToUpdate = videoPlaylistToUpdate 60 delayWhen(() => this.serverService.videoPlaylistPrivaciesLoaded)
63 61 )
64 this.hydrateFormFromPlaylist() 62 .subscribe(
65 63 videoPlaylistToUpdate => {
66 this.serverService.videoPlaylistPrivaciesLoaded.subscribe( 64 this.videoPlaylistPrivacies = this.serverService.getVideoPlaylistPrivacies()
67 () => { 65 this.videoPlaylistToUpdate = videoPlaylistToUpdate
68 this.videoPlaylistPrivacies = this.serverService.getVideoPlaylistPrivacies() 66
69 .filter(p => { 67 this.hydrateFormFromPlaylist()
70 // If the playlist is not private, we cannot put it in private anymore 68 },
71 return this.videoPlaylistToUpdate.privacy.id === VideoPlaylistPrivacy.PRIVATE || 69
72 p.id !== VideoPlaylistPrivacy.PRIVATE 70 err => this.error = err.message
73 }) 71 )
74 }
75 )
76 },
77
78 err => this.error = err.message
79 )
80 })
81 } 72 }
82 73
83 ngOnDestroy () { 74 ngOnDestroy () {
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
index 84d464800..2854093c4 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
@@ -20,7 +20,7 @@
20 <my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button> 20 <my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button>
21 21
22 <my-button i18n-label label="Change ownership" 22 <my-button i18n-label label="Change ownership"
23 className="action-button-change-ownership" 23 className="action-button-change-ownership grey-button"
24 icon="im-with-her" 24 icon="im-with-her"
25 (click)="changeOwnership($event, video)" 25 (click)="changeOwnership($event, video)"
26 ></my-button> 26 ></my-button>
diff --git a/client/src/app/+signup/+register/custom-stepper.component.html b/client/src/app/+signup/+register/custom-stepper.component.html
new file mode 100644
index 000000000..bf507fc4f
--- /dev/null
+++ b/client/src/app/+signup/+register/custom-stepper.component.html
@@ -0,0 +1,25 @@
1<section class="container">
2 <header>
3 <ng-container *ngFor="let step of steps; let i = index; let isLast = last;">
4 <div
5 class="step-info" [ngClass]="{ active: selectedIndex === i, completed: isCompleted(step) }"
6 (click)="onClick(i)"
7 >
8 <div class="step-index">
9 <ng-container *ngIf="!isCompleted(step)">{{ i + 1 }}</ng-container>
10 <my-global-icon *ngIf="isCompleted(step)" iconName="tick"></my-global-icon>
11 </div>
12
13 <div class="step-label">{{ step.label }}</div>
14 </div>
15
16 <!-- Do no display if this is the last child -->
17 <div *ngIf="!isLast" class="connector"></div>
18 </ng-container>
19 </header>
20
21 <div [style.display]="selected ? 'block' : 'none'">
22 <ng-container [ngTemplateOutlet]="selected.content"></ng-container>
23 </div>
24
25</section>
diff --git a/client/src/app/+signup/+register/custom-stepper.component.scss b/client/src/app/+signup/+register/custom-stepper.component.scss
new file mode 100644
index 000000000..2371c8ae5
--- /dev/null
+++ b/client/src/app/+signup/+register/custom-stepper.component.scss
@@ -0,0 +1,66 @@
1@import '_variables';
2@import '_mixins';
3
4$grey-color: #9CA3AB;
5$index-block-height: 32px;
6
7header {
8 display: flex;
9 justify-content: space-between;
10 font-size: 15px;
11 margin-bottom: 30px;
12
13 .step-info {
14 color: $grey-color;
15 display: flex;
16 flex-direction: column;
17 align-items: center;
18 width: $index-block-height;
19
20 .step-index {
21 display: flex;
22 justify-content: center;
23 align-items: center;
24 width: $index-block-height;
25 height: $index-block-height;
26 border-radius: 100px;
27 border: 2px solid $grey-color;
28 margin-bottom: 10px;
29
30 my-global-icon {
31 @include apply-svg-color(var(--mainBackgroundColor));
32
33 width: 22px;
34 height: 22px;
35 }
36 }
37
38 .step-label {
39 width: max-content;
40 }
41
42 &.active,
43 &.completed {
44 .step-index {
45 border-color: var(--mainColor);
46 background-color: var(--mainColor);
47 color: var(--mainBackgroundColor);
48 }
49
50 .step-label {
51 color: var(--mainColor);
52 }
53 }
54
55 &.completed {
56 cursor: pointer;
57 }
58 }
59
60 .connector {
61 flex: auto;
62 margin: $index-block-height/2 10px 0 10px;
63 height: 2px;
64 background-color: $grey-color;
65 }
66}
diff --git a/client/src/app/+signup/+register/custom-stepper.component.ts b/client/src/app/+signup/+register/custom-stepper.component.ts
new file mode 100644
index 000000000..2ae40f3a9
--- /dev/null
+++ b/client/src/app/+signup/+register/custom-stepper.component.ts
@@ -0,0 +1,19 @@
1import { Component } from '@angular/core'
2import { CdkStep, CdkStepper } from '@angular/cdk/stepper'
3
4@Component({
5 selector: 'my-custom-stepper',
6 templateUrl: './custom-stepper.component.html',
7 styleUrls: [ './custom-stepper.component.scss' ],
8 providers: [ { provide: CdkStepper, useExisting: CustomStepperComponent } ]
9})
10export class CustomStepperComponent extends CdkStepper {
11
12 onClick (index: number): void {
13 this.selectedIndex = index
14 }
15
16 isCompleted (step: CdkStep) {
17 return step.stepControl && step.stepControl.dirty && step.stepControl.valid
18 }
19}
diff --git a/client/src/app/signup/signup-routing.module.ts b/client/src/app/+signup/+register/register-routing.module.ts
index 820d16d4d..e3a5001dc 100644
--- a/client/src/app/signup/signup-routing.module.ts
+++ b/client/src/app/+signup/+register/register-routing.module.ts
@@ -1,17 +1,18 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core' 3import { MetaGuard } from '@ngx-meta/core'
4import { SignupComponent } from './signup.component' 4import { RegisterComponent } from './register.component'
5import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' 5import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service'
6import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
6 7
7const signupRoutes: Routes = [ 8const registerRoutes: Routes = [
8 { 9 {
9 path: 'signup', 10 path: '',
10 component: SignupComponent, 11 component: RegisterComponent,
11 canActivate: [ MetaGuard ], 12 canActivate: [ MetaGuard, UnloggedGuard ],
12 data: { 13 data: {
13 meta: { 14 meta: {
14 title: 'Signup' 15 title: 'Register'
15 } 16 }
16 }, 17 },
17 resolve: { 18 resolve: {
@@ -21,7 +22,7 @@ const signupRoutes: Routes = [
21] 22]
22 23
23@NgModule({ 24@NgModule({
24 imports: [ RouterModule.forChild(signupRoutes) ], 25 imports: [ RouterModule.forChild(registerRoutes) ],
25 exports: [ RouterModule ] 26 exports: [ RouterModule ]
26}) 27})
27export class SignupRoutingModule {} 28export class RegisterRoutingModule {}
diff --git a/client/src/app/+signup/+register/register-step-channel.component.html b/client/src/app/+signup/+register/register-step-channel.component.html
new file mode 100644
index 000000000..68ea4473a
--- /dev/null
+++ b/client/src/app/+signup/+register/register-step-channel.component.html
@@ -0,0 +1,50 @@
1<form role="form" [formGroup]="form">
2
3 <div class="channel-explanations">
4 <p i18n>
5 A channel is an entity in which you upload your videos. Creating several of them helps you to organize and separate your content.<br />
6 For example, you could decide to have a channel to publish your piano concerts, and another channel in which you publish your videos talking about ecology.
7 </p>
8
9 <p>
10 Other users can decide to subscribe any channel they want, to be notified when you publish a new video.
11 </p>
12 </div>
13
14 <div class="form-group">
15 <label for="name" i18n>Channel name</label>
16
17 <div class="input-group">
18 <input
19 type="text" id="name" i18n-placeholder placeholder="Example: my_super_channel"
20 formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }"
21 >
22 <div class="input-group-append">
23 <span class="input-group-text">@{{ instanceHost }}</span>
24 </div>
25 </div>
26
27 <div *ngIf="formErrors.name" class="form-error">
28 {{ formErrors.name }}
29 </div>
30
31 <div *ngIf="isSameThanUsername()" class="form-error" i18n>
32 Channel name cannot be the same than your account name. You can click on the first step to update your account name.
33 </div>
34 </div>
35
36 <div class="form-group">
37 <label for="displayName" i18n>Channel display name</label>
38
39 <div class="input-group">
40 <input
41 type="text" id="displayName"
42 formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
43 >
44 </div>
45
46 <div *ngIf="formErrors.displayName" class="form-error">
47 {{ formErrors.displayName }}
48 </div>
49 </div>
50</form>
diff --git a/client/src/app/+signup/+register/register-step-channel.component.ts b/client/src/app/+signup/+register/register-step-channel.component.ts
new file mode 100644
index 000000000..9e13f75b3
--- /dev/null
+++ b/client/src/app/+signup/+register/register-step-channel.component.ts
@@ -0,0 +1,40 @@
1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import { AuthService } from '@app/core'
3import { FormReactive, VideoChannelValidatorsService } from '@app/shared'
4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
5import { FormGroup } from '@angular/forms'
6
7@Component({
8 selector: 'my-register-step-channel',
9 templateUrl: './register-step-channel.component.html',
10 styleUrls: [ './register.component.scss' ]
11})
12export class RegisterStepChannelComponent extends FormReactive implements OnInit {
13 @Input() username: string
14 @Output() formBuilt = new EventEmitter<FormGroup>()
15
16 constructor (
17 protected formValidatorService: FormValidatorService,
18 private authService: AuthService,
19 private videoChannelValidatorsService: VideoChannelValidatorsService
20 ) {
21 super()
22 }
23
24 get instanceHost () {
25 return window.location.host
26 }
27
28 isSameThanUsername () {
29 return this.username && this.username === this.form.value['name']
30 }
31
32 ngOnInit () {
33 this.buildForm({
34 name: this.videoChannelValidatorsService.VIDEO_CHANNEL_NAME,
35 displayName: this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME
36 })
37
38 setTimeout(() => this.formBuilt.emit(this.form))
39 }
40}
diff --git a/client/src/app/+signup/+register/register-step-user.component.html b/client/src/app/+signup/+register/register-step-user.component.html
new file mode 100644
index 000000000..cd0c78bfa
--- /dev/null
+++ b/client/src/app/+signup/+register/register-step-user.component.html
@@ -0,0 +1,54 @@
1<form role="form" [formGroup]="form">
2
3 <div class="form-group">
4 <label for="username" i18n>Username</label>
5
6 <div class="input-group">
7 <input
8 type="text" id="username" i18n-placeholder placeholder="Example: jane_doe"
9 formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
10 >
11 <div class="input-group-append">
12 <span class="input-group-text">@{{ instanceHost }}</span>
13 </div>
14 </div>
15
16 <div *ngIf="formErrors.username" class="form-error">
17 {{ formErrors.username }}
18 </div>
19 </div>
20
21 <div class="form-group">
22 <label for="email" i18n>Email</label>
23 <input
24 type="text" id="email" i18n-placeholder placeholder="Email"
25 formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }"
26 >
27 <div *ngIf="formErrors.email" class="form-error">
28 {{ formErrors.email }}
29 </div>
30 </div>
31
32 <div class="form-group">
33 <label for="password" i18n>Password</label>
34 <input
35 type="password" id="password" i18n-placeholder placeholder="Password"
36 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
37 >
38 <div *ngIf="formErrors.password" class="form-error">
39 {{ formErrors.password }}
40 </div>
41 </div>
42
43 <div class="form-group form-group-terms">
44 <my-peertube-checkbox
45 inputName="terms" formControlName="terms"
46 i18n-labelHtml
47 labelHtml="I am at least 16 years old and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance"
48 ></my-peertube-checkbox>
49
50 <div *ngIf="formErrors.terms" class="form-error">
51 {{ formErrors.terms }}
52 </div>
53 </div>
54</form>
diff --git a/client/src/app/+signup/+register/register-step-user.component.ts b/client/src/app/+signup/+register/register-step-user.component.ts
new file mode 100644
index 000000000..3825ae371
--- /dev/null
+++ b/client/src/app/+signup/+register/register-step-user.component.ts
@@ -0,0 +1,37 @@
1import { Component, EventEmitter, OnInit, Output } from '@angular/core'
2import { AuthService } from '@app/core'
3import { FormReactive, UserValidatorsService } from '@app/shared'
4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
5import { FormGroup } from '@angular/forms'
6
7@Component({
8 selector: 'my-register-step-user',
9 templateUrl: './register-step-user.component.html',
10 styleUrls: [ './register.component.scss' ]
11})
12export class RegisterStepUserComponent extends FormReactive implements OnInit {
13 @Output() formBuilt = new EventEmitter<FormGroup>()
14
15 constructor (
16 protected formValidatorService: FormValidatorService,
17 private authService: AuthService,
18 private userValidatorsService: UserValidatorsService
19 ) {
20 super()
21 }
22
23 get instanceHost () {
24 return window.location.host
25 }
26
27 ngOnInit () {
28 this.buildForm({
29 username: this.userValidatorsService.USER_USERNAME,
30 password: this.userValidatorsService.USER_PASSWORD,
31 email: this.userValidatorsService.USER_EMAIL,
32 terms: this.userValidatorsService.USER_TERMS
33 })
34
35 setTimeout(() => this.formBuilt.emit(this.form))
36 }
37}
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html
new file mode 100644
index 000000000..24def68c1
--- /dev/null
+++ b/client/src/app/+signup/+register/register.component.html
@@ -0,0 +1,41 @@
1<div class="margin-content">
2
3 <div i18n class="title-page title-page-single">
4 Create an account
5 </div>
6
7 <my-signup-success *ngIf="signupDone" [message]="success"></my-signup-success>
8 <div *ngIf="info" class="alert alert-info">{{ info }}</div>
9
10 <div class="wrapper" *ngIf="!signupDone">
11 <div>
12 <my-custom-stepper linear *ngIf="!signupDone">
13 <cdk-step [stepControl]="formStepUser" i18n-label label="User information">
14 <my-register-step-user (formBuilt)="onUserFormBuilt($event)"></my-register-step-user>
15
16 <button i18n cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid">Next</button>
17 </cdk-step>
18
19 <cdk-step [stepControl]="formStepChannel" i18n-label label="Channel information">
20 <my-register-step-channel (formBuilt)="onChannelFormBuilt($event)" [username]="getUsername()"></my-register-step-channel>
21
22 <button i18n cdkStepperNext (click)="signup()"
23 [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()"
24 >
25 Create my account
26 </button>
27 </cdk-step>
28
29 <cdk-step i18n-label label="Done" editable="false">
30 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
31 </cdk-step>
32 </my-custom-stepper>
33 </div>
34
35 <div>
36 <label i18n>Features found on this instance</label>
37 <my-instance-features-table></my-instance-features-table>
38 </div>
39 </div>
40
41</div>
diff --git a/client/src/app/signup/signup.component.scss b/client/src/app/+signup/+register/register.component.scss
index 90e1e8e74..6f61b78f7 100644
--- a/client/src/app/signup/signup.component.scss
+++ b/client/src/app/+signup/+register/register.component.scss
@@ -1,16 +1,32 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.alert {
5 font-size: 15px;
6 text-align: center;
7}
8
9.wrapper {
10 display: flex;
11 justify-content: space-between;
12 flex-wrap: wrap;
13
14 & > div {
15 margin-bottom: 40px;
16 width: 450px;
17
18 @media screen and (max-width: 500px) {
19 width: auto;
20 }
21 }
22}
23
4my-instance-features-table { 24my-instance-features-table {
5 display: block; 25 display: block;
6 26
7 margin-bottom: 40px; 27 margin-bottom: 40px;
8} 28}
9 29
10form {
11 margin: 0 60px 40px 0;
12}
13
14.form-group-terms { 30.form-group-terms {
15 margin: 30px 0; 31 margin: 30px 0;
16} 32}
@@ -25,15 +41,18 @@ form {
25 41
26input:not([type=submit]) { 42input:not([type=submit]) {
27 @include peertube-input-text(400px); 43 @include peertube-input-text(400px);
44
28 display: block; 45 display: block;
29 46
30 &#username { 47 &#username,
31 width: auto; 48 &#name {
49 width: auto !important;
32 flex-grow: 1; 50 flex-grow: 1;
33 } 51 }
34} 52}
35 53
36input[type=submit] { 54input[type=submit],
55button {
37 @include peertube-button; 56 @include peertube-button;
38 @include orange-button; 57 @include orange-button;
39} 58}
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts
new file mode 100644
index 000000000..cd6059728
--- /dev/null
+++ b/client/src/app/+signup/+register/register.component.ts
@@ -0,0 +1,89 @@
1import { Component } from '@angular/core'
2import { AuthService, Notifier, RedirectService, ServerService } from '@app/core'
3import { UserService, UserValidatorsService } from '@app/shared'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { UserRegister } from '@shared/models/users/user-register.model'
6import { FormGroup } from '@angular/forms'
7
8@Component({
9 selector: 'my-register',
10 templateUrl: './register.component.html',
11 styleUrls: [ './register.component.scss' ]
12})
13export class RegisterComponent {
14 info: string = null
15 error: string = null
16 success: string = null
17 signupDone = false
18
19 formStepUser: FormGroup
20 formStepChannel: FormGroup
21
22 constructor (
23 private authService: AuthService,
24 private userValidatorsService: UserValidatorsService,
25 private notifier: Notifier,
26 private userService: UserService,
27 private serverService: ServerService,
28 private redirectService: RedirectService,
29 private i18n: I18n
30 ) {
31 }
32
33 get requiresEmailVerification () {
34 return this.serverService.getConfig().signup.requiresEmailVerification
35 }
36
37 hasSameChannelAndAccountNames () {
38 return this.getUsername() === this.getChannelName()
39 }
40
41 getUsername () {
42 if (!this.formStepUser) return undefined
43
44 return this.formStepUser.value['username']
45 }
46
47 getChannelName () {
48 if (!this.formStepChannel) return undefined
49
50 return this.formStepChannel.value['name']
51 }
52
53 onUserFormBuilt (form: FormGroup) {
54 this.formStepUser = form
55 }
56
57 onChannelFormBuilt (form: FormGroup) {
58 this.formStepChannel = form
59 }
60
61 signup () {
62 this.error = null
63
64 const body: UserRegister = Object.assign(this.formStepUser.value, { channel: this.formStepChannel.value })
65
66 this.userService.signup(body).subscribe(
67 () => {
68 this.signupDone = true
69
70 if (this.requiresEmailVerification) {
71 this.info = this.i18n('Now please check your emails to verify your account and complete signup.')
72 return
73 }
74
75 // Auto login
76 this.authService.login(body.username, body.password)
77 .subscribe(
78 () => {
79 this.success = this.i18n('You are now logged in as {{username}}!', { username: body.username })
80 },
81
82 err => this.error = err.message
83 )
84 },
85
86 err => this.error = err.message
87 )
88 }
89}
diff --git a/client/src/app/+signup/+register/register.module.ts b/client/src/app/+signup/+register/register.module.ts
new file mode 100644
index 000000000..46336cbd0
--- /dev/null
+++ b/client/src/app/+signup/+register/register.module.ts
@@ -0,0 +1,33 @@
1import { NgModule } from '@angular/core'
2import { RegisterRoutingModule } from './register-routing.module'
3import { RegisterComponent } from './register.component'
4import { SharedModule } from '@app/shared'
5import { CdkStepperModule } from '@angular/cdk/stepper'
6import { RegisterStepChannelComponent } from './register-step-channel.component'
7import { RegisterStepUserComponent } from './register-step-user.component'
8import { CustomStepperComponent } from './custom-stepper.component'
9import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module'
10
11@NgModule({
12 imports: [
13 RegisterRoutingModule,
14 SharedModule,
15 CdkStepperModule,
16 SignupSharedModule
17 ],
18
19 declarations: [
20 RegisterComponent,
21 CustomStepperComponent,
22 RegisterStepChannelComponent,
23 RegisterStepUserComponent
24 ],
25
26 exports: [
27 RegisterComponent
28 ],
29
30 providers: [
31 ]
32})
33export class RegisterModule { }
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html
index 2e4180632..2e4180632 100644
--- a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html
+++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss
index efec6b706..efec6b706 100644
--- a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss
+++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
index cfd471fa4..cfd471fa4 100644
--- a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
+++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html
index a83d4a3c2..728709ca6 100644
--- a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html
+++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html
@@ -3,9 +3,9 @@
3 Verify account email confirmation 3 Verify account email confirmation
4 </div> 4 </div>
5 5
6 <div i18n *ngIf="success; else verificationError"> 6 <my-signup-success i18n *ngIf="success; else verificationError" message="Your email has been verified and you may now login.">
7 Your email has been verified and you may now login. Redirecting... 7 </my-signup-success>
8 </div> 8
9 <ng-template #verificationError> 9 <ng-template #verificationError>
10 <div> 10 <div>
11 <span i18n>An error occurred. </span> 11 <span i18n>An error occurred. </span>
diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts
index f9ecf664b..3fb2d1cd8 100644
--- a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts
+++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts
@@ -40,9 +40,6 @@ export class VerifyAccountEmailComponent implements OnInit {
40 .subscribe( 40 .subscribe(
41 () => { 41 () => {
42 this.success = true 42 this.success = true
43 setTimeout(() => {
44 this.router.navigate([ '/login' ])
45 }, 2000)
46 }, 43 },
47 44
48 err => { 45 err => {
diff --git a/client/src/app/+verify-account/verify-account-routing.module.ts b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts
index a038f0336..16d5fe0d0 100644
--- a/client/src/app/+verify-account/verify-account-routing.module.ts
+++ b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts
@@ -1,12 +1,8 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3
4import { MetaGuard } from '@ngx-meta/core' 3import { MetaGuard } from '@ngx-meta/core'
5 4import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component'
6import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component' 5import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component'
7import {
8 VerifyAccountAskSendEmailComponent
9} from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component'
10 6
11const verifyAccountRoutes: Routes = [ 7const verifyAccountRoutes: Routes = [
12 { 8 {
diff --git a/client/src/app/+signup/+verify-account/verify-account.module.ts b/client/src/app/+signup/+verify-account/verify-account.module.ts
new file mode 100644
index 000000000..9fe14e81e
--- /dev/null
+++ b/client/src/app/+signup/+verify-account/verify-account.module.ts
@@ -0,0 +1,25 @@
1import { NgModule } from '@angular/core'
2import { VerifyAccountRoutingModule } from './verify-account-routing.module'
3import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component'
4import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component'
5import { SharedModule } from '@app/shared'
6import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module'
7
8@NgModule({
9 imports: [
10 VerifyAccountRoutingModule,
11 SharedModule,
12 SignupSharedModule
13 ],
14
15 declarations: [
16 VerifyAccountEmailComponent,
17 VerifyAccountAskSendEmailComponent
18 ],
19
20 exports: [],
21
22 providers: []
23})
24export class VerifyAccountModule {
25}
diff --git a/client/src/app/+signup/shared/signup-shared.module.ts b/client/src/app/+signup/shared/signup-shared.module.ts
new file mode 100644
index 000000000..cd21fdef3
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-shared.module.ts
@@ -0,0 +1,21 @@
1import { NgModule } from '@angular/core'
2import { SignupSuccessComponent } from '../shared/signup-success.component'
3import { SharedModule } from '@app/shared'
4
5@NgModule({
6 imports: [
7 SharedModule
8 ],
9
10 declarations: [
11 SignupSuccessComponent
12 ],
13
14 exports: [
15 SignupSuccessComponent
16 ],
17
18 providers: [
19 ]
20})
21export class SignupSharedModule { }
diff --git a/client/src/app/+signup/shared/signup-success.component.html b/client/src/app/+signup/shared/signup-success.component.html
new file mode 100644
index 000000000..e35f858c6
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-success.component.html
@@ -0,0 +1,16 @@
1<!-- Thanks: Amit Singh Sansoya from https://codepen.io/amit3200/pen/zWMJOO -->
2
3<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130.2 130.2">
4 <circle class="path circle" fill="none" stroke="#73AF55" stroke-width="6" stroke-miterlimit="10" cx="65.1" cy="65.1" r="62.1"/>
5 <polyline class="path check" fill="none" stroke="#73AF55" stroke-width="6" stroke-linecap="round" stroke-miterlimit="10" points="100.2,40.2 51.5,88.8 29.8,67.5 "/>
6</svg>
7
8<p class="bottom-message">Welcome on PeerTube!</p>
9
10<div *ngIf="message" class="alert alert-success">
11 <p>{{ message }}</p>
12
13 <p i18n>
14 If you need help to use PeerTube, you can take a look to the <a href="https://docs.joinpeertube.org/#/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>.
15 </p>
16</div>
diff --git a/client/src/app/+signup/shared/signup-success.component.scss b/client/src/app/+signup/shared/signup-success.component.scss
new file mode 100644
index 000000000..fbc27c8bc
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-success.component.scss
@@ -0,0 +1,76 @@
1svg {
2 width: 100px;
3 display: block;
4 margin: 40px auto 0;
5}
6
7.path {
8 stroke-dasharray: 1000;
9 stroke-dashoffset: 0;
10
11 &.circle {
12 -webkit-animation: dash .9s ease-in-out;
13 animation: dash .9s ease-in-out;
14 }
15
16 &.line {
17 stroke-dashoffset: 1000;
18 -webkit-animation: dash .9s .35s ease-in-out forwards;
19 animation: dash .9s .35s ease-in-out forwards;
20 }
21
22 &.check {
23 stroke-dashoffset: -100;
24 -webkit-animation: dash-check .9s .35s ease-in-out forwards;
25 animation: dash-check .9s .35s ease-in-out forwards;
26 }
27}
28
29.bottom-message {
30 text-align: center;
31 margin: 20px 0 60px;
32 font-size: 1.25em;
33 color: #73AF55;
34}
35
36.alert {
37 font-size: 15px;
38 text-align: center;
39}
40
41
42@-webkit-keyframes dash {
43 0% {
44 stroke-dashoffset: 1000;
45 }
46 100% {
47 stroke-dashoffset: 0;
48 }
49}
50
51@keyframes dash {
52 0% {
53 stroke-dashoffset: 1000;
54 }
55 100% {
56 stroke-dashoffset: 0;
57 }
58}
59
60@-webkit-keyframes dash-check {
61 0% {
62 stroke-dashoffset: -100;
63 }
64 100% {
65 stroke-dashoffset: 900;
66 }
67}
68
69@keyframes dash-check {
70 0% {
71 stroke-dashoffset: -100;
72 }
73 100% {
74 stroke-dashoffset: 900;
75 }
76}
diff --git a/client/src/app/+signup/shared/signup-success.component.ts b/client/src/app/+signup/shared/signup-success.component.ts
new file mode 100644
index 000000000..19fb5922a
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-success.component.ts
@@ -0,0 +1,10 @@
1import { Component, Input } from '@angular/core'
2
3@Component({
4 selector: 'my-signup-success',
5 templateUrl: './signup-success.component.html',
6 styleUrls: [ './signup-success.component.scss' ]
7})
8export class SignupSuccessComponent {
9 @Input() message: string
10}
diff --git a/client/src/app/+verify-account/index.ts b/client/src/app/+verify-account/index.ts
deleted file mode 100644
index 733f5ba77..000000000
--- a/client/src/app/+verify-account/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1export * from '@app/+verify-account/verify-account-routing.module'
2export * from '@app/+verify-account/verify-account.module'
diff --git a/client/src/app/+verify-account/verify-account.module.ts b/client/src/app/+verify-account/verify-account.module.ts
deleted file mode 100644
index 9092c6b4f..000000000
--- a/client/src/app/+verify-account/verify-account.module.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import { NgModule } from '@angular/core'
2
3import { VerifyAccountRoutingModule } from '@app/+verify-account/verify-account-routing.module'
4import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component'
5import {
6 VerifyAccountAskSendEmailComponent
7} from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component'
8import { SharedModule } from '@app/shared'
9
10@NgModule({
11 imports: [
12 VerifyAccountRoutingModule,
13 SharedModule
14 ],
15
16 declarations: [
17 VerifyAccountEmailComponent,
18 VerifyAccountAskSendEmailComponent
19 ],
20
21 exports: [
22 ],
23
24 providers: [
25 ]
26})
27export class VerifyAccountModule { }
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
index 907aefae1..7990044a2 100644
--- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
+++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
@@ -5,7 +5,7 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
5import { Subscription } from 'rxjs' 5import { Subscription } from 'rxjs'
6import { Notifier } from '@app/core' 6import { Notifier } from '@app/core'
7import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' 7import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
8import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 8import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model'
9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
10 10
11@Component({ 11@Component({
@@ -46,8 +46,7 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy {
46 } 46 }
47 47
48 onNearOfBottom () { 48 onNearOfBottom () {
49 // Last page 49 if (!hasMoreItems(this.pagination)) return
50 if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
51 50
52 this.pagination.currentPage += 1 51 this.pagination.currentPage += 1
53 this.loadVideoPlaylists() 52 this.loadVideoPlaylists()
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
index 5e60b34b4..629fd4450 100644
--- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
+++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
@@ -29,6 +29,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
29 private videoChannelSub: Subscription 29 private videoChannelSub: Subscription
30 30
31 constructor ( 31 constructor (
32 protected i18n: I18n,
32 protected router: Router, 33 protected router: Router,
33 protected serverService: ServerService, 34 protected serverService: ServerService,
34 protected route: ActivatedRoute, 35 protected route: ActivatedRoute,
@@ -36,7 +37,6 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
36 protected notifier: Notifier, 37 protected notifier: Notifier,
37 protected confirmService: ConfirmService, 38 protected confirmService: ConfirmService,
38 protected screenService: ScreenService, 39 protected screenService: ScreenService,
39 private i18n: I18n,
40 private videoChannelService: VideoChannelService, 40 private videoChannelService: VideoChannelService,
41 private videoService: VideoService 41 private videoService: VideoService
42 ) { 42 ) {
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts
index db8888dba..7ca51f226 100644
--- a/client/src/app/app-routing.module.ts
+++ b/client/src/app/app-routing.module.ts
@@ -16,7 +16,7 @@ const routes: Routes = [
16 }, 16 },
17 { 17 {
18 path: 'verify-account', 18 path: 'verify-account',
19 loadChildren: './+verify-account/verify-account.module#VerifyAccountModule' 19 loadChildren: './+signup/+verify-account/verify-account.module#VerifyAccountModule'
20 }, 20 },
21 { 21 {
22 path: 'accounts', 22 path: 'accounts',
@@ -31,6 +31,10 @@ const routes: Routes = [
31 loadChildren: './+about/about.module#AboutModule' 31 loadChildren: './+about/about.module#AboutModule'
32 }, 32 },
33 { 33 {
34 path: 'signup',
35 loadChildren: './+signup/+register/register.module#RegisterModule'
36 },
37 {
34 path: '', 38 path: '',
35 component: AppComponent // Avoid 404, app component will redirect dynamically 39 component: AppComponent // Avoid 404, app component will redirect dynamically
36 }, 40 },
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 0bbc2e08b..1e2936a37 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -14,7 +14,6 @@ import { HeaderComponent } from './header'
14import { LoginModule } from './login' 14import { LoginModule } from './login'
15import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' 15import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu'
16import { SharedModule } from './shared' 16import { SharedModule } from './shared'
17import { SignupModule } from './signup'
18import { VideosModule } from './videos' 17import { VideosModule } from './videos'
19import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' 18import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n'
20import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 19import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
@@ -53,7 +52,6 @@ export function metaFactory (serverService: ServerService): MetaLoader {
53 CoreModule, 52 CoreModule,
54 LoginModule, 53 LoginModule,
55 ResetPasswordModule, 54 ResetPasswordModule,
56 SignupModule,
57 SearchModule, 55 SearchModule,
58 SharedModule, 56 SharedModule,
59 VideosModule, 57 VideosModule,
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index d3e72afb4..06fa8fcf1 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -20,6 +20,7 @@ import { Notifier } from './notification'
20import { MessageService } from 'primeng/api' 20import { MessageService } from 'primeng/api'
21import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' 21import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
22import { ServerConfigResolver } from './routing/server-config-resolver.service' 22import { ServerConfigResolver } from './routing/server-config-resolver.service'
23import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
23 24
24@NgModule({ 25@NgModule({
25 imports: [ 26 imports: [
@@ -58,6 +59,8 @@ import { ServerConfigResolver } from './routing/server-config-resolver.service'
58 ThemeService, 59 ThemeService,
59 LoginGuard, 60 LoginGuard,
60 UserRightGuard, 61 UserRightGuard,
62 UnloggedGuard,
63
61 RedirectService, 64 RedirectService,
62 Notifier, 65 Notifier,
63 MessageService, 66 MessageService,
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts
index e1db4097b..571822b76 100644
--- a/client/src/app/core/routing/redirect.service.ts
+++ b/client/src/app/core/routing/redirect.service.ts
@@ -42,7 +42,14 @@ export class RedirectService {
42 } 42 }
43 43
44 redirectToPreviousRoute () { 44 redirectToPreviousRoute () {
45 if (this.previousUrl) return this.router.navigateByUrl(this.previousUrl) 45 const exceptions = [
46 '/verify-account'
47 ]
48
49 if (this.previousUrl) {
50 const isException = exceptions.find(e => this.previousUrl.startsWith(e))
51 if (!isException) return this.router.navigateByUrl(this.previousUrl)
52 }
46 53
47 return this.redirectToHomepage() 54 return this.redirectToHomepage()
48 } 55 }
diff --git a/client/src/app/core/routing/unlogged-guard.service.ts b/client/src/app/core/routing/unlogged-guard.service.ts
new file mode 100644
index 000000000..3132a1a77
--- /dev/null
+++ b/client/src/app/core/routing/unlogged-guard.service.ts
@@ -0,0 +1,25 @@
1import { Injectable } from '@angular/core'
2import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router'
3import { AuthService } from '../auth/auth.service'
4import { RedirectService } from './redirect.service'
5
6@Injectable()
7export class UnloggedGuard implements CanActivate, CanActivateChild {
8
9 constructor (
10 private router: Router,
11 private auth: AuthService,
12 private redirectService: RedirectService
13 ) {}
14
15 canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
16 if (this.auth.isLoggedIn() === false) return true
17
18 this.redirectService.redirectToHomepage()
19 return false
20 }
21
22 canActivateChild (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
23 return this.canActivate(route, state)
24 }
25}
diff --git a/client/src/app/shared/actor/actor.model.ts b/client/src/app/shared/actor/actor.model.ts
index adecec1fc..5a517c975 100644
--- a/client/src/app/shared/actor/actor.model.ts
+++ b/client/src/app/shared/actor/actor.model.ts
@@ -4,7 +4,6 @@ import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
4 4
5export abstract class Actor implements ActorServer { 5export abstract class Actor implements ActorServer {
6 id: number 6 id: number
7 uuid: string
8 url: string 7 url: string
9 name: string 8 name: string
10 host: string 9 host: string
@@ -35,7 +34,6 @@ export abstract class Actor implements ActorServer {
35 34
36 protected constructor (hash: ActorServer) { 35 protected constructor (hash: ActorServer) {
37 this.id = hash.id 36 this.id = hash.id
38 this.uuid = hash.uuid
39 this.url = hash.url 37 this.url = hash.url
40 this.name = hash.name 38 this.name = hash.name
41 this.host = hash.host 39 this.host = hash.host
diff --git a/client/src/app/shared/buttons/button.component.scss b/client/src/app/shared/buttons/button.component.scss
index 04199a2a9..99d7f51c1 100644
--- a/client/src/app/shared/buttons/button.component.scss
+++ b/client/src/app/shared/buttons/button.component.scss
@@ -5,16 +5,9 @@
5 @include peertube-button-link; 5 @include peertube-button-link;
6 @include button-with-icon(21px, 0, -2px); 6 @include button-with-icon(21px, 0, -2px);
7 7
8 font-weight: $font-semibold; 8 // FIXME: Firefox does not apply global .orange-button icon color
9 color: $grey-foreground-color; 9 &.orange-button {
10 background-color: $grey-background-color; 10 @include apply-svg-color(#fff)
11
12 &:hover {
13 background-color: $grey-background-hover-color;
14 }
15
16 my-global-icon {
17 @include apply-svg-color($grey-foreground-color);
18 } 11 }
19} 12}
20 13
diff --git a/client/src/app/shared/buttons/button.component.ts b/client/src/app/shared/buttons/button.component.ts
index c2b69d31a..cf334e8d5 100644
--- a/client/src/app/shared/buttons/button.component.ts
+++ b/client/src/app/shared/buttons/button.component.ts
@@ -9,7 +9,7 @@ import { GlobalIconName } from '@app/shared/images/global-icon.component'
9 9
10export class ButtonComponent { 10export class ButtonComponent {
11 @Input() label = '' 11 @Input() label = ''
12 @Input() className: string = undefined 12 @Input() className = 'grey-button'
13 @Input() icon: GlobalIconName = undefined 13 @Input() icon: GlobalIconName = undefined
14 @Input() title: string = undefined 14 @Input() title: string = undefined
15 15
diff --git a/client/src/app/shared/buttons/delete-button.component.html b/client/src/app/shared/buttons/delete-button.component.html
index b4acb9d32..25196fbd5 100644
--- a/client/src/app/shared/buttons/delete-button.component.html
+++ b/client/src/app/shared/buttons/delete-button.component.html
@@ -1,4 +1,4 @@
1<span class="action-button action-button-delete" [title]="title" role="button"> 1<span class="action-button action-button-delete grey-button" [title]="title" role="button">
2 <my-global-icon iconName="delete"></my-global-icon> 2 <my-global-icon iconName="delete"></my-global-icon>
3 3
4 <span class="button-label" *ngIf="label">{{ label }}</span> 4 <span class="button-label" *ngIf="label">{{ label }}</span>
diff --git a/client/src/app/shared/buttons/edit-button.component.html b/client/src/app/shared/buttons/edit-button.component.html
index da3addbae..3d7cd4780 100644
--- a/client/src/app/shared/buttons/edit-button.component.html
+++ b/client/src/app/shared/buttons/edit-button.component.html
@@ -1,4 +1,4 @@
1<a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit"> 1<a class="action-button action-button-edit grey-button" [routerLink]="routerLink" i18n-title title="Edit">
2 <my-global-icon iconName="edit"></my-global-icon> 2 <my-global-icon iconName="edit"></my-global-icon>
3 3
4 <span class="button-label" *ngIf="label">{{ label }}</span> 4 <span class="button-label" *ngIf="label">{{ label }}</span>
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.scss b/client/src/app/shared/forms/peertube-checkbox.component.scss
index ea321ee65..84ea788af 100644
--- a/client/src/app/shared/forms/peertube-checkbox.component.scss
+++ b/client/src/app/shared/forms/peertube-checkbox.component.scss
@@ -14,9 +14,6 @@
14 14
15 input { 15 input {
16 @include peertube-checkbox(1px); 16 @include peertube-checkbox(1px);
17
18 width: 10px;
19 margin-right: 10px;
20 } 17 }
21 } 18 }
22 19
diff --git a/client/src/app/shared/forms/reactive-file.component.html b/client/src/app/shared/forms/reactive-file.component.html
index 7d691059d..f6bf5f9ae 100644
--- a/client/src/app/shared/forms/reactive-file.component.html
+++ b/client/src/app/shared/forms/reactive-file.component.html
@@ -1,6 +1,9 @@
1<div class="root"> 1<div class="root">
2 <div class="button-file"> 2 <div class="button-file" [ngClass]="{ 'with-icon': !!icon }">
3 <my-global-icon *ngIf="icon" [iconName]="icon"></my-global-icon>
4
3 <span>{{ inputLabel }}</span> 5 <span>{{ inputLabel }}</span>
6
4 <input 7 <input
5 type="file" 8 type="file"
6 [name]="inputName" [id]="inputName" [accept]="extensions" 9 [name]="inputName" [id]="inputName" [accept]="extensions"
@@ -8,7 +11,5 @@
8 /> 11 />
9 </div> 12 </div>
10 13
11 <div i18n class="file-constraints">(extensions: {{ allowedExtensionsMessage }}, max size: {{ maxFileSize | bytes }})</div>
12
13 <div class="filename" *ngIf="displayFilename === true && filename">{{ filename }}</div> 14 <div class="filename" *ngIf="displayFilename === true && filename">{{ filename }}</div>
14</div> 15</div>
diff --git a/client/src/app/shared/forms/reactive-file.component.scss b/client/src/app/shared/forms/reactive-file.component.scss
index d89844264..84c23c1d6 100644
--- a/client/src/app/shared/forms/reactive-file.component.scss
+++ b/client/src/app/shared/forms/reactive-file.component.scss
@@ -8,13 +8,11 @@
8 8
9 .button-file { 9 .button-file {
10 @include peertube-button-file(auto); 10 @include peertube-button-file(auto);
11 @include grey-button;
11 12
12 min-width: 190px; 13 &.with-icon {
13 } 14 @include button-with-icon;
14 15 }
15 .file-constraints {
16 margin-left: 5px;
17 font-size: 13px;
18 } 16 }
19 17
20 .filename { 18 .filename {
diff --git a/client/src/app/shared/forms/reactive-file.component.ts b/client/src/app/shared/forms/reactive-file.component.ts
index f60c38e8d..b7a821d4f 100644
--- a/client/src/app/shared/forms/reactive-file.component.ts
+++ b/client/src/app/shared/forms/reactive-file.component.ts
@@ -2,6 +2,7 @@ import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@ang
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { GlobalIconName } from '@app/shared/images/global-icon.component'
5 6
6@Component({ 7@Component({
7 selector: 'my-reactive-file', 8 selector: 'my-reactive-file',
@@ -21,6 +22,7 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
21 @Input() extensions: string[] = [] 22 @Input() extensions: string[] = []
22 @Input() maxFileSize: number 23 @Input() maxFileSize: number
23 @Input() displayFilename = false 24 @Input() displayFilename = false
25 @Input() icon: GlobalIconName
24 26
25 @Output() fileChanged = new EventEmitter<Blob>() 27 @Output() fileChanged = new EventEmitter<Blob>()
26 28
diff --git a/client/src/app/shared/images/image-upload.component.html b/client/src/app/shared/images/image-upload.component.html
deleted file mode 100644
index c09c862c4..000000000
--- a/client/src/app/shared/images/image-upload.component.html
+++ /dev/null
@@ -1,9 +0,0 @@
1<div class="root">
2 <my-reactive-file
3 [inputName]="inputName" [inputLabel]="inputLabel" [extensions]="videoImageExtensions" [maxFileSize]="maxVideoImageSize"
4 (fileChanged)="onFileChanged($event)"
5 ></my-reactive-file>
6
7 <img *ngIf="imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" [src]="imageSrc" class="preview" />
8 <div *ngIf="!imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" class="preview no-image"></div>
9</div>
diff --git a/client/src/app/shared/images/image-upload.component.scss b/client/src/app/shared/images/image-upload.component.scss
deleted file mode 100644
index b63963bca..000000000
--- a/client/src/app/shared/images/image-upload.component.scss
+++ /dev/null
@@ -1,18 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4.root {
5 height: auto;
6 display: flex;
7 align-items: center;
8
9 .preview {
10 border: 2px solid grey;
11 border-radius: 4px;
12 margin-left: 50px;
13
14 &.no-image {
15 background-color: #ececec;
16 }
17 }
18}
diff --git a/client/src/app/shared/images/preview-upload.component.html b/client/src/app/shared/images/preview-upload.component.html
new file mode 100644
index 000000000..5e1d5211b
--- /dev/null
+++ b/client/src/app/shared/images/preview-upload.component.html
@@ -0,0 +1,13 @@
1<div class="root">
2 <div class="preview-container">
3 <my-reactive-file
4 [inputName]="inputName" [inputLabel]="inputLabel" [extensions]="videoImageExtensions" [maxFileSize]="maxVideoImageSize"
5 icon="edit" (fileChanged)="onFileChanged($event)"
6 ></my-reactive-file>
7
8 <img *ngIf="imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" [src]="imageSrc" class="preview" />
9 <div *ngIf="!imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" class="preview no-image"></div>
10 </div>
11
12 <div i18n class="file-constraints">(extensions: {{ allowedExtensionsMessage }}, max size: {{ maxVideoImageSize | bytes }})</div>
13</div>
diff --git a/client/src/app/shared/images/preview-upload.component.scss b/client/src/app/shared/images/preview-upload.component.scss
new file mode 100644
index 000000000..257060239
--- /dev/null
+++ b/client/src/app/shared/images/preview-upload.component.scss
@@ -0,0 +1,27 @@
1@import '_variables';
2@import '_mixins';
3
4.root {
5 height: auto;
6 display: flex;
7 flex-direction: column;
8
9 .preview-container {
10 position: relative;
11
12 my-reactive-file {
13 position: absolute;
14 bottom: 10px;
15 left: 10px;
16 }
17
18 .preview {
19 border: 2px solid grey;
20 border-radius: 4px;
21
22 &.no-image {
23 background-color: #ececec;
24 }
25 }
26 }
27}
diff --git a/client/src/app/shared/images/image-upload.component.ts b/client/src/app/shared/images/preview-upload.component.ts
index 2da1592ff..44b78866e 100644
--- a/client/src/app/shared/images/image-upload.component.ts
+++ b/client/src/app/shared/images/preview-upload.component.ts
@@ -1,27 +1,28 @@
1import { Component, forwardRef, Input } from '@angular/core' 1import { Component, forwardRef, Input, OnInit } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' 3import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
4import { ServerService } from '@app/core' 4import { ServerService } from '@app/core'
5 5
6@Component({ 6@Component({
7 selector: 'my-image-upload', 7 selector: 'my-preview-upload',
8 styleUrls: [ './image-upload.component.scss' ], 8 styleUrls: [ './preview-upload.component.scss' ],
9 templateUrl: './image-upload.component.html', 9 templateUrl: './preview-upload.component.html',
10 providers: [ 10 providers: [
11 { 11 {
12 provide: NG_VALUE_ACCESSOR, 12 provide: NG_VALUE_ACCESSOR,
13 useExisting: forwardRef(() => ImageUploadComponent), 13 useExisting: forwardRef(() => PreviewUploadComponent),
14 multi: true 14 multi: true
15 } 15 }
16 ] 16 ]
17}) 17})
18export class ImageUploadComponent implements ControlValueAccessor { 18export class PreviewUploadComponent implements OnInit, ControlValueAccessor {
19 @Input() inputLabel: string 19 @Input() inputLabel: string
20 @Input() inputName: string 20 @Input() inputName: string
21 @Input() previewWidth: string 21 @Input() previewWidth: string
22 @Input() previewHeight: string 22 @Input() previewHeight: string
23 23
24 imageSrc: SafeResourceUrl 24 imageSrc: SafeResourceUrl
25 allowedExtensionsMessage = ''
25 26
26 private file: File 27 private file: File
27 28
@@ -38,6 +39,10 @@ export class ImageUploadComponent implements ControlValueAccessor {
38 return this.serverService.getConfig().video.image.size.max 39 return this.serverService.getConfig().video.image.size.max
39 } 40 }
40 41
42 ngOnInit () {
43 this.allowedExtensionsMessage = this.videoImageExtensions.join(', ')
44 }
45
41 onFileChanged (file: File) { 46 onFileChanged (file: File) {
42 this.file = file 47 this.file = file
43 48
diff --git a/client/src/app/+admin/follows/shared/follow.service.ts b/client/src/app/shared/instance/follow.service.ts
index c2b8ef006..5a44c64f1 100644
--- a/client/src/app/+admin/follows/shared/follow.service.ts
+++ b/client/src/app/shared/instance/follow.service.ts
@@ -3,13 +3,13 @@ import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { SortMeta } from 'primeng/primeng' 4import { SortMeta } from 'primeng/primeng'
5import { Observable } from 'rxjs' 5import { Observable } from 'rxjs'
6import { ActorFollow, ResultList } from '../../../../../../shared' 6import { ActorFollow, ResultList } from '@shared/index'
7import { environment } from '../../../../environments/environment' 7import { environment } from '../../../environments/environment'
8import { RestExtractor, RestPagination, RestService } from '../../../shared' 8import { RestExtractor, RestPagination, RestService } from '../rest'
9 9
10@Injectable() 10@Injectable()
11export class FollowService { 11export class FollowService {
12 private static BASE_APPLICATION_URL = environment.apiUrl + '/api/v1/server' 12 private static BASE_APPLICATION_URL = 'https://peertube2.cpy.re' + '/api/v1/server'
13 13
14 constructor ( 14 constructor (
15 private authHttp: HttpClient, 15 private authHttp: HttpClient,
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index ded65653f..1d49c7bc8 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -69,7 +69,7 @@ import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/sha
69import { ConfirmComponent } from '@app/shared/confirm/confirm.component' 69import { ConfirmComponent } from '@app/shared/confirm/confirm.component'
70import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component' 70import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
71import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 71import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
72import { ImageUploadComponent } from '@app/shared/images/image-upload.component' 72import { PreviewUploadComponent } from '@app/shared/images/preview-upload.component'
73import { GlobalIconComponent } from '@app/shared/images/global-icon.component' 73import { GlobalIconComponent } from '@app/shared/images/global-icon.component'
74import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component' 74import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component'
75import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component' 75import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component'
@@ -85,6 +85,7 @@ import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklis
85import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' 85import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component'
86import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' 86import { VideoReportComponent } from '@app/shared/video/modals/video-report.component'
87import { ClipboardModule } from 'ngx-clipboard' 87import { ClipboardModule } from 'ngx-clipboard'
88import { FollowService } from '@app/shared/instance/follow.service'
88 89
89@NgModule({ 90@NgModule({
90 imports: [ 91 imports: [
@@ -154,7 +155,7 @@ import { ClipboardModule } from 'ngx-clipboard'
154 ConfirmComponent, 155 ConfirmComponent,
155 156
156 GlobalIconComponent, 157 GlobalIconComponent,
157 ImageUploadComponent 158 PreviewUploadComponent
158 ], 159 ],
159 160
160 exports: [ 161 exports: [
@@ -218,7 +219,7 @@ import { ClipboardModule } from 'ngx-clipboard'
218 ConfirmComponent, 219 ConfirmComponent,
219 220
220 GlobalIconComponent, 221 GlobalIconComponent,
221 ImageUploadComponent, 222 PreviewUploadComponent,
222 223
223 NumberFormatterPipe, 224 NumberFormatterPipe,
224 ObjectLengthPipe, 225 ObjectLengthPipe,
@@ -271,6 +272,8 @@ import { ClipboardModule } from 'ngx-clipboard'
271 272
272 UserNotificationService, 273 UserNotificationService,
273 274
275 FollowService,
276
274 I18n 277 I18n
275 ] 278 ]
276}) 279})
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts
index cc5c051f1..20883456f 100644
--- a/client/src/app/shared/users/user.service.ts
+++ b/client/src/app/shared/users/user.service.ts
@@ -9,6 +9,7 @@ import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
9import { SortMeta } from 'primeng/api' 9import { SortMeta } from 'primeng/api'
10import { BytesPipe } from 'ngx-pipes' 10import { BytesPipe } from 'ngx-pipes'
11import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { UserRegister } from '@shared/models/users/user-register.model'
12 13
13@Injectable() 14@Injectable()
14export class UserService { 15export class UserService {
@@ -64,7 +65,7 @@ export class UserService {
64 .pipe(catchError(err => this.restExtractor.handleError(err))) 65 .pipe(catchError(err => this.restExtractor.handleError(err)))
65 } 66 }
66 67
67 signup (userCreate: UserCreate) { 68 signup (userCreate: UserRegister) {
68 return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) 69 return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
69 .pipe( 70 .pipe(
70 map(this.restExtractor.extractDataBool), 71 map(this.restExtractor.extractDataBool),
diff --git a/client/src/app/shared/video-channel/video-channel.service.ts b/client/src/app/shared/video-channel/video-channel.service.ts
index d0bec649a..0168d37d9 100644
--- a/client/src/app/shared/video-channel/video-channel.service.ts
+++ b/client/src/app/shared/video-channel/video-channel.service.ts
@@ -2,7 +2,7 @@ import { catchError, map, tap } from 'rxjs/operators'
2import { Injectable } from '@angular/core' 2import { Injectable } from '@angular/core'
3import { Observable, ReplaySubject } from 'rxjs' 3import { Observable, ReplaySubject } from 'rxjs'
4import { RestExtractor } from '../rest/rest-extractor.service' 4import { RestExtractor } from '../rest/rest-extractor.service'
5import { HttpClient } from '@angular/common/http' 5import { HttpClient, HttpParams } from '@angular/common/http'
6import { VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '../../../../../shared/models/videos' 6import { VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '../../../../../shared/models/videos'
7import { AccountService } from '../account/account.service' 7import { AccountService } from '../account/account.service'
8import { ResultList } from '../../../../../shared' 8import { ResultList } from '../../../../../shared'
@@ -10,6 +10,8 @@ import { VideoChannel } from './video-channel.model'
10import { environment } from '../../../environments/environment' 10import { environment } from '../../../environments/environment'
11import { Account } from '@app/shared/account/account.model' 11import { Account } from '@app/shared/account/account.model'
12import { Avatar } from '../../../../../shared/models/avatars/avatar.model' 12import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
13import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
14import { RestService } from '@app/shared/rest'
13 15
14@Injectable() 16@Injectable()
15export class VideoChannelService { 17export class VideoChannelService {
@@ -29,6 +31,7 @@ export class VideoChannelService {
29 31
30 constructor ( 32 constructor (
31 private authHttp: HttpClient, 33 private authHttp: HttpClient,
34 private restService: RestService,
32 private restExtractor: RestExtractor 35 private restExtractor: RestExtractor
33 ) { } 36 ) { }
34 37
@@ -41,8 +44,16 @@ export class VideoChannelService {
41 ) 44 )
42 } 45 }
43 46
44 listAccountVideoChannels (account: Account): Observable<ResultList<VideoChannel>> { 47 listAccountVideoChannels (account: Account, componentPagination?: ComponentPagination): Observable<ResultList<VideoChannel>> {
45 return this.authHttp.get<ResultList<VideoChannelServer>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels') 48 const pagination = componentPagination
49 ? this.restService.componentPaginationToRestPagination(componentPagination)
50 : { start: 0, count: 20 }
51
52 let params = new HttpParams()
53 params = this.restService.addRestGetParams(params, pagination)
54
55 const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels'
56 return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params })
46 .pipe( 57 .pipe(
47 map(res => VideoChannelService.extractVideoChannels(res)), 58 map(res => VideoChannelService.extractVideoChannels(res)),
48 catchError(err => this.restExtractor.handleError(err)) 59 catchError(err => this.restExtractor.handleError(err))
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html
index 268677977..efd369bca 100644
--- a/client/src/app/shared/video/abstract-video-list.html
+++ b/client/src/app/shared/video/abstract-video-list.html
@@ -6,7 +6,7 @@
6 </div> 6 </div>
7 </div> 7 </div>
8 8
9 <my-feed [syndicationItems]="syndicationItems"></my-feed> 9 <my-feed *ngIf="titlePage" [syndicationItems]="syndicationItems"></my-feed>
10 10
11 <div class="moderation-block" *ngIf="displayModerationBlock"> 11 <div class="moderation-block" *ngIf="displayModerationBlock">
12 <my-peertube-checkbox 12 <my-peertube-checkbox
@@ -22,11 +22,17 @@
22 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" 22 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true"
23 class="videos" 23 class="videos"
24 > 24 >
25 <my-video-miniature 25 <ng-container *ngFor="let video of videos; trackBy: videoById;">
26 *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType" 26 <div class="date-title" *ngIf="getCurrentGroupedDateLabel(video)">
27 [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" 27 {{ getCurrentGroupedDateLabel(video) }}
28 (videoBlacklisted)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" 28 </div>
29 > 29
30 </my-video-miniature> 30 <my-video-miniature
31 [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType"
32 [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions"
33 (videoBlacklisted)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)"
34 >
35 </my-video-miniature>
36 </ng-container>
31 </div> 37 </div>
32</div> 38</div>
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss
index 9d481d6e4..98b80fdfd 100644
--- a/client/src/app/shared/video/abstract-video-list.scss
+++ b/client/src/app/shared/video/abstract-video-list.scss
@@ -24,33 +24,19 @@
24 } 24 }
25} 25}
26 26
27.margin-content { 27.date-title {
28 width: $video-miniature-width * 6; 28 font-size: 16px;
29 margin: auto !important; 29 font-weight: $font-semibold;
30 30 margin-bottom: 20px;
31 @media screen and (max-width: 1800px) { 31 margin-top: -10px;
32 width: $video-miniature-width * 5; 32 padding-top: 20px;
33 }
34 33
35 @media screen and (max-width: 1800px - $video-miniature-width) { 34 &:not(:first-child) {
36 width: $video-miniature-width * 4; 35 border-top: 1px solid $separator-border-color;
37 } 36 }
37}
38 38
39 @media screen and (max-width: 1800px - (2* $video-miniature-width)) { 39.margin-content {
40 width: $video-miniature-width * 3; 40 @include adapt-margin-content-width;
41 }
42
43 @media screen and (max-width: 1800px - (3* $video-miniature-width)) {
44 width: $video-miniature-width * 2;
45 }
46
47 @media screen and (max-width: 500px) {
48 width: auto;
49 margin: 0 !important;
50
51 .videos {
52 @include video-miniature-small-screen;
53 }
54 }
55} 41}
56 42
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
index fa9d38735..dc8f9cda9 100644
--- a/client/src/app/shared/video/abstract-video-list.ts
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -11,6 +11,17 @@ import { MiniatureDisplayOptions, OwnerDisplayType } from '@app/shared/video/vid
11import { Syndication } from '@app/shared/video/syndication.model' 11import { Syndication } from '@app/shared/video/syndication.model'
12import { Notifier, ServerService } from '@app/core' 12import { Notifier, ServerService } from '@app/core'
13import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' 13import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
14import { I18n } from '@ngx-translate/i18n-polyfill'
15import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date'
16
17enum GroupDate {
18 UNKNOWN = 0,
19 TODAY = 1,
20 YESTERDAY = 2,
21 LAST_WEEK = 3,
22 LAST_MONTH = 4,
23 OLDER = 5
24}
14 25
15export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook { 26export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook {
16 pagination: ComponentPagination = { 27 pagination: ComponentPagination = {
@@ -31,6 +42,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
31 displayModerationBlock = false 42 displayModerationBlock = false
32 titleTooltip: string 43 titleTooltip: string
33 displayVideoActions = true 44 displayVideoActions = true
45 groupByDate = false
34 46
35 disabled = false 47 disabled = false
36 48
@@ -50,11 +62,15 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
50 protected abstract serverService: ServerService 62 protected abstract serverService: ServerService
51 protected abstract screenService: ScreenService 63 protected abstract screenService: ScreenService
52 protected abstract router: Router 64 protected abstract router: Router
65 protected abstract i18n: I18n
53 abstract titlePage: string 66 abstract titlePage: string
54 67
55 private resizeSubscription: Subscription 68 private resizeSubscription: Subscription
56 private angularState: number 69 private angularState: number
57 70
71 private groupedDateLabels: { [id in GroupDate]: string }
72 private groupedDates: { [id: number]: GroupDate } = {}
73
58 abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }> 74 abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }>
59 75
60 abstract generateSyndicationList (): void 76 abstract generateSyndicationList (): void
@@ -64,6 +80,15 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
64 } 80 }
65 81
66 ngOnInit () { 82 ngOnInit () {
83 this.groupedDateLabels = {
84 [GroupDate.UNKNOWN]: null,
85 [GroupDate.TODAY]: this.i18n('Today'),
86 [GroupDate.YESTERDAY]: this.i18n('Yesterday'),
87 [GroupDate.LAST_WEEK]: this.i18n('Last week'),
88 [GroupDate.LAST_MONTH]: this.i18n('Last month'),
89 [GroupDate.OLDER]: this.i18n('Older')
90 }
91
67 // Subscribe to route changes 92 // Subscribe to route changes
68 const routeParams = this.route.snapshot.queryParams 93 const routeParams = this.route.snapshot.queryParams
69 this.loadRouteParams(routeParams) 94 this.loadRouteParams(routeParams)
@@ -113,6 +138,8 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
113 this.pagination.totalItems = totalVideos 138 this.pagination.totalItems = totalVideos
114 this.videos = this.videos.concat(videos) 139 this.videos = this.videos.concat(videos)
115 140
141 if (this.groupByDate) this.buildGroupedDateLabels()
142
116 this.onMoreVideos() 143 this.onMoreVideos()
117 }, 144 },
118 145
@@ -134,6 +161,59 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
134 this.videos = this.videos.filter(v => v.id !== video.id) 161 this.videos = this.videos.filter(v => v.id !== video.id)
135 } 162 }
136 163
164 buildGroupedDateLabels () {
165 let currentGroupedDate: GroupDate = GroupDate.UNKNOWN
166
167 for (const video of this.videos) {
168 const publishedDate = video.publishedAt
169
170 if (currentGroupedDate <= GroupDate.TODAY && isToday(publishedDate)) {
171 if (currentGroupedDate === GroupDate.TODAY) continue
172
173 currentGroupedDate = GroupDate.TODAY
174 this.groupedDates[ video.id ] = currentGroupedDate
175 continue
176 }
177
178 if (currentGroupedDate <= GroupDate.YESTERDAY && isYesterday(publishedDate)) {
179 if (currentGroupedDate === GroupDate.YESTERDAY) continue
180
181 currentGroupedDate = GroupDate.YESTERDAY
182 this.groupedDates[ video.id ] = currentGroupedDate
183 continue
184 }
185
186 if (currentGroupedDate <= GroupDate.LAST_WEEK && isLastWeek(publishedDate)) {
187 if (currentGroupedDate === GroupDate.LAST_WEEK) continue
188
189 currentGroupedDate = GroupDate.LAST_WEEK
190 this.groupedDates[ video.id ] = currentGroupedDate
191 continue
192 }
193
194 if (currentGroupedDate <= GroupDate.LAST_MONTH && isLastMonth(publishedDate)) {
195 if (currentGroupedDate === GroupDate.LAST_MONTH) continue
196
197 currentGroupedDate = GroupDate.LAST_MONTH
198 this.groupedDates[ video.id ] = currentGroupedDate
199 continue
200 }
201
202 if (currentGroupedDate <= GroupDate.OLDER) {
203 if (currentGroupedDate === GroupDate.OLDER) continue
204
205 currentGroupedDate = GroupDate.OLDER
206 this.groupedDates[ video.id ] = currentGroupedDate
207 }
208 }
209 }
210
211 getCurrentGroupedDateLabel (video: Video) {
212 if (this.groupByDate === false) return undefined
213
214 return this.groupedDateLabels[this.groupedDates[video.id]]
215 }
216
137 // On videos hook for children that want to do something 217 // On videos hook for children that want to do something
138 protected onMoreVideos () { /* empty */ } 218 protected onMoreVideos () { /* empty */ }
139 219
diff --git a/client/src/app/shared/video/modals/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts
index d6d10d29e..a07560f87 100644
--- a/client/src/app/shared/video/modals/video-download.component.ts
+++ b/client/src/app/shared/video/modals/video-download.component.ts
@@ -1,6 +1,6 @@
1import { Component, ElementRef, ViewChild } from '@angular/core' 1import { Component, ElementRef, ViewChild } from '@angular/core'
2import { VideoDetails } from '../../../shared/video/video-details.model' 2import { VideoDetails } from '../../../shared/video/video-details.model'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
6 6
@@ -16,6 +16,7 @@ export class VideoDownloadComponent {
16 resolutionId: number | string = -1 16 resolutionId: number | string = -1
17 17
18 video: VideoDetails 18 video: VideoDetails
19 activeModal: NgbActiveModal
19 20
20 constructor ( 21 constructor (
21 private notifier: Notifier, 22 private notifier: Notifier,
@@ -26,9 +27,7 @@ export class VideoDownloadComponent {
26 show (video: VideoDetails) { 27 show (video: VideoDetails) {
27 this.video = video 28 this.video = video
28 29
29 const m = this.modalService.open(this.modal) 30 this.activeModal = this.modalService.open(this.modal)
30 m.result.then(() => this.onClose())
31 .catch(() => this.onClose())
32 31
33 this.resolutionId = this.video.files[0].resolution.id 32 this.resolutionId = this.video.files[0].resolution.id
34 } 33 }
@@ -39,6 +38,7 @@ export class VideoDownloadComponent {
39 38
40 download () { 39 download () {
41 window.location.assign(this.getLink()) 40 window.location.assign(this.getLink())
41 this.activeModal.close()
42 } 42 }
43 43
44 getLink () { 44 getLink () {
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts
index 22f024656..e4d443a06 100644
--- a/client/src/app/shared/video/video-details.model.ts
+++ b/client/src/app/shared/video/video-details.model.ts
@@ -1,5 +1,4 @@
1import { UserRight, VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFile, VideoState } from '../../../../../shared' 1import { VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFile, VideoState } from '../../../../../shared'
2import { AuthUser } from '../../core'
3import { Video } from '../../shared/video/video.model' 2import { Video } from '../../shared/video/video.model'
4import { Account } from '@app/shared/account/account.model' 3import { Account } from '@app/shared/account/account.model'
5import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 4import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts
index 1f633d427..67d8e7711 100644
--- a/client/src/app/shared/video/video-edit.model.ts
+++ b/client/src/app/shared/video/video-edit.model.ts
@@ -85,6 +85,11 @@ export class VideoEdit implements VideoUpdate {
85 const originallyPublishedAt = new Date(values['originallyPublishedAt']) 85 const originallyPublishedAt = new Date(values['originallyPublishedAt'])
86 this.originallyPublishedAt = originallyPublishedAt.toISOString() 86 this.originallyPublishedAt = originallyPublishedAt.toISOString()
87 } 87 }
88
89 // Use the same file than the preview for the thumbnail
90 if (this.previewfile) {
91 this.thumbnailfile = this.previewfile
92 }
88 } 93 }
89 94
90 toFormPatch () { 95 toFormPatch () {
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index 0cef3eb8f..6f9de9241 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -52,7 +52,6 @@ export class Video implements VideoServerModel {
52 52
53 account: { 53 account: {
54 id: number 54 id: number
55 uuid: string
56 name: string 55 name: string
57 displayName: string 56 displayName: string
58 url: string 57 url: string
@@ -62,7 +61,6 @@ export class Video implements VideoServerModel {
62 61
63 channel: { 62 channel: {
64 id: number 63 id: number
65 uuid: string
66 name: string 64 name: string
67 displayName: string 65 displayName: string
68 url: string 66 url: string
diff --git a/client/src/app/shared/video/videos-selection.component.ts b/client/src/app/shared/video/videos-selection.component.ts
index 955ebca9f..d69f7b70e 100644
--- a/client/src/app/shared/video/videos-selection.component.ts
+++ b/client/src/app/shared/video/videos-selection.component.ts
@@ -20,6 +20,7 @@ import { Video } from '@app/shared/video/video.model'
20import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' 20import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
21import { VideoSortField } from '@app/shared/video/sort-field.type' 21import { VideoSortField } from '@app/shared/video/sort-field.type'
22import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 22import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
23import { I18n } from '@ngx-translate/i18n-polyfill'
23 24
24export type SelectionType = { [ id: number ]: boolean } 25export type SelectionType = { [ id: number ]: boolean }
25 26
@@ -44,6 +45,7 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
44 globalButtonsTemplate: TemplateRef<any> 45 globalButtonsTemplate: TemplateRef<any>
45 46
46 constructor ( 47 constructor (
48 protected i18n: I18n,
47 protected router: Router, 49 protected router: Router,
48 protected route: ActivatedRoute, 50 protected route: ActivatedRoute,
49 protected notifier: Notifier, 51 protected notifier: Notifier,
diff --git a/client/src/app/signup/index.ts b/client/src/app/signup/index.ts
deleted file mode 100644
index b0aca9723..000000000
--- a/client/src/app/signup/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1export * from './signup-routing.module'
2export * from './signup.component'
3export * from './signup.module'
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html
deleted file mode 100644
index 07d24b381..000000000
--- a/client/src/app/signup/signup.component.html
+++ /dev/null
@@ -1,72 +0,0 @@
1<div class="margin-content">
2
3 <div i18n class="title-page title-page-single">
4 Create an account
5 </div>
6
7 <div *ngIf="info" class="alert alert-info">{{ info }}</div>
8 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
9
10 <div class="d-flex justify-content-left flex-wrap">
11 <form role="form" (ngSubmit)="signup()" [formGroup]="form">
12 <div class="form-group">
13 <label for="username" i18n>Username</label>
14
15 <div class="input-group">
16 <input
17 type="text" id="username" i18n-placeholder placeholder="Example: jane_doe"
18 formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
19 >
20 <div class="input-group-append">
21 <span class="input-group-text">@{{ instanceHost }}</span>
22 </div>
23 </div>
24
25 <div *ngIf="formErrors.username" class="form-error">
26 {{ formErrors.username }}
27 </div>
28 </div>
29
30 <div class="form-group">
31 <label for="email" i18n>Email</label>
32 <input
33 type="text" id="email" i18n-placeholder placeholder="Email"
34 formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }"
35 >
36 <div *ngIf="formErrors.email" class="form-error">
37 {{ formErrors.email }}
38 </div>
39 </div>
40
41 <div class="form-group">
42 <label for="password" i18n>Password</label>
43 <input
44 type="password" id="password" i18n-placeholder placeholder="Password"
45 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
46 >
47 <div *ngIf="formErrors.password" class="form-error">
48 {{ formErrors.password }}
49 </div>
50 </div>
51
52 <div class="form-group form-group-terms">
53 <my-peertube-checkbox
54 inputName="terms" formControlName="terms"
55 i18n-labelHtml labelHtml="I am at least 16 years old and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance"
56 ></my-peertube-checkbox>
57
58 <div *ngIf="formErrors.terms" class="form-error">
59 {{ formErrors.terms }}
60 </div>
61 </div>
62
63 <input type="submit" i18n-value value="Signup" [disabled]="!form.valid || signupDone">
64 </form>
65
66 <div>
67 <label i18n>Features found on this instance</label>
68 <my-instance-features-table></my-instance-features-table>
69 </div>
70 </div>
71
72</div>
diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts
deleted file mode 100644
index 13941ec79..000000000
--- a/client/src/app/signup/signup.component.ts
+++ /dev/null
@@ -1,78 +0,0 @@
1import { Component, OnInit } from '@angular/core'
2import { AuthService, Notifier, RedirectService, ServerService } from '@app/core'
3import { UserCreate } from '../../../../shared'
4import { FormReactive, UserService, UserValidatorsService } from '../shared'
5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7
8@Component({
9 selector: 'my-signup',
10 templateUrl: './signup.component.html',
11 styleUrls: [ './signup.component.scss' ]
12})
13export class SignupComponent extends FormReactive implements OnInit {
14 info: string = null
15 error: string = null
16 signupDone = false
17
18 constructor (
19 protected formValidatorService: FormValidatorService,
20 private authService: AuthService,
21 private userValidatorsService: UserValidatorsService,
22 private notifier: Notifier,
23 private userService: UserService,
24 private serverService: ServerService,
25 private redirectService: RedirectService,
26 private i18n: I18n
27 ) {
28 super()
29 }
30
31 get instanceHost () {
32 return window.location.host
33 }
34
35 get requiresEmailVerification () {
36 return this.serverService.getConfig().signup.requiresEmailVerification
37 }
38
39 ngOnInit () {
40 this.buildForm({
41 username: this.userValidatorsService.USER_USERNAME,
42 password: this.userValidatorsService.USER_PASSWORD,
43 email: this.userValidatorsService.USER_EMAIL,
44 terms: this.userValidatorsService.USER_TERMS
45 })
46 }
47
48 signup () {
49 this.error = null
50
51 const userCreate: UserCreate = this.form.value
52
53 this.userService.signup(userCreate).subscribe(
54 () => {
55 this.signupDone = true
56
57 if (this.requiresEmailVerification) {
58 this.info = this.i18n('Welcome! Now please check your emails to verify your account and complete signup.')
59 return
60 }
61
62 // Auto login
63 this.authService.login(userCreate.username, userCreate.password)
64 .subscribe(
65 () => {
66 this.notifier.success(this.i18n('You are now logged in as {{username}}!', { username: userCreate.username }))
67
68 this.redirectService.redirectToHomepage()
69 },
70
71 err => this.error = err.message
72 )
73 },
74
75 err => this.error = err.message
76 )
77 }
78}
diff --git a/client/src/app/signup/signup.module.ts b/client/src/app/signup/signup.module.ts
deleted file mode 100644
index 61560ddcf..000000000
--- a/client/src/app/signup/signup.module.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import { NgModule } from '@angular/core'
2
3import { SignupRoutingModule } from './signup-routing.module'
4import { SignupComponent } from './signup.component'
5import { SharedModule } from '../shared'
6
7@NgModule({
8 imports: [
9 SignupRoutingModule,
10 SharedModule
11 ],
12
13 declarations: [
14 SignupComponent
15 ],
16
17 exports: [
18 SignupComponent
19 ],
20
21 providers: [
22 ]
23})
24export class SignupModule { }
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html
index 99695204d..28572d611 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html
@@ -187,18 +187,14 @@
187 <ng-template ngbTabContent> 187 <ng-template ngbTabContent>
188 <div class="row advanced-settings"> 188 <div class="row advanced-settings">
189 <div class="col-md-12 col-xl-8"> 189 <div class="col-md-12 col-xl-8">
190 <div class="form-group">
191 <my-image-upload
192 i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
193 previewWidth="200px" previewHeight="110px"
194 ></my-image-upload>
195 </div>
196 190
197 <div class="form-group"> 191 <div class="form-group">
198 <my-image-upload 192 <label i18n for="previewfile">Video preview</label>
199 i18n-inputLabel inputLabel="Upload preview" inputName="previewfile" formControlName="previewfile" 193
194 <my-preview-upload
195 i18n-inputLabel inputLabel="Edit" inputName="previewfile" formControlName="previewfile"
200 previewWidth="360px" previewHeight="200px" 196 previewWidth="360px" previewHeight="200px"
201 ></my-image-upload> 197 ></my-preview-upload>
202 </div> 198 </div>
203 199
204 <div class="form-group"> 200 <div class="form-group">
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
index 8345645f6..cea352bfb 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
@@ -13,6 +13,7 @@ import { VideoCaptionAddModalComponent } from '@app/videos/+video-edit/shared/vi
13import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 13import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
14import { removeElementFromArray } from '@app/shared/misc/utils' 14import { removeElementFromArray } from '@app/shared/misc/utils'
15import { VideoConstant, VideoPrivacy } from '../../../../../../shared' 15import { VideoConstant, VideoPrivacy } from '../../../../../../shared'
16import { VideoService } from '@app/shared/video/video.service'
16 17
17@Component({ 18@Component({
18 selector: 'my-video-edit', 19 selector: 'my-video-edit',
@@ -23,7 +24,6 @@ export class VideoEditComponent implements OnInit, OnDestroy {
23 @Input() form: FormGroup 24 @Input() form: FormGroup
24 @Input() formErrors: { [ id: string ]: string } = {} 25 @Input() formErrors: { [ id: string ]: string } = {}
25 @Input() validationMessages: FormReactiveValidationMessages = {} 26 @Input() validationMessages: FormReactiveValidationMessages = {}
26 @Input() videoPrivacies: VideoConstant<VideoPrivacy>[] = []
27 @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] 27 @Input() userVideoChannels: { id: number, label: string, support: string }[] = []
28 @Input() schedulePublicationPossible = true 28 @Input() schedulePublicationPossible = true
29 @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] 29 @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = []
@@ -34,6 +34,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
34 // So that it can be accessed in the template 34 // So that it can be accessed in the template
35 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY 35 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
36 36
37 videoPrivacies: VideoConstant<VideoPrivacy>[] = []
37 videoCategories: VideoConstant<number>[] = [] 38 videoCategories: VideoConstant<number>[] = []
38 videoLicences: VideoConstant<number>[] = [] 39 videoLicences: VideoConstant<number>[] = []
39 videoLanguages: VideoConstant<string>[] = [] 40 videoLanguages: VideoConstant<string>[] = []
@@ -58,6 +59,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
58 private formValidatorService: FormValidatorService, 59 private formValidatorService: FormValidatorService,
59 private videoValidatorsService: VideoValidatorsService, 60 private videoValidatorsService: VideoValidatorsService,
60 private videoCaptionService: VideoCaptionService, 61 private videoCaptionService: VideoCaptionService,
62 private videoService: VideoService,
61 private route: ActivatedRoute, 63 private route: ActivatedRoute,
62 private router: Router, 64 private router: Router,
63 private notifier: Notifier, 65 private notifier: Notifier,
@@ -100,7 +102,6 @@ export class VideoEditComponent implements OnInit, OnDestroy {
100 language: this.videoValidatorsService.VIDEO_LANGUAGE, 102 language: this.videoValidatorsService.VIDEO_LANGUAGE,
101 description: this.videoValidatorsService.VIDEO_DESCRIPTION, 103 description: this.videoValidatorsService.VIDEO_DESCRIPTION,
102 tags: null, 104 tags: null,
103 thumbnailfile: null,
104 previewfile: null, 105 previewfile: null,
105 support: this.videoValidatorsService.VIDEO_SUPPORT, 106 support: this.videoValidatorsService.VIDEO_SUPPORT,
106 schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT, 107 schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT,
@@ -133,6 +134,9 @@ export class VideoEditComponent implements OnInit, OnDestroy {
133 this.videoLicences = this.serverService.getVideoLicences() 134 this.videoLicences = this.serverService.getVideoLicences()
134 this.videoLanguages = this.serverService.getVideoLanguages() 135 this.videoLanguages = this.serverService.getVideoLanguages()
135 136
137 const privacies = this.serverService.getVideoPrivacies()
138 this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies)
139
136 this.initialVideoCaptions = this.videoCaptions.map(c => c.language.id) 140 this.initialVideoCaptions = this.videoCaptions.map(c => c.language.id)
137 141
138 this.ngZone.runOutsideAngular(() => { 142 this.ngZone.runOutsideAngular(() => {
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
index 537d7ffa2..7a495fea5 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -58,7 +58,7 @@
58<form [hidden]="!hasImportedVideo" novalidate [formGroup]="form"> 58<form [hidden]="!hasImportedVideo" novalidate [formGroup]="form">
59 <my-video-edit 59 <my-video-edit
60 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" 60 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
61 [validationMessages]="validationMessages" [videoPrivacies]="explainedVideoPrivacies" [userVideoChannels]="userVideoChannels" 61 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
62 ></my-video-edit> 62 ></my-video-edit>
63 63
64 <div class="submit-container"> 64 <div class="submit-container">
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
index d2e9f6cfe..ed9cb5840 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -100,7 +100,6 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
100 previewUrl: null 100 previewUrl: null
101 })) 101 }))
102 102
103 this.explainedVideoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies)
104 103
105 this.hydrateFormFromVideo() 104 this.hydrateFormFromVideo()
106 }, 105 },
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
index 984b9d590..e4f19faa8 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
@@ -51,7 +51,7 @@
51<form [hidden]="!hasImportedVideo" novalidate [formGroup]="form"> 51<form [hidden]="!hasImportedVideo" novalidate [formGroup]="form">
52 <my-video-edit 52 <my-video-edit
53 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" 53 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
54 [validationMessages]="validationMessages" [videoPrivacies]="explainedVideoPrivacies" [userVideoChannels]="userVideoChannels" 54 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
55 ></my-video-edit> 55 ></my-video-edit>
56 56
57 <div class="submit-container"> 57 <div class="submit-container">
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-send.ts b/client/src/app/videos/+video-edit/video-add-components/video-send.ts
index 8401caeec..580c123a0 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-send.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-send.ts
@@ -14,7 +14,6 @@ import { CanComponentDeactivateResult } from '@app/shared/guards/can-deactivate-
14export abstract class VideoSend extends FormReactive implements OnInit { 14export abstract class VideoSend extends FormReactive implements OnInit {
15 userVideoChannels: { id: number, label: string, support: string }[] = [] 15 userVideoChannels: { id: number, label: string, support: string }[] = []
16 videoPrivacies: VideoConstant<VideoPrivacy>[] = [] 16 videoPrivacies: VideoConstant<VideoPrivacy>[] = []
17 explainedVideoPrivacies: VideoConstant<VideoPrivacy>[] = []
18 videoCaptions: VideoCaptionEdit[] = [] 17 videoCaptions: VideoCaptionEdit[] = []
19 18
20 firstStepPrivacyId = 0 19 firstStepPrivacyId = 0
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
index 536769d2f..0f904affb 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
@@ -26,6 +26,27 @@
26 </select> 26 </select>
27 </div> 27 </div>
28 </div> 28 </div>
29
30 <ng-container *ngIf="isUploadingAudioFile">
31 <div class="form-group audio-preview">
32 <label i18n for="previewfileUpload">Video background image</label>
33
34 <div i18n class="audio-image-info">
35 Image that will be merged with your audio file.
36 <br />
37 The chosen image will be definitive and cannot be modified.
38 </div>
39
40 <my-preview-upload
41 i18n-inputLabel inputLabel="Edit" inputName="previewfileUpload" [(ngModel)]="previewfileUpload"
42 previewWidth="360px" previewHeight="200px"
43 ></my-preview-upload>
44 </div>
45
46 <div class="form-group upload-audio-button">
47 <my-button className="orange-button" i18n-label [label]="getAudioUploadLabel()" icon="upload" (click)="uploadFirstStep(true)"></my-button>
48 </div>
49 </ng-container>
29 </div> 50 </div>
30</div> 51</div>
31 52
@@ -50,7 +71,7 @@
50<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> 71<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
51 <my-video-edit 72 <my-video-edit
52 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" 73 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
53 [validationMessages]="validationMessages" [videoPrivacies]="explainedVideoPrivacies" [userVideoChannels]="userVideoChannels" 74 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
54 [waitTranscodingEnabled]="waitTranscodingEnabled" 75 [waitTranscodingEnabled]="waitTranscodingEnabled"
55 ></my-video-edit> 76 ></my-video-edit>
56 77
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
index 8adf8f169..684342f09 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
@@ -1,9 +1,20 @@
1@import 'variables'; 1@import 'variables';
2@import 'mixins'; 2@import 'mixins';
3 3
4.first-step-block .form-group-channel { 4.first-step-block {
5 margin-bottom: 20px; 5
6 margin-top: 35px; 6 .form-group-channel {
7 margin-bottom: 20px;
8 margin-top: 35px;
9 }
10
11 .audio-image-info {
12 margin-bottom: 10px;
13 }
14
15 .audio-preview {
16 margin: 30px 0;
17 }
7} 18}
8 19
9.upload-progress-cancel { 20.upload-progress-cancel {
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
index d6d4bad21..69fa13a2f 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
@@ -35,8 +35,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
35 userVideoQuotaUsed = 0 35 userVideoQuotaUsed = 0
36 userVideoQuotaUsedDaily = 0 36 userVideoQuotaUsedDaily = 0
37 37
38 isUploadingAudioFile = false
38 isUploadingVideo = false 39 isUploadingVideo = false
39 isUpdatingVideo = false 40 isUpdatingVideo = false
41
40 videoUploaded = false 42 videoUploaded = false
41 videoUploadObservable: Subscription = null 43 videoUploadObservable: Subscription = null
42 videoUploadPercents = 0 44 videoUploadPercents = 0
@@ -44,7 +46,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
44 id: 0, 46 id: 0,
45 uuid: '' 47 uuid: ''
46 } 48 }
49
47 waitTranscodingEnabled = true 50 waitTranscodingEnabled = true
51 previewfileUpload: File
48 52
49 error: string 53 error: string
50 54
@@ -100,6 +104,17 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
100 } 104 }
101 } 105 }
102 106
107 getVideoFile () {
108 return this.videofileInput.nativeElement.files[0]
109 }
110
111 getAudioUploadLabel () {
112 const videofile = this.getVideoFile()
113 if (!videofile) return this.i18n('Upload')
114
115 return this.i18n('Upload {{videofileName}}', { videofileName: videofile.name })
116 }
117
103 fileChange () { 118 fileChange () {
104 this.uploadFirstStep() 119 this.uploadFirstStep()
105 } 120 }
@@ -114,38 +129,15 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
114 } 129 }
115 } 130 }
116 131
117 uploadFirstStep () { 132 uploadFirstStep (clickedOnButton = false) {
118 const videofile = this.videofileInput.nativeElement.files[0] 133 const videofile = this.getVideoFile()
119 if (!videofile) return 134 if (!videofile) return
120 135
121 // Check global user quota 136 if (!this.checkGlobalUserQuota(videofile)) return
122 const bytePipes = new BytesPipe() 137 if (!this.checkDailyUserQuota(videofile)) return
123 const videoQuota = this.authService.getUser().videoQuota
124 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
125 const msg = this.i18n(
126 'Your video quota is exceeded with this video (video size: {{videoSize}}, used: {{videoQuotaUsed}}, quota: {{videoQuota}})',
127 {
128 videoSize: bytePipes.transform(videofile.size, 0),
129 videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0),
130 videoQuota: bytePipes.transform(videoQuota, 0)
131 }
132 )
133 this.notifier.error(msg)
134 return
135 }
136 138
137 // Check daily user quota 139 if (clickedOnButton === false && this.isAudioFile(videofile.name)) {
138 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily 140 this.isUploadingAudioFile = true
139 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
140 const msg = this.i18n(
141 'Your daily video quota is exceeded with this video (video size: {{videoSize}}, used: {{quotaUsedDaily}}, quota: {{quotaDaily}})',
142 {
143 videoSize: bytePipes.transform(videofile.size, 0),
144 quotaUsedDaily: bytePipes.transform(this.userVideoQuotaUsedDaily, 0),
145 quotaDaily: bytePipes.transform(videoQuotaDaily, 0)
146 }
147 )
148 this.notifier.error(msg)
149 return 141 return
150 } 142 }
151 143
@@ -180,6 +172,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
180 formData.append('channelId', '' + channelId) 172 formData.append('channelId', '' + channelId)
181 formData.append('videofile', videofile) 173 formData.append('videofile', videofile)
182 174
175 if (this.previewfileUpload) {
176 formData.append('previewfile', this.previewfileUpload)
177 formData.append('thumbnailfile', this.previewfileUpload)
178 }
179
183 this.isUploadingVideo = true 180 this.isUploadingVideo = true
184 this.firstStepDone.emit(name) 181 this.firstStepDone.emit(name)
185 182
@@ -187,11 +184,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
187 name, 184 name,
188 privacy, 185 privacy,
189 nsfw, 186 nsfw,
190 channelId 187 channelId,
188 previewfile: this.previewfileUpload
191 }) 189 })
192 190
193 this.explainedVideoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies)
194
195 this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe( 191 this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe(
196 event => { 192 event => {
197 if (event.type === HttpEventType.UploadProgress) { 193 if (event.type === HttpEventType.UploadProgress) {
@@ -251,4 +247,52 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
251 } 247 }
252 ) 248 )
253 } 249 }
250
251 private checkGlobalUserQuota (videofile: File) {
252 const bytePipes = new BytesPipe()
253
254 // Check global user quota
255 const videoQuota = this.authService.getUser().videoQuota
256 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
257 const msg = this.i18n(
258 'Your video quota is exceeded with this video (video size: {{videoSize}}, used: {{videoQuotaUsed}}, quota: {{videoQuota}})',
259 {
260 videoSize: bytePipes.transform(videofile.size, 0),
261 videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0),
262 videoQuota: bytePipes.transform(videoQuota, 0)
263 }
264 )
265 this.notifier.error(msg)
266
267 return false
268 }
269
270 return true
271 }
272
273 private checkDailyUserQuota (videofile: File) {
274 const bytePipes = new BytesPipe()
275
276 // Check daily user quota
277 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily
278 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
279 const msg = this.i18n(
280 'Your daily video quota is exceeded with this video (video size: {{videoSize}}, used: {{quotaUsedDaily}}, quota: {{quotaDaily}})',
281 {
282 videoSize: bytePipes.transform(videofile.size, 0),
283 quotaUsedDaily: bytePipes.transform(this.userVideoQuotaUsedDaily, 0),
284 quotaDaily: bytePipes.transform(videoQuotaDaily, 0)
285 }
286 )
287 this.notifier.error(msg)
288
289 return false
290 }
291
292 return true
293 }
294
295 private isAudioFile (filename: string) {
296 return filename.endsWith('.mp3') || filename.endsWith('.flac') || filename.endsWith('.ogg')
297 }
254} 298}
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html
index b5cab7ed5..aa148311f 100644
--- a/client/src/app/videos/+video-edit/video-update.component.html
+++ b/client/src/app/videos/+video-edit/video-update.component.html
@@ -7,7 +7,7 @@
7 7
8 <my-video-edit 8 <my-video-edit
9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible" 9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
10 [validationMessages]="validationMessages" [videoPrivacies]="explainedVideoPrivacies" [userVideoChannels]="userVideoChannels" 10 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
11 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled" 11 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled"
12 ></my-video-edit> 12 ></my-video-edit>
13 13
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts
index 10f797d02..e990ceb13 100644
--- a/client/src/app/videos/+video-edit/video-update.component.ts
+++ b/client/src/app/videos/+video-edit/video-update.component.ts
@@ -3,7 +3,6 @@ import { Component, HostListener, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { LoadingBarService } from '@ngx-loading-bar/core' 4import { LoadingBarService } from '@ngx-loading-bar/core'
5import { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
6import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos'
7import { ServerService } from '../../core' 6import { ServerService } from '../../core'
8import { FormReactive } from '../../shared' 7import { FormReactive } from '../../shared'
9import { VideoEdit } from '../../shared/video/video-edit.model' 8import { VideoEdit } from '../../shared/video/video-edit.model'
@@ -23,8 +22,6 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
23 video: VideoEdit 22 video: VideoEdit
24 23
25 isUpdatingVideo = false 24 isUpdatingVideo = false
26 videoPrivacies: VideoConstant<VideoPrivacy>[] = []
27 explainedVideoPrivacies: VideoConstant<VideoPrivacy>[] = []
28 userVideoChannels: { id: number, label: string, support: string }[] = [] 25 userVideoChannels: { id: number, label: string, support: string }[] = []
29 schedulePublicationPossible = false 26 schedulePublicationPossible = false
30 videoCaptions: VideoCaptionEdit[] = [] 27 videoCaptions: VideoCaptionEdit[] = []
@@ -49,9 +46,6 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
49 ngOnInit () { 46 ngOnInit () {
50 this.buildForm({}) 47 this.buildForm({})
51 48
52 this.serverService.videoPrivaciesLoaded
53 .subscribe(() => this.videoPrivacies = this.serverService.getVideoPrivacies())
54
55 this.route.data 49 this.route.data
56 .pipe(map(data => data.videoData)) 50 .pipe(map(data => data.videoData))
57 .subscribe(({ video, videoChannels, videoCaptions }) => { 51 .subscribe(({ video, videoChannels, videoCaptions }) => {
@@ -59,15 +53,6 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
59 this.userVideoChannels = videoChannels 53 this.userVideoChannels = videoChannels
60 this.videoCaptions = videoCaptions 54 this.videoCaptions = videoCaptions
61 55
62 // We cannot set private a video that was not private
63 if (this.video.privacy !== VideoPrivacy.PRIVATE) {
64 this.videoPrivacies = this.videoPrivacies.filter(p => p.id !== VideoPrivacy.PRIVATE)
65 } else { // We can schedule video publication only if it it is private
66 this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE
67 }
68
69 this.explainedVideoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies)
70
71 const videoFiles = (video as VideoDetails).files 56 const videoFiles = (video as VideoDetails).files
72 if (videoFiles.length > 1) { // Already transcoded 57 if (videoFiles.length > 1) { // Already transcoded
73 this.waitTranscodingEnabled = false 58 this.waitTranscodingEnabled = false
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index 631504eab..2d13f1b58 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -6,7 +6,7 @@ import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
6import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' 6import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
7import { MetaService } from '@ngx-meta/core' 7import { MetaService } from '@ngx-meta/core'
8import { Notifier, ServerService } from '@app/core' 8import { Notifier, ServerService } from '@app/core'
9import { forkJoin, Subscription } from 'rxjs' 9import { forkJoin, Observable, Subscription } from 'rxjs'
10import { Hotkey, HotkeysService } from 'angular2-hotkeys' 10import { Hotkey, HotkeysService } from 'angular2-hotkeys'
11import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' 11import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
12import { AuthService, ConfirmService } from '../../core' 12import { AuthService, ConfirmService } from '../../core'
@@ -135,22 +135,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
135 135
136 setLike () { 136 setLike () {
137 if (this.isUserLoggedIn() === false) return 137 if (this.isUserLoggedIn() === false) return
138 if (this.userRating === 'like') { 138
139 // Already liked this video 139 // Already liked this video
140 this.setRating('none') 140 if (this.userRating === 'like') this.setRating('none')
141 } else { 141 else this.setRating('like')
142 this.setRating('like')
143 }
144 } 142 }
145 143
146 setDislike () { 144 setDislike () {
147 if (this.isUserLoggedIn() === false) return 145 if (this.isUserLoggedIn() === false) return
148 if (this.userRating === 'dislike') { 146
149 // Already disliked this video 147 // Already disliked this video
150 this.setRating('none') 148 if (this.userRating === 'dislike') this.setRating('none')
151 } else { 149 else this.setRating('dislike')
152 this.setRating('dislike')
153 }
154 } 150 }
155 151
156 showMoreDescription () { 152 showMoreDescription () {
@@ -249,12 +245,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
249 ) 245 )
250 .subscribe(([ video, captionsResult ]) => { 246 .subscribe(([ video, captionsResult ]) => {
251 const queryParams = this.route.snapshot.queryParams 247 const queryParams = this.route.snapshot.queryParams
252 const startTime = queryParams.start
253 const stopTime = queryParams.stop
254 const subtitle = queryParams.subtitle
255 const playerMode = queryParams.mode
256 248
257 this.onVideoFetched(video, captionsResult.data, { startTime, stopTime, subtitle, playerMode }) 249 const urlOptions = {
250 startTime: queryParams.start,
251 stopTime: queryParams.stop,
252 subtitle: queryParams.subtitle,
253 playerMode: queryParams.mode
254 }
255
256 this.onVideoFetched(video, captionsResult.data, urlOptions)
258 .catch(err => this.handleError(err)) 257 .catch(err => this.handleError(err))
259 }) 258 })
260 } 259 }
@@ -279,6 +278,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
279 private updateVideoDescription (description: string) { 278 private updateVideoDescription (description: string) {
280 this.video.description = description 279 this.video.description = description
281 this.setVideoDescriptionHTML() 280 this.setVideoDescriptionHTML()
281 .catch(err => console.error(err))
282 } 282 }
283 283
284 private async setVideoDescriptionHTML () { 284 private async setVideoDescriptionHTML () {
@@ -385,7 +385,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
385 captions: videoCaptions.length !== 0, 385 captions: videoCaptions.length !== 0,
386 peertubeLink: false, 386 peertubeLink: false,
387 387
388 videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, 388 videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE
389 ? this.videoService.getVideoViewUrl(this.video.uuid)
390 : null,
389 embedUrl: this.video.embedUrl, 391 embedUrl: this.video.embedUrl,
390 392
391 language: this.localeId, 393 language: this.localeId,
@@ -466,20 +468,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
466 } 468 }
467 469
468 private setRating (nextRating: UserVideoRateType) { 470 private setRating (nextRating: UserVideoRateType) {
469 let method 471 const ratingMethods: { [id in UserVideoRateType]: (id: number) => Observable<any> } = {
470 switch (nextRating) { 472 like: this.videoService.setVideoLike,
471 case 'like': 473 dislike: this.videoService.setVideoDislike,
472 method = this.videoService.setVideoLike 474 none: this.videoService.unsetVideoLike
473 break
474 case 'dislike':
475 method = this.videoService.setVideoDislike
476 break
477 case 'none':
478 method = this.videoService.unsetVideoLike
479 break
480 } 475 }
481 476
482 method.call(this.videoService, this.video.id) 477 ratingMethods[nextRating].call(this.videoService, this.video.id)
483 .subscribe( 478 .subscribe(
484 () => { 479 () => {
485 // Update the video like attribute 480 // Update the video like attribute
@@ -545,25 +540,29 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
545 private flushPlayer () { 540 private flushPlayer () {
546 // Remove player if it exists 541 // Remove player if it exists
547 if (this.player) { 542 if (this.player) {
548 this.player.dispose() 543 try {
549 this.player = undefined 544 this.player.dispose()
545 this.player = undefined
546 } catch (err) {
547 console.error('Cannot dispose player.', err)
548 }
550 } 549 }
551 } 550 }
552 551
553 private initHotkeys () { 552 private initHotkeys () {
554 this.hotkeys = [ 553 this.hotkeys = [
555 new Hotkey('shift+l', (event: KeyboardEvent): boolean => { 554 new Hotkey('shift+l', () => {
556 this.setLike() 555 this.setLike()
557 return false 556 return false
558 }, undefined, this.i18n('Like the video')), 557 }, undefined, this.i18n('Like the video')),
559 new Hotkey('shift+d', (event: KeyboardEvent): boolean => { 558
559 new Hotkey('shift+d', () => {
560 this.setDislike() 560 this.setDislike()
561 return false 561 return false
562 }, undefined, this.i18n('Dislike the video')), 562 }, undefined, this.i18n('Dislike the video')),
563 new Hotkey('shift+s', (event: KeyboardEvent): boolean => { 563
564 this.subscribeButton.subscribed ? 564 new Hotkey('shift+s', () => {
565 this.subscribeButton.unsubscribe() : 565 this.subscribeButton.subscribed ? this.subscribeButton.unsubscribe() : this.subscribeButton.subscribe()
566 this.subscribeButton.subscribe()
567 return false 566 return false
568 }, undefined, this.i18n('Subscribe to the account')) 567 }, undefined, this.i18n('Subscribe to the account'))
569 ] 568 ]
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts
index 13d4023c2..65543343c 100644
--- a/client/src/app/videos/video-list/video-local.component.ts
+++ b/client/src/app/videos/video-list/video-local.component.ts
@@ -22,13 +22,13 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
22 filter: VideoFilter = 'local' 22 filter: VideoFilter = 'local'
23 23
24 constructor ( 24 constructor (
25 protected i18n: I18n,
25 protected router: Router, 26 protected router: Router,
26 protected serverService: ServerService, 27 protected serverService: ServerService,
27 protected route: ActivatedRoute, 28 protected route: ActivatedRoute,
28 protected notifier: Notifier, 29 protected notifier: Notifier,
29 protected authService: AuthService, 30 protected authService: AuthService,
30 protected screenService: ScreenService, 31 protected screenService: ScreenService,
31 private i18n: I18n,
32 private videoService: VideoService 32 private videoService: VideoService
33 ) { 33 ) {
34 super() 34 super()
diff --git a/client/src/app/videos/video-list/video-overview.component.html b/client/src/app/videos/video-list/video-overview.component.html
index b644dd798..f59de584a 100644
--- a/client/src/app/videos/video-list/video-overview.component.html
+++ b/client/src/app/videos/video-list/video-overview.component.html
@@ -3,7 +3,7 @@
3 <div class="no-results" i18n *ngIf="notResults">No results.</div> 3 <div class="no-results" i18n *ngIf="notResults">No results.</div>
4 4
5 <div class="section" *ngFor="let object of overview.categories"> 5 <div class="section" *ngFor="let object of overview.categories">
6 <div class="section-title" i18n> 6 <div class="section-title">
7 <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a> 7 <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a>
8 </div> 8 </div>
9 9
@@ -11,7 +11,7 @@
11 </div> 11 </div>
12 12
13 <div class="section" *ngFor="let object of overview.tags"> 13 <div class="section" *ngFor="let object of overview.tags">
14 <div class="section-title" i18n> 14 <div class="section-title">
15 <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a> 15 <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a>
16 </div> 16 </div>
17 17
@@ -19,7 +19,7 @@
19 </div> 19 </div>
20 20
21 <div class="section channel" *ngFor="let object of overview.channels"> 21 <div class="section channel" *ngFor="let object of overview.channels">
22 <div class="section-title" i18n> 22 <div class="section-title">
23 <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]"> 23 <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]">
24 <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" /> 24 <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" />
25 25
diff --git a/client/src/app/videos/video-list/video-overview.component.scss b/client/src/app/videos/video-list/video-overview.component.scss
index a24766783..ade6f53b7 100644
--- a/client/src/app/videos/video-list/video-overview.component.scss
+++ b/client/src/app/videos/video-list/video-overview.component.scss
@@ -2,62 +2,10 @@
2@import '_mixins'; 2@import '_mixins';
3@import '_miniature'; 3@import '_miniature';
4 4
5.section { 5.margin-content {
6 max-height: 500px; // 2 rows max 6 @include adapt-margin-content-width;
7 overflow: hidden;
8 padding-top: 10px;
9
10 &:first-child {
11 padding-top: 30px;
12 }
13
14 my-video-miniature {
15 text-align: left;
16 }
17}
18
19.section-title {
20 font-size: 24px;
21 font-weight: $font-semibold;
22 margin-bottom: 10px;
23
24 a {
25 &:hover, &:focus:not(.focus-visible), &:active {
26 text-decoration: none;
27 outline: none;
28 }
29
30 color: var(--mainForegroundColor);
31 }
32} 7}
33 8
34.channel { 9.section {
35 .section-title a { 10 @include miniature-rows;
36 display: flex;
37 width: fit-content;
38 align-items: center;
39
40 img {
41 @include avatar(28px);
42
43 margin-right: 8px;
44 }
45 }
46}
47
48@media screen and (max-width: 500px) {
49 .margin-content {
50 margin: 0 !important;
51 }
52
53 .section-title {
54 font-size: 17px;
55 }
56
57 .section {
58 max-height: initial;
59 overflow: initial;
60
61 @include video-miniature-small-screen;
62 }
63} 11}
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts
index 80cef813e..f54bade98 100644
--- a/client/src/app/videos/video-list/video-recently-added.component.ts
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -17,15 +17,16 @@ import { Notifier, ServerService } from '@app/core'
17export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy { 17export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy {
18 titlePage: string 18 titlePage: string
19 sort: VideoSortField = '-publishedAt' 19 sort: VideoSortField = '-publishedAt'
20 groupByDate = true
20 21
21 constructor ( 22 constructor (
23 protected i18n: I18n,
22 protected route: ActivatedRoute, 24 protected route: ActivatedRoute,
23 protected serverService: ServerService, 25 protected serverService: ServerService,
24 protected router: Router, 26 protected router: Router,
25 protected notifier: Notifier, 27 protected notifier: Notifier,
26 protected authService: AuthService, 28 protected authService: AuthService,
27 protected screenService: ScreenService, 29 protected screenService: ScreenService,
28 private i18n: I18n,
29 private videoService: VideoService 30 private videoService: VideoService
30 ) { 31 ) {
31 super() 32 super()
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index e2ad95bc4..a2c819ebe 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -19,13 +19,13 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
19 defaultSort: VideoSortField = '-trending' 19 defaultSort: VideoSortField = '-trending'
20 20
21 constructor ( 21 constructor (
22 protected i18n: I18n,
22 protected router: Router, 23 protected router: Router,
23 protected serverService: ServerService, 24 protected serverService: ServerService,
24 protected route: ActivatedRoute, 25 protected route: ActivatedRoute,
25 protected notifier: Notifier, 26 protected notifier: Notifier,
26 protected authService: AuthService, 27 protected authService: AuthService,
27 protected screenService: ScreenService, 28 protected screenService: ScreenService,
28 private i18n: I18n,
29 private videoService: VideoService 29 private videoService: VideoService
30 ) { 30 ) {
31 super() 31 super()
diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
index 2f0685ccc..3caa371d8 100644
--- a/client/src/app/videos/video-list/video-user-subscriptions.component.ts
+++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
@@ -19,15 +19,16 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
19 titlePage: string 19 titlePage: string
20 sort = '-publishedAt' as VideoSortField 20 sort = '-publishedAt' as VideoSortField
21 ownerDisplayType: OwnerDisplayType = 'auto' 21 ownerDisplayType: OwnerDisplayType = 'auto'
22 groupByDate = true
22 23
23 constructor ( 24 constructor (
25 protected i18n: I18n,
24 protected router: Router, 26 protected router: Router,
25 protected serverService: ServerService, 27 protected serverService: ServerService,
26 protected route: ActivatedRoute, 28 protected route: ActivatedRoute,
27 protected notifier: Notifier, 29 protected notifier: Notifier,
28 protected authService: AuthService, 30 protected authService: AuthService,
29 protected screenService: ScreenService, 31 protected screenService: ScreenService,
30 private i18n: I18n,
31 private videoService: VideoService 32 private videoService: VideoService
32 ) { 33 ) {
33 super() 34 super()
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index 6cdd54372..31cbc7dfd 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -117,8 +117,17 @@ export class PeertubePlayerManager {
117 videojs(options.common.playerElement, videojsOptions, function (this: any) { 117 videojs(options.common.playerElement, videojsOptions, function (this: any) {
118 const player = this 118 const player = this
119 119
120 player.tech_.one('error', () => self.maybeFallbackToWebTorrent(mode, player, options)) 120 let alreadyFallback = false
121 player.one('error', () => self.maybeFallbackToWebTorrent(mode, player, options)) 121
122 player.tech_.one('error', () => {
123 if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options)
124 alreadyFallback = true
125 })
126
127 player.one('error', () => {
128 if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options)
129 alreadyFallback = true
130 })
122 131
123 self.addContextMenu(mode, player, options.common.embedUrl) 132 self.addContextMenu(mode, player, options.common.embedUrl)
124 133
diff --git a/client/src/sass/include/_miniature.scss b/client/src/sass/include/_miniature.scss
index b62187fd2..0c2ee2d0d 100644
--- a/client/src/sass/include/_miniature.scss
+++ b/client/src/sass/include/_miniature.scss
@@ -138,3 +138,100 @@ $play-overlay-width: 18px;
138 } 138 }
139 } 139 }
140} 140}
141
142@mixin miniature-rows {
143 max-height: 540px; // 2 rows max
144 overflow: hidden;
145 padding-top: 10px;
146
147 &:first-child {
148 padding-top: 30px;
149 }
150
151 my-video-miniature {
152 text-align: left;
153 }
154
155 .section-title {
156 font-size: 24px;
157 font-weight: $font-semibold;
158 margin-bottom: 30px;
159 display: flex;
160 justify-content: space-between;
161
162 a {
163 &:hover, &:focus:not(.focus-visible), &:active {
164 text-decoration: none;
165 outline: none;
166 }
167
168 color: var(--mainForegroundColor);
169 }
170 }
171
172 &.channel {
173 .section-title {
174 a {
175 display: flex;
176 width: fit-content;
177 align-items: center;
178
179 img {
180 @include avatar(28px);
181
182 margin-right: 8px;
183 }
184 }
185
186 .followers {
187 color: $grey-foreground-color;
188 font-weight: normal;
189 font-size: 14px;
190 margin-left: 10px;
191 position: relative;
192 top: 2px;
193 }
194 }
195 }
196
197 @media screen and (max-width: $mobile-view) {
198 max-height: initial;
199 overflow: initial;
200
201 @include video-miniature-small-screen;
202
203 .section-title {
204 font-size: 17px;
205 }
206 }
207}
208
209@mixin adapt-margin-content-width {
210 width: $video-miniature-width * 6;
211 margin: auto !important;
212
213 @media screen and (max-width: 1800px) {
214 width: $video-miniature-width * 5;
215 }
216
217 @media screen and (max-width: 1800px - $video-miniature-width) {
218 width: $video-miniature-width * 4;
219 }
220
221 @media screen and (max-width: 1800px - (2* $video-miniature-width)) {
222 width: $video-miniature-width * 3;
223 }
224
225 @media screen and (max-width: 1800px - (3* $video-miniature-width)) {
226 width: $video-miniature-width * 2;
227 }
228
229 @media screen and (max-width: 500px) {
230 width: auto;
231 margin: 0 !important;
232
233 .videos {
234 @include video-miniature-small-screen;
235 }
236 }
237}
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index 262a8136f..d4a2269a1 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -331,7 +331,12 @@
331} 331}
332 332
333@mixin peertube-checkbox ($border-width) { 333@mixin peertube-checkbox ($border-width) {
334 display: none; 334 opacity: 0;
335 position: absolute;
336
337 &:focus + span {
338 outline: 1px solid #1e5180;
339 }
335 340
336 & + span { 341 & + span {
337 position: relative; 342 position: relative;