aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+my-library/+my-video-channels
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-11-12 15:28:54 +0100
committerChocobozzz <chocobozzz@cpy.re>2020-11-13 12:02:21 +0100
commit17119e4a546522468878cf115558b17949ab50d0 (patch)
tree3f130cfd7fdccf5aeeac9beee941750590239047 /client/src/app/+my-library/+my-video-channels
parentb4bc269e5517849b5b89052f0c1a2c01b6f65089 (diff)
downloadPeerTube-17119e4a546522468878cf115558b17949ab50d0.tar.gz
PeerTube-17119e4a546522468878cf115558b17949ab50d0.tar.zst
PeerTube-17119e4a546522468878cf115558b17949ab50d0.zip
Reorganize left menu and account menu
Add my-settings and my-library in left menu Move administration below my-library Split account menu: my-setting and my library
Diffstat (limited to 'client/src/app/+my-library/+my-video-channels')
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts78
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html105
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss67
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts22
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts135
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts41
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.html49
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss125
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts171
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts31
10 files changed, 824 insertions, 0 deletions
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
new file mode 100644
index 000000000..1d0cbf246
--- /dev/null
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
@@ -0,0 +1,78 @@
1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router'
3import { AuthService, Notifier } from '@app/core'
4import {
5 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
6 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
7 VIDEO_CHANNEL_NAME_VALIDATOR,
8 VIDEO_CHANNEL_SUPPORT_VALIDATOR
9} from '@app/shared/form-validators/video-channel-validators'
10import { FormValidatorService } from '@app/shared/shared-forms'
11import { VideoChannelService } from '@app/shared/shared-main'
12import { VideoChannelCreate } from '@shared/models'
13import { MyVideoChannelEdit } from './my-video-channel-edit'
14
15@Component({
16 templateUrl: './my-video-channel-edit.component.html',
17 styleUrls: [ './my-video-channel-edit.component.scss' ]
18})
19export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit {
20 error: string
21
22 constructor (
23 protected formValidatorService: FormValidatorService,
24 private authService: AuthService,
25 private notifier: Notifier,
26 private router: Router,
27 private videoChannelService: VideoChannelService
28 ) {
29 super()
30 }
31
32 ngOnInit () {
33 this.buildForm({
34 name: VIDEO_CHANNEL_NAME_VALIDATOR,
35 'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
36 description: VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
37 support: VIDEO_CHANNEL_SUPPORT_VALIDATOR
38 })
39 }
40
41 formValidated () {
42 this.error = undefined
43
44 const body = this.form.value
45 const videoChannelCreate: VideoChannelCreate = {
46 name: body.name,
47 displayName: body['display-name'],
48 description: body.description || null,
49 support: body.support || null
50 }
51
52 this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe(
53 () => {
54 this.authService.refreshUserInformation()
55
56 this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`)
57 this.router.navigate([ '/my-library', 'video-channels' ])
58 },
59
60 err => {
61 if (err.status === 409) {
62 this.error = $localize`This name already exists on this instance.`
63 return
64 }
65
66 this.error = err.message
67 }
68 )
69 }
70
71 isCreation () {
72 return true
73 }
74
75 getFormButtonTitle () {
76 return $localize`Create`
77 }
78}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
new file mode 100644
index 000000000..7e0c4e732
--- /dev/null
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
@@ -0,0 +1,105 @@
1<nav aria-label="breadcrumb">
2 <ol class="breadcrumb">
3 <li class="breadcrumb-item">
4 <a routerLink="/my-library/video-channels" i18n>My Channels</a>
5 </li>
6
7 <ng-container *ngIf="isCreation()">
8 <li class="breadcrumb-item active" i18n>Create</li>
9 </ng-container>
10 <ng-container *ngIf="!isCreation()">
11 <li class="breadcrumb-item active" i18n>Edit</li>
12 <li class="breadcrumb-item active" aria-current="page">
13 <a *ngIf="videoChannelToUpdate" [routerLink]="[ '/my-library/video-channels/update', videoChannelToUpdate?.nameWithHost ]">{{ videoChannelToUpdate?.displayName }}</a>
14 </li>
15 </ng-container>
16 </ol>
17</nav>
18
19<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
20
21<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
22
23 <div class="form-row"> <!-- channel grid -->
24 <div class="form-group col-12 col-lg-4 col-xl-3">
25 <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
26 <div *ngIf="!isCreation() && videoChannelToUpdate" class="video-channel-title" i18n>CHANNEL</div>
27 </div>
28
29 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
30
31 <div class="form-group" *ngIf="isCreation()">
32 <label i18n for="name">Name</label>
33 <div class="input-group">
34 <input
35 type="text" id="name" i18n-placeholder placeholder="Example: my_channel"
36 formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control"
37 >
38 <div class="input-group-append">
39 <span class="input-group-text">@{{ instanceHost }}</span>
40 </div>
41 </div>
42 <div *ngIf="formErrors['name']" class="form-error">
43 {{ formErrors['name'] }}
44 </div>
45 </div>
46
47 <my-actor-avatar-info
48 *ngIf="!isCreation() && videoChannelToUpdate"
49 [actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)"
50 ></my-actor-avatar-info>
51
52 <div class="form-group">
53 <label i18n for="display-name">Display name</label>
54 <input
55 type="text" id="display-name" class="form-control"
56 formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
57 >
58 <div *ngIf="formErrors['display-name']" class="form-error">
59 {{ formErrors['display-name'] }}
60 </div>
61 </div>
62
63 <div class="form-group">
64 <label i18n for="description">Description</label>
65 <textarea
66 id="description" formControlName="description" class="form-control"
67 [ngClass]="{ 'input-error': formErrors['description'] }"
68 ></textarea>
69 <div *ngIf="formErrors.description" class="form-error">
70 {{ formErrors.description }}
71 </div>
72 </div>
73
74 <div class="form-group">
75 <label for="support">Support</label>
76 <my-help
77 helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support your channel (membership platform...).<br /><br />
78 When you will upload a video in this channel, the video support field will be automatically filled by this text."
79 ></my-help>
80 <my-markdown-textarea
81 id="support" formControlName="support" textareaMaxWidth="500px" markdownType="enhanced"
82 [classes]="{ 'input-error': formErrors['support'] }"
83 ></my-markdown-textarea>
84 <div *ngIf="formErrors.support" class="form-error">
85 {{ formErrors.support }}
86 </div>
87 </div>
88
89 <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()">
90 <my-peertube-checkbox
91 inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate"
92 i18n-labelText labelText="Overwrite support field of all videos of this channel"
93 ></my-peertube-checkbox>
94 </div>
95
96 </div>
97 </div>
98
99 <div class="form-row"> <!-- submit placement block -->
100 <div class="col-md-7 col-xl-5"></div>
101 <div class="col-md-5 col-xl-5 d-inline-flex">
102 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
103 </div>
104 </div>
105</form>
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
new file mode 100644
index 000000000..8f8af655c
--- /dev/null
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
@@ -0,0 +1,67 @@
1@import '_variables';
2@import '_mixins';
3
4label {
5 font-weight: $font-regular;
6 font-size: 100%;
7}
8
9.video-channel-title {
10 @include settings-big-title;
11}
12
13my-actor-avatar-info {
14 display: block;
15 margin-bottom: 20px;
16}
17
18.input-group {
19 @include peertube-input-group(fit-content);
20}
21
22.input-group-append {
23 height: 30px;
24}
25
26input {
27 &[type=text] {
28 @include peertube-input-text(340px);
29
30 display: block;
31
32 &#name {
33 width: auto;
34 flex-grow: 1;
35 }
36 }
37
38 &[type=submit] {
39 @include peertube-button;
40 @include orange-button;
41 margin-left: auto;
42 }
43}
44
45textarea {
46 @include peertube-textarea(500px, 150px);
47
48 display: block;
49}
50
51.peertube-select-container {
52 @include peertube-select-container(340px);
53}
54
55.breadcrumb {
56 @include breadcrumb;
57}
58
59@media screen and (max-width: $small-view) {
60 input[type=text]#name {
61 width: auto !important;
62 }
63
64 label[for=name] + div, textarea {
65 width: 100%;
66 }
67}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
new file mode 100644
index 000000000..09db0df9d
--- /dev/null
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
@@ -0,0 +1,22 @@
1import { FormReactive } from '@app/shared/shared-forms'
2import { VideoChannel } from '@app/shared/shared-main'
3
4export abstract class MyVideoChannelEdit extends FormReactive {
5 // We need it even in the create component because it's used in the edit template
6 videoChannelToUpdate: VideoChannel
7
8 abstract isCreation (): boolean
9 abstract getFormButtonTitle (): string
10
11 get instanceHost () {
12 return window.location.host
13 }
14
15 // We need this method so angular does not complain in child template that doesn't need this
16 onAvatarChange (formData: FormData) { /* empty */ }
17
18 // Should be implemented by the child
19 isBulkUpdateVideosDisplayed () {
20 return false
21 }
22}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
new file mode 100644
index 000000000..c6cb5ade6
--- /dev/null
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
@@ -0,0 +1,135 @@
1import { Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, Notifier, ServerService } from '@app/core'
5import {
6 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
7 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
8 VIDEO_CHANNEL_SUPPORT_VALIDATOR
9} from '@app/shared/form-validators/video-channel-validators'
10import { FormValidatorService } from '@app/shared/shared-forms'
11import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
12import { ServerConfig, VideoChannelUpdate } from '@shared/models'
13import { MyVideoChannelEdit } from './my-video-channel-edit'
14
15@Component({
16 selector: 'my-video-channel-update',
17 templateUrl: './my-video-channel-edit.component.html',
18 styleUrls: [ './my-video-channel-edit.component.scss' ]
19})
20export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy {
21 error: string
22 videoChannelToUpdate: VideoChannel
23
24 private paramsSub: Subscription
25 private oldSupportField: string
26 private serverConfig: ServerConfig
27
28 constructor (
29 protected formValidatorService: FormValidatorService,
30 private authService: AuthService,
31 private notifier: Notifier,
32 private router: Router,
33 private route: ActivatedRoute,
34 private videoChannelService: VideoChannelService,
35 private serverService: ServerService
36 ) {
37 super()
38 }
39
40 ngOnInit () {
41 this.serverConfig = this.serverService.getTmpConfig()
42 this.serverService.getConfig()
43 .subscribe(config => this.serverConfig = config)
44
45 this.buildForm({
46 'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
47 description: VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
48 support: VIDEO_CHANNEL_SUPPORT_VALIDATOR,
49 bulkVideosSupportUpdate: null
50 })
51
52 this.paramsSub = this.route.params.subscribe(routeParams => {
53 const videoChannelId = routeParams['videoChannelId']
54
55 this.videoChannelService.getVideoChannel(videoChannelId).subscribe(
56 videoChannelToUpdate => {
57 this.videoChannelToUpdate = videoChannelToUpdate
58
59 this.oldSupportField = videoChannelToUpdate.support
60
61 this.form.patchValue({
62 'display-name': videoChannelToUpdate.displayName,
63 description: videoChannelToUpdate.description,
64 support: videoChannelToUpdate.support
65 })
66 },
67
68 err => this.error = err.message
69 )
70 })
71 }
72
73 ngOnDestroy () {
74 if (this.paramsSub) this.paramsSub.unsubscribe()
75 }
76
77 formValidated () {
78 this.error = undefined
79
80 const body = this.form.value
81 const videoChannelUpdate: VideoChannelUpdate = {
82 displayName: body['display-name'],
83 description: body.description || null,
84 support: body.support || null,
85 bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false
86 }
87
88 this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe(
89 () => {
90 this.authService.refreshUserInformation()
91
92 this.notifier.success($localize`Video channel ${videoChannelUpdate.displayName} updated.`)
93
94 this.router.navigate([ '/my-library', 'video-channels' ])
95 },
96
97 err => this.error = err.message
98 )
99 }
100
101 onAvatarChange (formData: FormData) {
102 this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData)
103 .subscribe(
104 data => {
105 this.notifier.success($localize`Avatar changed.`)
106
107 this.videoChannelToUpdate.updateAvatar(data.avatar)
108 },
109
110 err => this.notifier.error(err.message)
111 )
112 }
113
114 get maxAvatarSize () {
115 return this.serverConfig.avatar.file.size.max
116 }
117
118 get avatarExtensions () {
119 return this.serverConfig.avatar.file.extensions.join(',')
120 }
121
122 isCreation () {
123 return false
124 }
125
126 getFormButtonTitle () {
127 return $localize`Update`
128 }
129
130 isBulkUpdateVideosDisplayed () {
131 if (this.oldSupportField === undefined) return false
132
133 return this.oldSupportField !== this.form.value['support']
134 }
135}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts
new file mode 100644
index 000000000..6b8efad0b
--- /dev/null
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels-routing.module.ts
@@ -0,0 +1,41 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component'
4import { MyVideoChannelCreateComponent } from './my-video-channel-create.component'
5import { MyVideoChannelsComponent } from './my-video-channels.component'
6
7const myVideoChannelsRoutes: Routes = [
8 {
9 path: '',
10 component: MyVideoChannelsComponent,
11 data: {
12 meta: {
13 title: $localize`My video channels`
14 }
15 }
16 },
17 {
18 path: 'create',
19 component: MyVideoChannelCreateComponent,
20 data: {
21 meta: {
22 title: $localize`Create a new video channel`
23 }
24 }
25 },
26 {
27 path: 'update/:videoChannelId',
28 component: MyVideoChannelUpdateComponent,
29 data: {
30 meta: {
31 title: $localize`Update video channel`
32 }
33 }
34 }
35]
36
37@NgModule({
38 imports: [ RouterModule.forChild(myVideoChannelsRoutes) ],
39 exports: [ RouterModule ]
40})
41export class MyVideoChannelsRoutingModule {}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
new file mode 100644
index 000000000..205d23cd5
--- /dev/null
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
@@ -0,0 +1,49 @@
1<h1>
2 <span>
3 <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon>
4 <ng-container i18n>My channels</ng-container>
5 <span class="badge badge-secondary">{{ totalItems }}</span>
6 </span>
7</h1>
8
9<div class="video-channels-header d-flex justify-content-between">
10 <div class="has-feedback has-clear">
11 <input type="text" placeholder="Search your channels" i18n-placeholder [(ngModel)]="channelsSearch"
12 (ngModelChange)="onChannelsSearchChanged()" />
13 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
14 <span class="sr-only" i18n>Clear filters</span>
15 </div>
16
17 <a class="create-button" routerLink="create">
18 <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
19 <ng-container i18n>Create video channel</ng-container>
20 </a>
21</div>
22
23<div class="video-channels">
24 <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel">
25 <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]">
26 <img [src]="videoChannel.avatarUrl" alt="Avatar" />
27 </a>
28
29 <div class="video-channel-info">
30 <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
31 <div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
32 <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
33 </a>
34
35 <div i18n class="video-channel-followers">{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
36
37 <div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div>
38
39 <div class="video-channel-buttons">
40 <my-edit-button label [routerLink]="[ 'update', videoChannel.nameWithHost ]"></my-edit-button>
41 <my-delete-button label (click)="deleteVideoChannel(videoChannel)"></my-delete-button>
42 </div>
43
44 <div *ngIf="!isInSmallView" class="w-100 d-flex justify-content-end">
45 <p-chart *ngIf="videoChannelsChartData && videoChannelsChartData[i]" type="line" [data]="videoChannelsChartData[i]" [options]="chartOptions" width="40vw" height="100px"></p-chart>
46 </div>
47 </div>
48 </div>
49</div>
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss
new file mode 100644
index 000000000..f2f42459f
--- /dev/null
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss
@@ -0,0 +1,125 @@
1@import '_variables';
2@import '_mixins';
3
4.create-button {
5 @include create-button;
6}
7
8input[type=text] {
9 @include peertube-input-text(300px);
10}
11
12::ng-deep .action-button {
13 &.action-button-edit {
14 margin-right: 10px;
15 }
16}
17
18.video-channel {
19 @include row-blocks;
20 padding-bottom: 0;
21
22 img {
23 @include avatar(80px);
24
25 margin-right: 10px;
26 }
27
28 .video-channel-info {
29 flex-grow: 1;
30
31 a.video-channel-names {
32 @include disable-default-a-behaviour;
33
34 width: fit-content;
35 display: flex;
36 align-items: baseline;
37 color: pvar(--mainForegroundColor);
38
39 .video-channel-display-name {
40 font-weight: $font-semibold;
41 font-size: 18px;
42 }
43
44 .video-channel-name {
45 font-size: 14px;
46 color: $grey-actor-name;
47 margin-left: 5px;
48 }
49 }
50 }
51
52 .video-channel-buttons {
53 margin-top: 10px;
54 min-width: 190px;
55 }
56}
57
58::ng-deep .chartjs-render-monitor {
59 position: relative;
60 top: 1px;
61}
62
63.video-channels-header {
64 margin-bottom: 30px;
65}
66
67@media screen and (max-width: $small-view) {
68 .video-channels-header {
69 text-align: center;
70 }
71
72 .video-channel {
73 padding-bottom: 10px;
74
75 .video-channel-info {
76 padding-bottom: 10px;
77 text-align: center;
78
79 .video-channel-names {
80 flex-direction: column;
81 align-items: center !important;
82 margin: auto;
83
84 .video-channel-name {
85 margin-left: 0px !important;
86 }
87 }
88 }
89
90 img {
91 margin-right: 0;
92 }
93
94 .video-channel-buttons {
95 align-self: center;
96 }
97 }
98}
99
100@media screen and (max-width: $mobile-view) {
101 .video-channels-header {
102 flex-direction: column;
103
104 input[type=text] {
105 width: 100% !important;
106 margin-bottom: 12px;
107 }
108 }
109}
110
111@media screen and (min-width: breakpoint(lg)) {
112 :host-context(.main-col:not(.expanded)) {
113 .video-channel-buttons {
114 float: right;
115 }
116 }
117}
118
119@media screen and (min-width: $small-view) {
120 :host-context(.expanded) {
121 .video-channel-buttons {
122 float: right;
123 }
124 }
125}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts
new file mode 100644
index 000000000..a63e98a51
--- /dev/null
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts
@@ -0,0 +1,171 @@
1import { ChartData } from 'chart.js'
2import { max, maxBy, min, minBy } from 'lodash-es'
3import { Subject } from 'rxjs'
4import { debounceTime, mergeMap } from 'rxjs/operators'
5import { Component, OnInit } from '@angular/core'
6import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core'
7import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
8
9@Component({
10 templateUrl: './my-video-channels.component.html',
11 styleUrls: [ './my-video-channels.component.scss' ]
12})
13export class MyVideoChannelsComponent implements OnInit {
14 totalItems: number
15
16 videoChannels: VideoChannel[] = []
17 videoChannelsChartData: ChartData[]
18 videoChannelsMinimumDailyViews = 0
19 videoChannelsMaximumDailyViews: number
20
21 channelsSearch: string
22 channelsSearchChanged = new Subject<string>()
23
24 private user: User
25
26 constructor (
27 private authService: AuthService,
28 private notifier: Notifier,
29 private confirmService: ConfirmService,
30 private videoChannelService: VideoChannelService,
31 private screenService: ScreenService
32 ) {}
33
34 ngOnInit () {
35 this.user = this.authService.getUser()
36
37 this.loadVideoChannels()
38
39 this.channelsSearchChanged
40 .pipe(debounceTime(500))
41 .subscribe(() => {
42 this.loadVideoChannels()
43 })
44 }
45
46 get isInSmallView () {
47 return this.screenService.isInSmallView()
48 }
49
50 get chartOptions () {
51 return {
52 legend: {
53 display: false
54 },
55 scales: {
56 xAxes: [{
57 display: false
58 }],
59 yAxes: [{
60 display: false,
61 ticks: {
62 min: Math.max(0, this.videoChannelsMinimumDailyViews - (3 * this.videoChannelsMaximumDailyViews / 100)),
63 max: Math.max(1, this.videoChannelsMaximumDailyViews)
64 }
65 }]
66 },
67 layout: {
68 padding: {
69 left: 15,
70 right: 15,
71 top: 10,
72 bottom: 0
73 }
74 },
75 elements: {
76 point: {
77 radius: 0
78 }
79 },
80 tooltips: {
81 mode: 'index',
82 intersect: false,
83 custom: function (tooltip: any) {
84 if (!tooltip) return
85 // disable displaying the color box
86 tooltip.displayColors = false
87 },
88 callbacks: {
89 label: (tooltip: any, data: any) => `${tooltip.value} views`
90 }
91 },
92 hover: {
93 mode: 'index',
94 intersect: false
95 }
96 }
97 }
98
99 resetSearch () {
100 this.channelsSearch = ''
101 this.onChannelsSearchChanged()
102 }
103
104 onChannelsSearchChanged () {
105 this.channelsSearchChanged.next()
106 }
107
108 async deleteVideoChannel (videoChannel: VideoChannel) {
109 const res = await this.confirmService.confirmWithInput(
110 $localize`Do you really want to delete ${videoChannel.displayName}?
111It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another
112channel with the same name (${videoChannel.name})!`,
113
114 $localize`Please type the display name of the video channel (${videoChannel.displayName}) to confirm`,
115
116 videoChannel.displayName,
117
118 $localize`Delete`
119 )
120 if (res === false) return
121
122 this.videoChannelService.removeVideoChannel(videoChannel)
123 .subscribe(
124 () => {
125 this.loadVideoChannels()
126 this.notifier.success($localize`Video channel ${videoChannel.displayName} deleted.`)
127 },
128
129 error => this.notifier.error(error.message)
130 )
131 }
132
133 private loadVideoChannels () {
134 this.authService.userInformationLoaded
135 .pipe(mergeMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true, this.channelsSearch)))
136 .subscribe(res => {
137 this.videoChannels = res.data
138 this.totalItems = res.total
139
140 // chart data
141 this.videoChannelsChartData = this.videoChannels.map(v => ({
142 labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()),
143 datasets: [
144 {
145 label: $localize`Views for the day`,
146 data: v.viewsPerDay.map(day => day.views),
147 fill: false,
148 borderColor: '#c6c6c6'
149 }
150 ]
151 } as ChartData))
152
153 // chart options that depend on chart data:
154 // we don't want to skew values and have min at 0, so we define what the floor/ceiling is here
155 this.videoChannelsMinimumDailyViews = min(
156 // compute local minimum daily views for each channel, by their "views" attribute
157 this.videoChannels.map(v => minBy(
158 v.viewsPerDay,
159 day => day.views
160 ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute
161 )
162 this.videoChannelsMaximumDailyViews = max(
163 // compute local maximum daily views for each channel, by their "views" attribute
164 this.videoChannels.map(v => maxBy(
165 v.viewsPerDay,
166 day => day.views
167 ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute
168 )
169 })
170 }
171}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
new file mode 100644
index 000000000..92b56db49
--- /dev/null
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
@@ -0,0 +1,31 @@
1import { ChartModule } from 'primeng/chart'
2import { NgModule } from '@angular/core'
3import { SharedFormModule } from '@app/shared/shared-forms'
4import { SharedGlobalIconModule } from '@app/shared/shared-icons'
5import { SharedMainModule } from '@app/shared/shared-main'
6import { MyVideoChannelCreateComponent } from './my-video-channel-create.component'
7import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component'
8import { MyVideoChannelsRoutingModule } from './my-video-channels-routing.module'
9import { MyVideoChannelsComponent } from './my-video-channels.component'
10
11@NgModule({
12 imports: [
13 MyVideoChannelsRoutingModule,
14
15 ChartModule,
16
17 SharedMainModule,
18 SharedFormModule,
19 SharedGlobalIconModule
20 ],
21
22 declarations: [
23 MyVideoChannelsComponent,
24 MyVideoChannelCreateComponent,
25 MyVideoChannelUpdateComponent
26 ],
27
28 exports: [],
29 providers: []
30})
31export class MyVideoChannelsModule { }