aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.scss2
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts10
-rw-r--r--client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html23
-rw-r--r--client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss49
-rw-r--r--client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts30
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts4
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss15
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.scss6
-rw-r--r--client/src/app/+my-account/my-account.component.html6
-rw-r--r--client/src/app/+my-account/my-account.module.ts4
-rw-r--r--client/src/app/+my-account/shared/actor-avatar-info.component.scss2
-rw-r--r--client/src/app/+video-channels/video-channels.component.html3
-rw-r--r--client/src/app/+video-channels/video-channels.component.scss15
-rw-r--r--client/src/app/menu/menu.component.html5
-rw-r--r--client/src/app/menu/menu.component.scss6
-rw-r--r--client/src/app/shared/shared.module.ts9
-rw-r--r--client/src/app/shared/user-subscription/index.ts2
-rw-r--r--client/src/app/shared/user-subscription/subscribe-button.component.html15
-rw-r--r--client/src/app/shared/user-subscription/subscribe-button.component.scss37
-rw-r--r--client/src/app/shared/user-subscription/subscribe-button.component.ts74
-rw-r--r--client/src/app/shared/user-subscription/user-subscription.service.ts66
-rw-r--r--client/src/app/shared/video-channel/video-channel.service.ts30
-rw-r--r--client/src/app/shared/video/abstract-video-list.html2
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts2
-rw-r--r--client/src/app/shared/video/video-details.model.ts12
-rw-r--r--client/src/app/shared/video/video-miniature.component.html8
-rw-r--r--client/src/app/shared/video/video-miniature.component.scss3
-rw-r--r--client/src/app/shared/video/video-miniature.component.ts35
-rw-r--r--client/src/app/shared/video/video.model.ts8
-rw-r--r--client/src/app/shared/video/video.service.ts18
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html9
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss17
-rw-r--r--client/src/app/videos/video-list/video-user-subscriptions.component.ts57
-rw-r--r--client/src/app/videos/videos-routing.module.ts15
-rw-r--r--client/src/app/videos/videos.module.ts4
-rw-r--r--client/src/assets/images/menu/podcasts.svg26
-rw-r--r--client/src/assets/images/menu/subscriptions.svg26
-rw-r--r--client/src/sass/application.scss7
-rw-r--r--client/src/sass/include/_mixins.scss57
-rw-r--r--client/src/sass/include/_variables.scss2
40 files changed, 630 insertions, 91 deletions
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 c9c7fa8eb..39c0840e4 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
@@ -2,7 +2,7 @@
2@import '_mixins'; 2@import '_mixins';
3 3
4.row { 4.row {
5 text-align: center; 5 justify-content: center;
6} 6}
7 7
8a.video-channel { 8a.video-channel {
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts
index 6f0806e8a..c1c979151 100644
--- a/client/src/app/+my-account/my-account-routing.module.ts
+++ b/client/src/app/+my-account/my-account-routing.module.ts
@@ -9,6 +9,7 @@ import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-vid
9import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' 9import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component'
10import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' 10import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component'
11import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' 11import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
12import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
12 13
13const myAccountRoutes: Routes = [ 14const myAccountRoutes: Routes = [
14 { 15 {
@@ -74,6 +75,15 @@ const myAccountRoutes: Routes = [
74 title: 'Account video imports' 75 title: 'Account video imports'
75 } 76 }
76 } 77 }
78 },
79 {
80 path: 'subscriptions',
81 component: MyAccountSubscriptionsComponent,
82 data: {
83 meta: {
84 title: 'Account subscriptions'
85 }
86 }
77 } 87 }
78 ] 88 ]
79 } 89 }
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html
new file mode 100644
index 000000000..4c68cd1a5
--- /dev/null
+++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html
@@ -0,0 +1,23 @@
1<div class="video-channels">
2 <div *ngFor="let videoChannel of videoChannels" class="video-channel">
3 <a [routerLink]="[ '/video-channels', videoChannel.name ]">
4 <img [src]="videoChannel.avatarUrl" alt="Avatar" />
5 </a>
6
7 <div class="video-channel-info">
8 <a [routerLink]="[ '/video-channels', videoChannel.name ]" class="video-channel-names" i18n-title title="Go to the channel">
9 <div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
10 <div class="video-channel-name">{{ videoChannel.name }}</div>
11 </a>
12
13 <div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div>
14
15 <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
16 <span i18n>Created by {{ videoChannel.ownerBy }}</span>
17 <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
18 </a>
19 </div>
20
21 <my-subscribe-button [videoChannel]="videoChannel"></my-subscribe-button>
22 </div>
23</div>
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss
new file mode 100644
index 000000000..2fbfa335b
--- /dev/null
+++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss
@@ -0,0 +1,49 @@
1@import '_variables';
2@import '_mixins';
3
4.video-channel {
5 @include row-blocks;
6
7 img {
8 @include avatar(80px);
9
10 margin-right: 10px;
11 }
12
13 .video-channel-info {
14 flex-grow: 1;
15
16 a.video-channel-names {
17 @include disable-default-a-behaviour;
18
19 width: fit-content;
20 display: flex;
21 align-items: baseline;
22 color: #000;
23
24 .video-channel-display-name {
25 font-weight: $font-semibold;
26 font-size: 18px;
27 }
28
29 .video-channel-name {
30 font-size: 14px;
31 color: $grey-actor-name;
32 margin-left: 5px;
33 }
34 }
35 }
36
37 .actor-owner {
38 @include actor-owner;
39 }
40
41 my-subscribe-button {
42 /deep/ span[role=button] {
43 padding: 7px 12px;
44 font-size: 16px;
45 }
46 }
47}
48
49
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts
new file mode 100644
index 000000000..1e94cf90b
--- /dev/null
+++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts
@@ -0,0 +1,30 @@
1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { UserSubscriptionService } from '@app/shared/user-subscription'
6
7@Component({
8 selector: 'my-account-subscriptions',
9 templateUrl: './my-account-subscriptions.component.html',
10 styleUrls: [ './my-account-subscriptions.component.scss' ]
11})
12export class MyAccountSubscriptionsComponent implements OnInit {
13 videoChannels: VideoChannel[] = []
14
15 constructor (
16 private userSubscriptionService: UserSubscriptionService,
17 private notificationsService: NotificationsService,
18 private i18n: I18n
19 ) {}
20
21 ngOnInit () {
22 this.userSubscriptionService.listSubscriptions()
23 .subscribe(
24 res => { console.log(res); this.videoChannels = res.data },
25
26 error => this.notificationsService.error(this.i18n('Error'), error.message)
27 )
28 }
29
30}
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 e25037e24..56697030b 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
@@ -78,7 +78,7 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
78 support: body.support || null 78 support: body.support || null
79 } 79 }
80 80
81 this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.uuid, videoChannelUpdate).subscribe( 81 this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe(
82 () => { 82 () => {
83 this.authService.refreshUserInformation() 83 this.authService.refreshUserInformation()
84 this.notificationsService.success( 84 this.notificationsService.success(
@@ -93,7 +93,7 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
93 } 93 }
94 94
95 onAvatarChange (formData: FormData) { 95 onAvatarChange (formData: FormData) {
96 this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.uuid, formData) 96 this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData)
97 .subscribe( 97 .subscribe(
98 data => { 98 data => {
99 this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.')) 99 this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.'))
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss
index f8fd2684e..5c892be01 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss
@@ -12,11 +12,7 @@
12} 12}
13 13
14.video-channel { 14.video-channel {
15 display: flex; 15 @include row-blocks;
16 min-height: 130px;
17 padding-bottom: 20px;
18 margin-bottom: 20px;
19 border-bottom: 1px solid #C6C6C6;
20 16
21 img { 17 img {
22 @include avatar(80px); 18 @include avatar(80px);
@@ -42,7 +38,7 @@
42 38
43 .video-channel-name { 39 .video-channel-name {
44 font-size: 14px; 40 font-size: 14px;
45 color: #777272; 41 color: $grey-actor-name;
46 margin-left: 5px; 42 margin-left: 5px;
47 } 43 }
48 } 44 }
@@ -64,12 +60,9 @@
64 } 60 }
65 61
66 .video-channel { 62 .video-channel {
67 flex-direction: column;
68 height: auto;
69 text-align: center;
70
71 .video-channel-names { 63 .video-channel-names {
72 justify-content: center; 64 flex-direction: column;
65 align-items: center !important;
73 } 66 }
74 67
75 img { 68 img {
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
index 64a04fa20..cd805be73 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
@@ -42,11 +42,7 @@
42} 42}
43 43
44.video { 44.video {
45 display: flex; 45 @include row-blocks;
46 min-height: 130px;
47 padding-bottom: 20px;
48 margin-bottom: 20px;
49 border-bottom: 1px solid #C6C6C6;
50 46
51 &:first-child { 47 &:first-child {
52 margin-top: 47px; 48 margin-top: 47px;
diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html
index ddb0570db..74742649c 100644
--- a/client/src/app/+my-account/my-account.component.html
+++ b/client/src/app/+my-account/my-account.component.html
@@ -2,11 +2,13 @@
2 <div class="sub-menu"> 2 <div class="sub-menu">
3 <a i18n routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My settings</a> 3 <a i18n routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My settings</a>
4 4
5 <a i18n routerLink="/my-account/video-channels" routerLinkActive="active" class="title-page">My video channels</a> 5 <a i18n routerLink="/my-account/video-channels" routerLinkActive="active" class="title-page">My channels</a>
6 6
7 <a i18n routerLink="/my-account/videos" routerLinkActive="active" class="title-page">My videos</a> 7 <a i18n routerLink="/my-account/videos" routerLinkActive="active" class="title-page">My videos</a>
8 8
9 <a *ngIf="isVideoImportEnabled()" i18n routerLink="/my-account/video-imports" routerLinkActive="active" class="title-page">My video imports</a> 9 <a i18n routerLink="/my-account/subscriptions" routerLinkActive="active" class="title-page">My subscriptions</a>
10
11 <a *ngIf="isVideoImportEnabled()" i18n routerLink="/my-account/video-imports" routerLinkActive="active" class="title-page">My imports</a>
10 </div> 12 </div>
11 13
12 <div class="margin-content"> 14 <div class="margin-content">
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 29b49e8d9..c93f38d4b 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -14,6 +14,7 @@ import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-accoun
14import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' 14import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
15import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' 15import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
16import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' 16import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone'
17import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
17 18
18@NgModule({ 19@NgModule({
19 imports: [ 20 imports: [
@@ -34,7 +35,8 @@ import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settin
34 MyAccountVideoChannelUpdateComponent, 35 MyAccountVideoChannelUpdateComponent,
35 ActorAvatarInfoComponent, 36 ActorAvatarInfoComponent,
36 MyAccountVideoImportsComponent, 37 MyAccountVideoImportsComponent,
37 MyAccountDangerZoneComponent 38 MyAccountDangerZoneComponent,
39 MyAccountSubscriptionsComponent
38 ], 40 ],
39 41
40 exports: [ 42 exports: [
diff --git a/client/src/app/+my-account/shared/actor-avatar-info.component.scss b/client/src/app/+my-account/shared/actor-avatar-info.component.scss
index 36a792f82..0b0c83de5 100644
--- a/client/src/app/+my-account/shared/actor-avatar-info.component.scss
+++ b/client/src/app/+my-account/shared/actor-avatar-info.component.scss
@@ -25,7 +25,7 @@
25 position: relative; 25 position: relative;
26 top: 2px; 26 top: 2px;
27 font-size: 14px; 27 font-size: 14px;
28 color: #777272; 28 color: $grey-actor-name;
29 } 29 }
30 } 30 }
31 31
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index 5a69a82a0..1941a2eab 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -8,6 +8,8 @@
8 <div class="actor-names"> 8 <div class="actor-names">
9 <div class="actor-display-name">{{ videoChannel.displayName }}</div> 9 <div class="actor-display-name">{{ videoChannel.displayName }}</div>
10 <div class="actor-name">{{ videoChannel.nameWithHost }}</div> 10 <div class="actor-name">{{ videoChannel.nameWithHost }}</div>
11
12 <my-subscribe-button [videoChannel]="videoChannel"></my-subscribe-button>
11 </div> 13 </div>
12 <div i18n class="actor-followers">{{ videoChannel.followersCount }} subscribers</div> 14 <div i18n class="actor-followers">{{ videoChannel.followersCount }} subscribers</div>
13 15
@@ -20,7 +22,6 @@
20 22
21 <div class="links"> 23 <div class="links">
22 <a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a> 24 <a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a>
23
24 <a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a> 25 <a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a>
25 </div> 26 </div>
26 </div> 27 </div>
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss
index 909b65bc7..a63b1ec06 100644
--- a/client/src/app/+video-channels/video-channels.component.scss
+++ b/client/src/app/+video-channels/video-channels.component.scss
@@ -3,4 +3,19 @@
3 3
4.sub-menu { 4.sub-menu {
5 @include sub-menu-with-actor; 5 @include sub-menu-with-actor;
6
7 .actor, .actor-info {
8 width: 100%;
9 }
10
11 .actor-name {
12 flex-grow: 1;
13 }
14
15 my-subscribe-button {
16 /deep/ span[role=button] {
17 padding: 7px 12px;
18 font-size: 16px;
19 }
20 }
6} \ No newline at end of file 21} \ No newline at end of file
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index 7edcdf501..bd03af9b3 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -42,6 +42,11 @@
42 <div class="panel-block"> 42 <div class="panel-block">
43 <div i18n class="block-title">Videos</div> 43 <div i18n class="block-title">Videos</div>
44 44
45 <a *ngIf="isLoggedIn" routerLink="/videos/subscriptions" routerLinkActive="active">
46 <span class="icon icon-videos-subscriptions"></span>
47 <ng-container i18n>Subscriptions</ng-container>
48 </a>
49
45 <a routerLink="/videos/trending" routerLinkActive="active"> 50 <a routerLink="/videos/trending" routerLinkActive="active">
46 <span class="icon icon-videos-trending"></span> 51 <span class="icon icon-videos-trending"></span>
47 <ng-container i18n>Trending</ng-container> 52 <ng-container i18n>Trending</ng-container>
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index 39f1e9be0..606fea961 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -135,6 +135,12 @@ menu {
135 135
136 margin-right: 18px; 136 margin-right: 18px;
137 137
138 &.icon-videos-subscriptions {
139 position: relative;
140 top: -2px;
141 background-image: url('../../assets/images/menu/subscriptions.svg');
142 }
143
138 &.icon-videos-trending { 144 &.icon-videos-trending {
139 position: relative; 145 position: relative;
140 top: -2px; 146 top: -2px;
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 722415a06..9bc7ad88b 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -36,7 +36,8 @@ import {
36 ReactiveFileComponent, 36 ReactiveFileComponent,
37 ResetPasswordValidatorsService, 37 ResetPasswordValidatorsService,
38 UserValidatorsService, 38 UserValidatorsService,
39 VideoAbuseValidatorsService, VideoBlacklistValidatorsService, 39 VideoAbuseValidatorsService,
40 VideoBlacklistValidatorsService,
40 VideoChannelValidatorsService, 41 VideoChannelValidatorsService,
41 VideoCommentValidatorsService, 42 VideoCommentValidatorsService,
42 VideoValidatorsService 43 VideoValidatorsService
@@ -49,6 +50,7 @@ import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.c
49import { VideoImportService } from '@app/shared/video-import/video-import.service' 50import { VideoImportService } from '@app/shared/video-import/video-import.service'
50import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component' 51import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component'
51import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' 52import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
53import { SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription'
52 54
53@NgModule({ 55@NgModule({
54 imports: [ 56 imports: [
@@ -83,7 +85,8 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N
83 InfiniteScrollerDirective, 85 InfiniteScrollerDirective,
84 HelpComponent, 86 HelpComponent,
85 ReactiveFileComponent, 87 ReactiveFileComponent,
86 PeertubeCheckboxComponent 88 PeertubeCheckboxComponent,
89 SubscribeButtonComponent
87 ], 90 ],
88 91
89 exports: [ 92 exports: [
@@ -115,6 +118,7 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N
115 HelpComponent, 118 HelpComponent,
116 ReactiveFileComponent, 119 ReactiveFileComponent,
117 PeertubeCheckboxComponent, 120 PeertubeCheckboxComponent,
121 SubscribeButtonComponent,
118 122
119 NumberFormatterPipe, 123 NumberFormatterPipe,
120 ObjectLengthPipe, 124 ObjectLengthPipe,
@@ -134,6 +138,7 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N
134 VideoChannelService, 138 VideoChannelService,
135 VideoCaptionService, 139 VideoCaptionService,
136 VideoImportService, 140 VideoImportService,
141 UserSubscriptionService,
137 142
138 FormValidatorService, 143 FormValidatorService,
139 CustomConfigValidatorsService, 144 CustomConfigValidatorsService,
diff --git a/client/src/app/shared/user-subscription/index.ts b/client/src/app/shared/user-subscription/index.ts
new file mode 100644
index 000000000..024b36a41
--- /dev/null
+++ b/client/src/app/shared/user-subscription/index.ts
@@ -0,0 +1,2 @@
1export * from './user-subscription.service'
2export * from './subscribe-button.component' \ No newline at end of file
diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.html b/client/src/app/shared/user-subscription/subscribe-button.component.html
new file mode 100644
index 000000000..63b313662
--- /dev/null
+++ b/client/src/app/shared/user-subscription/subscribe-button.component.html
@@ -0,0 +1,15 @@
1<span i18n *ngIf="subscribed === false" class="subscribe-button" role="button" (click)="subscribe()">
2 <span>Subscribe</span>
3 <span *ngIf="displayFollowers && videoChannel.followersCount !== 0" class="followers-count">
4 {{ videoChannel.followersCount | myNumberFormatter }}
5 </span>
6</span>
7
8<span *ngIf="subscribed === true" class="unsubscribe-button" role="button" (click)="unsubscribe()">
9 <span class="subscribed" i18n>Subscribed</span>
10 <span class="unsubscribe" i18n>Unsubscribe</span>
11
12 <span *ngIf="displayFollowers && videoChannel.followersCount !== 0" class="followers-count">
13 {{ videoChannel.followersCount | myNumberFormatter }}
14 </span>
15</span>
diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.scss b/client/src/app/shared/user-subscription/subscribe-button.component.scss
new file mode 100644
index 000000000..9811fdc0c
--- /dev/null
+++ b/client/src/app/shared/user-subscription/subscribe-button.component.scss
@@ -0,0 +1,37 @@
1@import '_variables';
2@import '_mixins';
3
4.subscribe-button {
5 @include peertube-button;
6 @include orange-button;
7}
8
9.unsubscribe-button {
10 @include peertube-button;
11 @include grey-button
12}
13
14.subscribe-button,
15.unsubscribe-button {
16 padding: 3px 7px;
17}
18
19.unsubscribe-button {
20 .subscribed {
21 display: inline;
22 }
23
24 .unsubscribe {
25 display: none;
26 }
27
28 &:hover {
29 .subscribed {
30 display: none;
31 }
32
33 .unsubscribe {
34 display: inline;
35 }
36 }
37} \ No newline at end of file
diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.ts b/client/src/app/shared/user-subscription/subscribe-button.component.ts
new file mode 100644
index 000000000..46d6dbaf7
--- /dev/null
+++ b/client/src/app/shared/user-subscription/subscribe-button.component.ts
@@ -0,0 +1,74 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { AuthService } from '@app/core'
3import { RestExtractor } from '@app/shared/rest'
4import { RedirectService } from '@app/core/routing/redirect.service'
5import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
6import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
7import { NotificationsService } from 'angular2-notifications'
8import { I18n } from '@ngx-translate/i18n-polyfill'
9
10@Component({
11 selector: 'my-subscribe-button',
12 templateUrl: './subscribe-button.component.html',
13 styleUrls: [ './subscribe-button.component.scss' ]
14})
15export class SubscribeButtonComponent implements OnInit {
16 @Input() videoChannel: VideoChannel
17 @Input() displayFollowers = false
18
19 subscribed: boolean
20
21 constructor (
22 private authService: AuthService,
23 private restExtractor: RestExtractor,
24 private redirectService: RedirectService,
25 private notificationsService: NotificationsService,
26 private userSubscriptionService: UserSubscriptionService,
27 private i18n: I18n
28 ) { }
29
30 get uri () {
31 return this.videoChannel.name + '@' + this.videoChannel.host
32 }
33
34 ngOnInit () {
35 this.userSubscriptionService.isSubscriptionExists(this.uri)
36 .subscribe(
37 exists => this.subscribed = exists,
38
39 err => this.notificationsService.error(this.i18n('Error'), err.message)
40 )
41 }
42
43 subscribe () {
44 this.userSubscriptionService.addSubscription(this.uri)
45 .subscribe(
46 () => {
47 this.subscribed = true
48
49 this.notificationsService.success(
50 this.i18n('Subscribed'),
51 this.i18n('Subscribed to {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName })
52 )
53 },
54
55 err => this.notificationsService.error(this.i18n('Error'), err.message)
56 )
57 }
58
59 unsubscribe () {
60 this.userSubscriptionService.deleteSubscription(this.uri)
61 .subscribe(
62 () => {
63 this.subscribed = false
64
65 this.notificationsService.success(
66 this.i18n('Unsubscribed'),
67 this.i18n('Unsubscribed from {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName })
68 )
69 },
70
71 err => this.notificationsService.error(this.i18n('Error'), err.message)
72 )
73 }
74}
diff --git a/client/src/app/shared/user-subscription/user-subscription.service.ts b/client/src/app/shared/user-subscription/user-subscription.service.ts
new file mode 100644
index 000000000..3103706d1
--- /dev/null
+++ b/client/src/app/shared/user-subscription/user-subscription.service.ts
@@ -0,0 +1,66 @@
1import { catchError, map } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { ResultList } from '../../../../../shared'
5import { environment } from '../../../environments/environment'
6import { RestExtractor } from '../rest'
7import { Observable, of } from 'rxjs'
8import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
9import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
10import { VideoChannel as VideoChannelServer } from '../../../../../shared/models/videos'
11
12@Injectable()
13export class UserSubscriptionService {
14 static BASE_USER_SUBSCRIPTIONS_URL = environment.apiUrl + '/api/v1/users/me/subscriptions'
15
16 constructor (
17 private authHttp: HttpClient,
18 private restExtractor: RestExtractor
19 ) {
20 }
21
22 deleteSubscription (nameWithHost: string) {
23 const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + '/' + nameWithHost
24
25 return this.authHttp.delete(url)
26 .pipe(
27 map(this.restExtractor.extractDataBool),
28 catchError(err => this.restExtractor.handleError(err))
29 )
30 }
31
32 addSubscription (nameWithHost: string) {
33 const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL
34
35 const body = { uri: nameWithHost }
36 return this.authHttp.post(url, body)
37 .pipe(
38 map(this.restExtractor.extractDataBool),
39 catchError(err => this.restExtractor.handleError(err))
40 )
41 }
42
43 listSubscriptions (): Observable<ResultList<VideoChannel>> {
44 const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL
45
46 return this.authHttp.get<ResultList<VideoChannelServer>>(url)
47 .pipe(
48 map(res => VideoChannelService.extractVideoChannels(res)),
49 catchError(err => this.restExtractor.handleError(err))
50 )
51 }
52
53 isSubscriptionExists (nameWithHost: string): Observable<boolean> {
54 const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + '/' + nameWithHost
55
56 return this.authHttp.get(url)
57 .pipe(
58 map(this.restExtractor.extractDataBool),
59 catchError(err => {
60 if (err.status === 404) return of(false)
61
62 return this.restExtractor.handleError(err)
63 })
64 )
65 }
66}
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 510dc9c3d..46b121790 100644
--- a/client/src/app/shared/video-channel/video-channel.service.ts
+++ b/client/src/app/shared/video-channel/video-channel.service.ts
@@ -22,6 +22,16 @@ export class VideoChannelService {
22 private restExtractor: RestExtractor 22 private restExtractor: RestExtractor
23 ) {} 23 ) {}
24 24
25 static extractVideoChannels (result: ResultList<VideoChannelServer>) {
26 const videoChannels: VideoChannel[] = []
27
28 for (const videoChannelJSON of result.data) {
29 videoChannels.push(new VideoChannel(videoChannelJSON))
30 }
31
32 return { data: videoChannels, total: result.total }
33 }
34
25 getVideoChannel (videoChannelName: string) { 35 getVideoChannel (videoChannelName: string) {
26 return this.authHttp.get<VideoChannel>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName) 36 return this.authHttp.get<VideoChannel>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName)
27 .pipe( 37 .pipe(
@@ -34,7 +44,7 @@ export class VideoChannelService {
34 listAccountVideoChannels (account: Account): Observable<ResultList<VideoChannel>> { 44 listAccountVideoChannels (account: Account): Observable<ResultList<VideoChannel>> {
35 return this.authHttp.get<ResultList<VideoChannelServer>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels') 45 return this.authHttp.get<ResultList<VideoChannelServer>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels')
36 .pipe( 46 .pipe(
37 map(res => this.extractVideoChannels(res)), 47 map(res => VideoChannelService.extractVideoChannels(res)),
38 catchError(err => this.restExtractor.handleError(err)) 48 catchError(err => this.restExtractor.handleError(err))
39 ) 49 )
40 } 50 }
@@ -47,16 +57,16 @@ export class VideoChannelService {
47 ) 57 )
48 } 58 }
49 59
50 updateVideoChannel (videoChannelUUID: string, videoChannel: VideoChannelUpdate) { 60 updateVideoChannel (videoChannelName: string, videoChannel: VideoChannelUpdate) {
51 return this.authHttp.put(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelUUID, videoChannel) 61 return this.authHttp.put(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName, videoChannel)
52 .pipe( 62 .pipe(
53 map(this.restExtractor.extractDataBool), 63 map(this.restExtractor.extractDataBool),
54 catchError(err => this.restExtractor.handleError(err)) 64 catchError(err => this.restExtractor.handleError(err))
55 ) 65 )
56 } 66 }
57 67
58 changeVideoChannelAvatar (videoChannelUUID: string, avatarForm: FormData) { 68 changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) {
59 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelUUID + '/avatar/pick' 69 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick'
60 70
61 return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm) 71 return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm)
62 .pipe(catchError(err => this.restExtractor.handleError(err))) 72 .pipe(catchError(err => this.restExtractor.handleError(err)))
@@ -69,14 +79,4 @@ export class VideoChannelService {
69 catchError(err => this.restExtractor.handleError(err)) 79 catchError(err => this.restExtractor.handleError(err))
70 ) 80 )
71 } 81 }
72
73 private extractVideoChannels (result: ResultList<VideoChannelServer>) {
74 const videoChannels: VideoChannel[] = []
75
76 for (const videoChannelJSON of result.data) {
77 videoChannels.push(new VideoChannel(videoChannelJSON))
78 }
79
80 return { data: videoChannels, total: result.total }
81 }
82} 82}
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html
index e8ded6ab8..d4b00c07c 100644
--- a/client/src/app/shared/video/abstract-video-list.html
+++ b/client/src/app/shared/video/abstract-video-list.html
@@ -14,7 +14,7 @@
14 <div *ngFor="let videos of videoPages" class="videos-page"> 14 <div *ngFor="let videos of videoPages" class="videos-page">
15 <my-video-miniature 15 <my-video-miniature
16 class="ng-animate" 16 class="ng-animate"
17 *ngFor="let video of videos" [video]="video" [user]="user" 17 *ngFor="let video of videos" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType"
18 > 18 >
19 </my-video-miniature> 19 </my-video-miniature>
20 </div> 20 </div>
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
index 59d3c1ebe..b8fd7f8eb 100644
--- a/client/src/app/shared/video/abstract-video-list.ts
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -11,6 +11,7 @@ import { VideoSortField } from './sort-field.type'
11import { Video } from './video.model' 11import { Video } from './video.model'
12import { I18n } from '@ngx-translate/i18n-polyfill' 12import { I18n } from '@ngx-translate/i18n-polyfill'
13import { ScreenService } from '@app/shared/misc/screen.service' 13import { ScreenService } from '@app/shared/misc/screen.service'
14import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
14 15
15export abstract class AbstractVideoList implements OnInit, OnDestroy { 16export abstract class AbstractVideoList implements OnInit, OnDestroy {
16 private static LINES_PER_PAGE = 4 17 private static LINES_PER_PAGE = 4
@@ -34,6 +35,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
34 videoWidth: number 35 videoWidth: number
35 videoHeight: number 36 videoHeight: number
36 videoPages: Video[][] = [] 37 videoPages: Video[][] = []
38 ownerDisplayType: OwnerDisplayType = 'account'
37 39
38 protected baseVideoWidth = 215 40 protected baseVideoWidth = 215
39 protected baseVideoHeight = 230 41 protected baseVideoHeight = 230
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts
index d346f985c..fa4ca7f93 100644
--- a/client/src/app/shared/video/video-details.model.ts
+++ b/client/src/app/shared/video/video-details.model.ts
@@ -1,14 +1,8 @@
1import { 1import { UserRight, VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFile, VideoState } from '../../../../../shared'
2 UserRight,
3 VideoChannel,
4 VideoConstant,
5 VideoDetails as VideoDetailsServerModel,
6 VideoFile,
7 VideoState
8} from '../../../../../shared'
9import { AuthUser } from '../../core' 2import { AuthUser } from '../../core'
10import { Video } from '../../shared/video/video.model' 3import { Video } from '../../shared/video/video.model'
11import { Account } from '@app/shared/account/account.model' 4import { Account } from '@app/shared/account/account.model'
5import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
12 6
13export class VideoDetails extends Video implements VideoDetailsServerModel { 7export class VideoDetails extends Video implements VideoDetailsServerModel {
14 descriptionPath: string 8 descriptionPath: string
@@ -30,7 +24,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
30 24
31 this.descriptionPath = hash.descriptionPath 25 this.descriptionPath = hash.descriptionPath
32 this.files = hash.files 26 this.files = hash.files
33 this.channel = hash.channel 27 this.channel = new VideoChannel(hash.channel)
34 this.account = new Account(hash.account) 28 this.account = new Account(hash.account)
35 this.tags = hash.tags 29 this.tags = hash.tags
36 this.support = hash.support 30 this.support = hash.support
diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html
index 3010e5ccc..de84bccf9 100644
--- a/client/src/app/shared/video/video-miniature.component.html
+++ b/client/src/app/shared/video/video-miniature.component.html
@@ -10,6 +10,12 @@
10 </a> 10 </a>
11 11
12 <span i18n class="video-miniature-created-at-views">{{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> 12 <span i18n class="video-miniature-created-at-views">{{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
13 <a class="video-miniature-account" [routerLink]="[ '/accounts', video.by ]">{{ video.by }}</a> 13
14 <a *ngIf="displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]">
15 {{ video.byAccount }}
16 </a>
17 <a *ngIf="displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
18 {{ video.byVideoChannel }}
19 </a>
14 </div> 20 </div>
15</div> 21</div>
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss
index 588eea3a7..6883650f4 100644
--- a/client/src/app/shared/video/video-miniature.component.scss
+++ b/client/src/app/shared/video/video-miniature.component.scss
@@ -38,7 +38,8 @@
38 font-size: 13px; 38 font-size: 13px;
39 } 39 }
40 40
41 .video-miniature-account { 41 .video-miniature-account,
42 .video-miniature-channel {
42 @include disable-default-a-behaviour; 43 @include disable-default-a-behaviour;
43 44
44 display: block; 45 display: block;
diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts
index d3f6dc1f6..07193ebd5 100644
--- a/client/src/app/shared/video/video-miniature.component.ts
+++ b/client/src/app/shared/video/video-miniature.component.ts
@@ -1,20 +1,51 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { User } from '../users' 2import { User } from '../users'
3import { Video } from './video.model' 3import { Video } from './video.model'
4import { ServerService } from '@app/core' 4import { ServerService } from '@app/core'
5 5
6export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto'
7
6@Component({ 8@Component({
7 selector: 'my-video-miniature', 9 selector: 'my-video-miniature',
8 styleUrls: [ './video-miniature.component.scss' ], 10 styleUrls: [ './video-miniature.component.scss' ],
9 templateUrl: './video-miniature.component.html' 11 templateUrl: './video-miniature.component.html'
10}) 12})
11export class VideoMiniatureComponent { 13export class VideoMiniatureComponent implements OnInit {
12 @Input() user: User 14 @Input() user: User
13 @Input() video: Video 15 @Input() video: Video
16 @Input() ownerDisplayType: OwnerDisplayType = 'account'
17
18 private ownerDisplayTypeChosen: 'account' | 'videoChannel'
14 19
15 constructor (private serverService: ServerService) { } 20 constructor (private serverService: ServerService) { }
16 21
22 ngOnInit () {
23 if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') {
24 this.ownerDisplayTypeChosen = this.ownerDisplayType
25 return
26 }
27
28 // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12)
29 // -> Use the account name
30 if (
31 this.video.channel.name === `${this.video.account.name}_channel` ||
32 this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
33 ) {
34 this.ownerDisplayTypeChosen = 'account'
35 } else {
36 this.ownerDisplayTypeChosen = 'videoChannel'
37 }
38 }
39
17 isVideoBlur () { 40 isVideoBlur () {
18 return this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig()) 41 return this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())
19 } 42 }
43
44 displayOwnerAccount () {
45 return this.ownerDisplayTypeChosen === 'account'
46 }
47
48 displayOwnerVideoChannel () {
49 return this.ownerDisplayTypeChosen === 'videoChannel'
50 }
20} 51}
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index df8253301..d80c10459 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -8,9 +8,12 @@ import { Actor } from '@app/shared/actor/actor.model'
8import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' 8import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
9 9
10export class Video implements VideoServerModel { 10export class Video implements VideoServerModel {
11 by: string 11 byVideoChannel: string
12 byAccount: string
13
12 accountAvatarUrl: string 14 accountAvatarUrl: string
13 videoChannelAvatarUrl: string 15 videoChannelAvatarUrl: string
16
14 createdAt: Date 17 createdAt: Date
15 updatedAt: Date 18 updatedAt: Date
16 publishedAt: Date 19 publishedAt: Date
@@ -110,7 +113,8 @@ export class Video implements VideoServerModel {
110 this.account = hash.account 113 this.account = hash.account
111 this.channel = hash.channel 114 this.channel = hash.channel
112 115
113 this.by = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host) 116 this.byAccount = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host)
117 this.byVideoChannel = Actor.CREATE_BY_STRING(hash.channel.name, hash.channel.host)
114 this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account) 118 this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account)
115 this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.channel) 119 this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.channel)
116 120
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts
index e44f1ee65..1a934c8e2 100644
--- a/client/src/app/shared/video/video.service.ts
+++ b/client/src/app/shared/video/video.service.ts
@@ -27,6 +27,7 @@ import { Account } from '@app/shared/account/account.model'
27import { AccountService } from '@app/shared/account/account.service' 27import { AccountService } from '@app/shared/account/account.service'
28import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' 28import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
29import { ServerService } from '@app/core' 29import { ServerService } from '@app/core'
30import { UserSubscriptionService } from '@app/shared/user-subscription'
30 31
31@Injectable() 32@Injectable()
32export class VideoService { 33export class VideoService {
@@ -157,6 +158,23 @@ export class VideoService {
157 ) 158 )
158 } 159 }
159 160
161 getUserSubscriptionVideos (
162 videoPagination: ComponentPagination,
163 sort: VideoSortField
164 ): Observable<{ videos: Video[], totalVideos: number }> {
165 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
166
167 let params = new HttpParams()
168 params = this.restService.addRestGetParams(params, pagination, sort)
169
170 return this.authHttp
171 .get<ResultList<Video>>(UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + '/videos', { params })
172 .pipe(
173 switchMap(res => this.extractVideos(res)),
174 catchError(err => this.restExtractor.handleError(err))
175 )
176 }
177
160 getVideos ( 178 getVideos (
161 videoPagination: ComponentPagination, 179 videoPagination: ComponentPagination,
162 sort: VideoSortField, 180 sort: VideoSortField,
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index c275258ef..8a49e3566 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -42,16 +42,17 @@
42 42
43 <img [src]="video.videoChannelAvatarUrl" alt="Video channel avatar" /> 43 <img [src]="video.videoChannelAvatarUrl" alt="Video channel avatar" />
44 </a> 44 </a>
45 <!-- Here will be the subscribe button --> 45
46 <my-subscribe-button [videoChannel]="video.channel"></my-subscribe-button>
46 </div> 47 </div>
47 48
48 <div class="video-info-by"> 49 <div class="video-info-by">
49 <a [routerLink]="[ '/accounts', video.by ]" i18n-title title="Go to the account page"> 50 <a [routerLink]="[ '/accounts', video.byAccount ]" i18n-title title="Go to the account page">
50 <span i18n>By {{ video.by }}</span> 51 <span i18n>By {{ video.byAccount }}</span>
51 <img [src]="video.accountAvatarUrl" alt="Account avatar" /> 52 <img [src]="video.accountAvatarUrl" alt="Account avatar" />
52 </a> 53 </a>
53 54
54 <my-help helpType="custom" i18n-customHtml customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.name}}@{{video.account.host}}</strong> and subscribe there. Subscription as a PeerTube user is being worked on in <a href='https://github.com/Chocobozzz/PeerTube/issues/470'>#470</a>."></my-help> 55 <my-help helpType="custom" i18n-customHtml customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.name}}@{{video.account.host}}</strong> and subscribe there."></my-help>
55 </div> 56 </div>
56 </div> 57 </div>
57 58
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index 1354de32e..5bf2f485a 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -125,6 +125,14 @@
125 margin: -2px 2px 0 5px; 125 margin: -2px 2px 0 5px;
126 } 126 }
127 } 127 }
128
129 my-subscribe-button {
130 /deep/ span[role=button] {
131 font-size: 13px !important;
132 }
133
134 margin-left: 5px;
135 }
128 } 136 }
129 137
130 .video-info-by { 138 .video-info-by {
@@ -369,7 +377,10 @@
369 377
370 .video-miniature-information { 378 .video-miniature-information {
371 flex-grow: 1; 379 flex-grow: 1;
372 margin-left: 10px; 380 }
381
382 .video-thumbnail {
383 margin-right: 10px
373 } 384 }
374 } 385 }
375 } 386 }
@@ -502,10 +513,6 @@
502 .other-videos { 513 .other-videos {
503 /deep/ .video-miniature { 514 /deep/ .video-miniature {
504 flex-direction: column; 515 flex-direction: column;
505
506 .video-miniature-information {
507 margin-left: 0 !important;
508 }
509 } 516 }
510 } 517 }
511 518
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
new file mode 100644
index 000000000..6e8959c54
--- /dev/null
+++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
@@ -0,0 +1,57 @@
1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { immutableAssign } from '@app/shared/misc/utils'
4import { Location } from '@angular/common'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service'
12import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
13
14@Component({
15 selector: 'my-videos-user-subscriptions',
16 styleUrls: [ '../../shared/video/abstract-video-list.scss' ],
17 templateUrl: '../../shared/video/abstract-video-list.html'
18})
19export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy {
20 titlePage: string
21 currentRoute = '/videos/subscriptions'
22 sort = '-publishedAt' as VideoSortField
23 ownerDisplayType: OwnerDisplayType = 'auto'
24
25 constructor (
26 protected router: Router,
27 protected route: ActivatedRoute,
28 protected notificationsService: NotificationsService,
29 protected authService: AuthService,
30 protected location: Location,
31 protected i18n: I18n,
32 protected screenService: ScreenService,
33 private videoService: VideoService
34 ) {
35 super()
36
37 this.titlePage = i18n('Videos from your subscriptions')
38 }
39
40 ngOnInit () {
41 super.ngOnInit()
42 }
43
44 ngOnDestroy () {
45 super.ngOnDestroy()
46 }
47
48 getVideosObservable (page: number) {
49 const newPagination = immutableAssign(this.pagination, { currentPage: page })
50
51 return this.videoService.getUserSubscriptionVideos(newPagination, this.sort)
52 }
53
54 generateSyndicationList () {
55 // not implemented yet
56 }
57}
diff --git a/client/src/app/videos/videos-routing.module.ts b/client/src/app/videos/videos-routing.module.ts
index 538a43c6d..18ed52570 100644
--- a/client/src/app/videos/videos-routing.module.ts
+++ b/client/src/app/videos/videos-routing.module.ts
@@ -5,6 +5,7 @@ import { MetaGuard } from '@ngx-meta/core'
5import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component' 5import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
6import { VideoTrendingComponent } from './video-list/video-trending.component' 6import { VideoTrendingComponent } from './video-list/video-trending.component'
7import { VideosComponent } from './videos.component' 7import { VideosComponent } from './videos.component'
8import { VideoUserSubscriptionsComponent } from '@app/videos/video-list/video-user-subscriptions.component'
8 9
9const videosRoutes: Routes = [ 10const videosRoutes: Routes = [
10 { 11 {
@@ -13,11 +14,6 @@ const videosRoutes: Routes = [
13 canActivateChild: [ MetaGuard ], 14 canActivateChild: [ MetaGuard ],
14 children: [ 15 children: [
15 { 16 {
16 path: 'list',
17 pathMatch: 'full',
18 redirectTo: 'recently-added'
19 },
20 {
21 path: 'trending', 17 path: 'trending',
22 component: VideoTrendingComponent, 18 component: VideoTrendingComponent,
23 data: { 19 data: {
@@ -36,6 +32,15 @@ const videosRoutes: Routes = [
36 } 32 }
37 }, 33 },
38 { 34 {
35 path: 'subscriptions',
36 component: VideoUserSubscriptionsComponent,
37 data: {
38 meta: {
39 title: 'Subscriptions'
40 }
41 }
42 },
43 {
39 path: 'local', 44 path: 'local',
40 component: VideoLocalComponent, 45 component: VideoLocalComponent,
41 data: { 46 data: {
diff --git a/client/src/app/videos/videos.module.ts b/client/src/app/videos/videos.module.ts
index c38257e08..3c3877273 100644
--- a/client/src/app/videos/videos.module.ts
+++ b/client/src/app/videos/videos.module.ts
@@ -5,6 +5,7 @@ import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.c
5import { VideoTrendingComponent } from './video-list/video-trending.component' 5import { VideoTrendingComponent } from './video-list/video-trending.component'
6import { VideosRoutingModule } from './videos-routing.module' 6import { VideosRoutingModule } from './videos-routing.module'
7import { VideosComponent } from './videos.component' 7import { VideosComponent } from './videos.component'
8import { VideoUserSubscriptionsComponent } from '@app/videos/video-list/video-user-subscriptions.component'
8 9
9@NgModule({ 10@NgModule({
10 imports: [ 11 imports: [
@@ -17,7 +18,8 @@ import { VideosComponent } from './videos.component'
17 18
18 VideoTrendingComponent, 19 VideoTrendingComponent,
19 VideoRecentlyAddedComponent, 20 VideoRecentlyAddedComponent,
20 VideoLocalComponent 21 VideoLocalComponent,
22 VideoUserSubscriptionsComponent
21 ], 23 ],
22 24
23 exports: [ 25 exports: [
diff --git a/client/src/assets/images/menu/podcasts.svg b/client/src/assets/images/menu/podcasts.svg
new file mode 100644
index 000000000..cd6efc54e
--- /dev/null
+++ b/client/src/assets/images/menu/podcasts.svg
@@ -0,0 +1,26 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>podcasts</title>
5 <desc>Created with Sketch.</desc>
6 <defs>
7 <linearGradient x1="50%" y1="0%" x2="50%" y2="97.3333865%" id="linearGradient-1">
8 <stop stop-color="#808080" offset="0%"></stop>
9 <stop stop-color="#808080" stop-opacity="0.247310915" offset="100%"></stop>
10 </linearGradient>
11 <linearGradient x1="50%" y1="0%" x2="50%" y2="97.8635204%" id="linearGradient-2">
12 <stop stop-color="#808080" offset="0%"></stop>
13 <stop stop-color="#808080" stop-opacity="0.250707654" offset="100%"></stop>
14 </linearGradient>
15 </defs>
16 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
17 <g id="Artboard-4" transform="translate(-532.000000, -775.000000)">
18 <g id="312" transform="translate(532.000000, 775.000000)">
19 <circle id="Oval-169" fill="#808080" cx="12" cy="10" r="3"></circle>
20 <path d="M16.3851456,13.8501206 C17.4222377,12.6991612 18,11.4167199 18,10 C18,6.74089158 15.2591084,4 12,4 C8.74089158,4 6,6.74089158 6,10 C6,11.4186069 6.57916224,12.7027674 7.61838071,13.8540306 C7.80341316,14.0590125 8.11958231,14.0751848 8.32456427,13.8901523 C8.52954623,13.7051199 8.5457185,13.3889507 8.36068606,13.1839688 C7.47616718,12.2040844 7,11.148292 7,10 C7,7.29317633 9.29317633,5 12,5 C14.7068237,5 17,7.29317633 17,10 C17,11.1466944 16.5249958,12.2010466 15.6422459,13.1807178 C15.4573954,13.3858639 15.4738483,13.7020185 15.6789944,13.886869 C15.8841405,14.0717195 16.2002951,14.0552666 16.3851456,13.8501206 Z" id="Oval-169" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
21 <path d="M17.5678226,18.3077078 C20.3159646,16.4626239 22,13.3733223 22,10 C22,4.4771525 17.5228475,0 12,0 C6.4771525,0 2,4.4771525 2,10 C2,13.3762414 3.68696556,16.4678678 6.43901638,18.3122954 C6.89779529,18.6197696 7.51896613,18.4971129 7.82644029,18.0383339 C8.13391444,17.579555 8.0112577,16.9583842 7.55247879,16.65091 C5.34877306,15.1739839 4,12.7021478 4,10 C4,5.581722 7.581722,2 12,2 C16.418278,2 20,5.581722 20,10 C20,12.699815 18.6535741,15.1697843 16.4529947,16.6472384 C15.9944687,16.9550897 15.8723227,17.5763611 16.180174,18.0348871 C16.4880252,18.4934131 17.1092967,18.6155591 17.5678226,18.3077078 Z" id="Oval-169" fill="url(#linearGradient-2)" fill-rule="nonzero"></path>
22 <path d="M9.32918137,15.9750882 C9.14737952,14.8842771 9.89826062,14 10.9979131,14 L13.0020869,14 C14.1055038,14 14.8534426,14.8793447 14.6708186,15.9750882 L13.6633817,22.0197096 C13.5731485,22.561109 13.0573397,23 12.5010434,23 L11.4989566,23 C10.9472481,23 10.4276519,22.5659113 10.3366183,22.0197096 L9.32918137,15.9750882 Z" id="Rectangle-217" fill="#808080"></path>
23 </g>
24 </g>
25 </g>
26</svg>
diff --git a/client/src/assets/images/menu/subscriptions.svg b/client/src/assets/images/menu/subscriptions.svg
new file mode 100644
index 000000000..cd6efc54e
--- /dev/null
+++ b/client/src/assets/images/menu/subscriptions.svg
@@ -0,0 +1,26 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>podcasts</title>
5 <desc>Created with Sketch.</desc>
6 <defs>
7 <linearGradient x1="50%" y1="0%" x2="50%" y2="97.3333865%" id="linearGradient-1">
8 <stop stop-color="#808080" offset="0%"></stop>
9 <stop stop-color="#808080" stop-opacity="0.247310915" offset="100%"></stop>
10 </linearGradient>
11 <linearGradient x1="50%" y1="0%" x2="50%" y2="97.8635204%" id="linearGradient-2">
12 <stop stop-color="#808080" offset="0%"></stop>
13 <stop stop-color="#808080" stop-opacity="0.250707654" offset="100%"></stop>
14 </linearGradient>
15 </defs>
16 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
17 <g id="Artboard-4" transform="translate(-532.000000, -775.000000)">
18 <g id="312" transform="translate(532.000000, 775.000000)">
19 <circle id="Oval-169" fill="#808080" cx="12" cy="10" r="3"></circle>
20 <path d="M16.3851456,13.8501206 C17.4222377,12.6991612 18,11.4167199 18,10 C18,6.74089158 15.2591084,4 12,4 C8.74089158,4 6,6.74089158 6,10 C6,11.4186069 6.57916224,12.7027674 7.61838071,13.8540306 C7.80341316,14.0590125 8.11958231,14.0751848 8.32456427,13.8901523 C8.52954623,13.7051199 8.5457185,13.3889507 8.36068606,13.1839688 C7.47616718,12.2040844 7,11.148292 7,10 C7,7.29317633 9.29317633,5 12,5 C14.7068237,5 17,7.29317633 17,10 C17,11.1466944 16.5249958,12.2010466 15.6422459,13.1807178 C15.4573954,13.3858639 15.4738483,13.7020185 15.6789944,13.886869 C15.8841405,14.0717195 16.2002951,14.0552666 16.3851456,13.8501206 Z" id="Oval-169" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
21 <path d="M17.5678226,18.3077078 C20.3159646,16.4626239 22,13.3733223 22,10 C22,4.4771525 17.5228475,0 12,0 C6.4771525,0 2,4.4771525 2,10 C2,13.3762414 3.68696556,16.4678678 6.43901638,18.3122954 C6.89779529,18.6197696 7.51896613,18.4971129 7.82644029,18.0383339 C8.13391444,17.579555 8.0112577,16.9583842 7.55247879,16.65091 C5.34877306,15.1739839 4,12.7021478 4,10 C4,5.581722 7.581722,2 12,2 C16.418278,2 20,5.581722 20,10 C20,12.699815 18.6535741,15.1697843 16.4529947,16.6472384 C15.9944687,16.9550897 15.8723227,17.5763611 16.180174,18.0348871 C16.4880252,18.4934131 17.1092967,18.6155591 17.5678226,18.3077078 Z" id="Oval-169" fill="url(#linearGradient-2)" fill-rule="nonzero"></path>
22 <path d="M9.32918137,15.9750882 C9.14737952,14.8842771 9.89826062,14 10.9979131,14 L13.0020869,14 C14.1055038,14 14.8534426,14.8793447 14.6708186,15.9750882 L13.6633817,22.0197096 C13.5731485,22.561109 13.0573397,23 12.5010434,23 L11.4989566,23 C10.9472481,23 10.4276519,22.5659113 10.3366183,22.0197096 L9.32918137,15.9750882 Z" id="Rectangle-217" fill="#808080"></path>
23 </g>
24 </g>
25 </g>
26</svg>
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index dc0ffe912..b2d7c2bec 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -83,6 +83,7 @@ label {
83 display: flex; 83 display: flex;
84 align-items: center; 84 align-items: center;
85 padding-left: $not-expanded-horizontal-margins; 85 padding-left: $not-expanded-horizontal-margins;
86 padding-right: $not-expanded-horizontal-margins;
86 } 87 }
87 88
88 // Override some properties if the main content is expanded (no menu on the left) 89 // Override some properties if the main content is expanded (no menu on the left)
@@ -96,6 +97,7 @@ label {
96 97
97 .sub-menu { 98 .sub-menu {
98 padding-left: $expanded-horizontal-margins; 99 padding-left: $expanded-horizontal-margins;
100 padding-right: $expanded-horizontal-margins;
99 } 101 }
100 } 102 }
101} 103}
@@ -294,6 +296,10 @@ table {
294 296
295 .sub-menu { 297 .sub-menu {
296 padding-left: 50px; 298 padding-left: 50px;
299
300 .title-page {
301 font-size: 15px;
302 }
297 } 303 }
298 } 304 }
299 } 305 }
@@ -316,6 +322,7 @@ table {
316 322
317 .sub-menu { 323 .sub-menu {
318 padding-left: 15px; 324 padding-left: 15px;
325 padding-right: 15px;
319 margin-bottom: 10px; 326 margin-bottom: 10px;
320 } 327 }
321 328
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index b0b0f544c..aafe478f9 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -335,6 +335,27 @@
335 font-size: 13px; 335 font-size: 13px;
336} 336}
337 337
338@mixin actor-owner {
339 @include disable-default-a-behaviour;
340
341 display: block;
342 font-size: 13px;
343 margin-top: 4px;
344 color: #000;
345
346 span:hover {
347 opacity: 0.8;
348 }
349
350 img {
351 @include avatar(18px);
352
353 margin-left: 7px;
354 position: relative;
355 top: -2px;
356 }
357}
358
338@mixin sub-menu-with-actor { 359@mixin sub-menu-with-actor {
339 height: 160px; 360 height: 160px;
340 display: flex; 361 display: flex;
@@ -371,7 +392,7 @@
371 position: relative; 392 position: relative;
372 top: 3px; 393 top: 3px;
373 font-size: 14px; 394 font-size: 14px;
374 color: #777272; 395 color: $grey-actor-name;
375 } 396 }
376 } 397 }
377 398
@@ -380,24 +401,7 @@
380 } 401 }
381 402
382 .actor-owner { 403 .actor-owner {
383 @include disable-default-a-behaviour; 404 @include actor-owner;
384
385 display: block;
386 font-size: 13px;
387 margin-top: 4px;
388 color: #000;
389
390 span:hover {
391 opacity: 0.8;
392 }
393
394 img {
395 @include avatar(18px);
396
397 margin-left: 7px;
398 position: relative;
399 top: -2px;
400 }
401 } 405 }
402 } 406 }
403 } 407 }
@@ -426,3 +430,18 @@
426 background-image: url($imageUrl); 430 background-image: url($imageUrl);
427 } 431 }
428} 432}
433
434@mixin row-blocks {
435 display: flex;
436 min-height: 130px;
437 padding-bottom: 20px;
438 margin-bottom: 20px;
439 border-bottom: 1px solid #C6C6C6;
440
441 @media screen and (max-width: 800px) {
442 flex-direction: column;
443 height: auto;
444 text-align: center;
445 align-items: center;
446 }
447} \ No newline at end of file
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss
index f1f755126..e6db98642 100644
--- a/client/src/sass/include/_variables.scss
+++ b/client/src/sass/include/_variables.scss
@@ -12,6 +12,8 @@ $black-background: #000;
12$grey-background: #f6f2f2; 12$grey-background: #f6f2f2;
13$red-error: #FF0000; 13$red-error: #FF0000;
14 14
15$grey-actor-name: #777272;
16
15$expanded-horizontal-margins: 150px; 17$expanded-horizontal-margins: 150px;
16$not-expanded-horizontal-margins: 30px; 18$not-expanded-horizontal-margins: 30px;
17 19