diff options
Diffstat (limited to 'client/src/app/+my-account')
45 files changed, 33 insertions, 3309 deletions
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts deleted file mode 100644 index e2ea87fb8..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts +++ /dev/null | |||
@@ -1,83 +0,0 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Router } from '@angular/router' | ||
3 | import { AuthService, Notifier } from '@app/core' | ||
4 | import { | ||
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' | ||
10 | import { FormValidatorService } from '@app/shared/shared-forms' | ||
11 | import { VideoChannelService } from '@app/shared/shared-main' | ||
12 | import { VideoChannelCreate } from '@shared/models' | ||
13 | import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' | ||
14 | |||
15 | @Component({ | ||
16 | selector: 'my-account-video-channel-create', | ||
17 | templateUrl: './my-account-video-channel-edit.component.html', | ||
18 | styleUrls: [ './my-account-video-channel-edit.component.scss' ] | ||
19 | }) | ||
20 | export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelEdit implements OnInit { | ||
21 | error: string | ||
22 | |||
23 | constructor ( | ||
24 | protected formValidatorService: FormValidatorService, | ||
25 | private authService: AuthService, | ||
26 | private notifier: Notifier, | ||
27 | private router: Router, | ||
28 | private videoChannelService: VideoChannelService | ||
29 | ) { | ||
30 | super() | ||
31 | } | ||
32 | |||
33 | get instanceHost () { | ||
34 | return window.location.host | ||
35 | } | ||
36 | |||
37 | ngOnInit () { | ||
38 | this.buildForm({ | ||
39 | name: VIDEO_CHANNEL_NAME_VALIDATOR, | ||
40 | 'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, | ||
41 | description: VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, | ||
42 | support: VIDEO_CHANNEL_SUPPORT_VALIDATOR | ||
43 | }) | ||
44 | } | ||
45 | |||
46 | formValidated () { | ||
47 | this.error = undefined | ||
48 | |||
49 | const body = this.form.value | ||
50 | const videoChannelCreate: VideoChannelCreate = { | ||
51 | name: body.name, | ||
52 | displayName: body['display-name'], | ||
53 | description: body.description || null, | ||
54 | support: body.support || null | ||
55 | } | ||
56 | |||
57 | this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe( | ||
58 | () => { | ||
59 | this.authService.refreshUserInformation() | ||
60 | |||
61 | this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`) | ||
62 | this.router.navigate([ '/my-account', 'video-channels' ]) | ||
63 | }, | ||
64 | |||
65 | err => { | ||
66 | if (err.status === 409) { | ||
67 | this.error = $localize`This name already exists on this instance.` | ||
68 | return | ||
69 | } | ||
70 | |||
71 | this.error = err.message | ||
72 | } | ||
73 | ) | ||
74 | } | ||
75 | |||
76 | isCreation () { | ||
77 | return true | ||
78 | } | ||
79 | |||
80 | getFormButtonTitle () { | ||
81 | return $localize`Create` | ||
82 | } | ||
83 | } | ||
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 deleted file mode 100644 index 048d143cd..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.html +++ /dev/null | |||
@@ -1,105 +0,0 @@ | |||
1 | <nav aria-label="breadcrumb"> | ||
2 | <ol class="breadcrumb"> | ||
3 | <li class="breadcrumb-item"> | ||
4 | <a routerLink="/my-account/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-account/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-account/+my-account-video-channels/my-account-video-channel-edit.component.scss b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.scss deleted file mode 100644 index 8f8af655c..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.scss +++ /dev/null | |||
@@ -1,67 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | label { | ||
5 | font-weight: $font-regular; | ||
6 | font-size: 100%; | ||
7 | } | ||
8 | |||
9 | .video-channel-title { | ||
10 | @include settings-big-title; | ||
11 | } | ||
12 | |||
13 | my-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 | |||
26 | input { | ||
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 | |||
45 | textarea { | ||
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-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 deleted file mode 100644 index 710c51d8e..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.ts +++ /dev/null | |||
@@ -1,19 +0,0 @@ | |||
1 | import { FormReactive } from '@app/shared/shared-forms' | ||
2 | import { VideoChannel } from '@app/shared/shared-main' | ||
3 | |||
4 | export abstract class MyAccountVideoChannelEdit extends FormReactive { | ||
5 | // We need it even in the create component because it's used in the edit template | ||
6 | videoChannelToUpdate: VideoChannel | ||
7 | instanceHost: string | ||
8 | |||
9 | abstract isCreation (): boolean | ||
10 | abstract getFormButtonTitle (): string | ||
11 | |||
12 | // We need this method so angular does not complain in child template that doesn't need this | ||
13 | onAvatarChange (formData: FormData) { /* empty */ } | ||
14 | |||
15 | // Should be implemented by the child | ||
16 | isBulkUpdateVideosDisplayed () { | ||
17 | return false | ||
18 | } | ||
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 deleted file mode 100644 index 01659b8da..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-update.component.ts +++ /dev/null | |||
@@ -1,135 +0,0 @@ | |||
1 | import { Subscription } from 'rxjs' | ||
2 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
3 | import { ActivatedRoute, Router } from '@angular/router' | ||
4 | import { AuthService, Notifier, ServerService } from '@app/core' | ||
5 | import { | ||
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' | ||
10 | import { FormValidatorService } from '@app/shared/shared-forms' | ||
11 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | ||
12 | import { ServerConfig, VideoChannelUpdate } from '@shared/models' | ||
13 | import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' | ||
14 | |||
15 | @Component({ | ||
16 | selector: 'my-account-video-channel-update', | ||
17 | templateUrl: './my-account-video-channel-edit.component.html', | ||
18 | styleUrls: [ './my-account-video-channel-edit.component.scss' ] | ||
19 | }) | ||
20 | export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit 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-account', '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-account/+my-account-video-channels/my-account-video-channels-routing.module.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts deleted file mode 100644 index 3aa3e360f..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts +++ /dev/null | |||
@@ -1,41 +0,0 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { RouterModule, Routes } from '@angular/router' | ||
3 | import { MyAccountVideoChannelUpdateComponent } from './my-account-video-channel-update.component' | ||
4 | import { MyAccountVideoChannelCreateComponent } from './my-account-video-channel-create.component' | ||
5 | import { MyAccountVideoChannelsComponent } from './my-account-video-channels.component' | ||
6 | |||
7 | const myAccountVideoChannelsRoutes: Routes = [ | ||
8 | { | ||
9 | path: '', | ||
10 | component: MyAccountVideoChannelsComponent, | ||
11 | data: { | ||
12 | meta: { | ||
13 | title: $localize`Account video channels` | ||
14 | } | ||
15 | } | ||
16 | }, | ||
17 | { | ||
18 | path: 'create', | ||
19 | component: MyAccountVideoChannelCreateComponent, | ||
20 | data: { | ||
21 | meta: { | ||
22 | title: $localize`Create new video channel` | ||
23 | } | ||
24 | } | ||
25 | }, | ||
26 | { | ||
27 | path: 'update/:videoChannelId', | ||
28 | component: MyAccountVideoChannelUpdateComponent, | ||
29 | data: { | ||
30 | meta: { | ||
31 | title: $localize`Update video channel` | ||
32 | } | ||
33 | } | ||
34 | } | ||
35 | ] | ||
36 | |||
37 | @NgModule({ | ||
38 | imports: [ RouterModule.forChild(myAccountVideoChannelsRoutes) ], | ||
39 | exports: [ RouterModule ] | ||
40 | }) | ||
41 | export class MyAccountVideoChannelsRoutingModule {} | ||
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html deleted file mode 100644 index 205d23cd5..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html +++ /dev/null | |||
@@ -1,49 +0,0 @@ | |||
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-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 deleted file mode 100644 index f2f42459f..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss +++ /dev/null | |||
@@ -1,125 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .create-button { | ||
5 | @include create-button; | ||
6 | } | ||
7 | |||
8 | input[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-account/+my-account-video-channels/my-account-video-channels.component.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts deleted file mode 100644 index 281801ff6..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts +++ /dev/null | |||
@@ -1,172 +0,0 @@ | |||
1 | import { ChartData } from 'chart.js' | ||
2 | import { max, maxBy, min, minBy } from 'lodash-es' | ||
3 | import { Subject } from 'rxjs' | ||
4 | import { debounceTime, mergeMap } from 'rxjs/operators' | ||
5 | import { Component, OnInit } from '@angular/core' | ||
6 | import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core' | ||
7 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-account-video-channels', | ||
11 | templateUrl: './my-account-video-channels.component.html', | ||
12 | styleUrls: [ './my-account-video-channels.component.scss' ] | ||
13 | }) | ||
14 | export class MyAccountVideoChannelsComponent implements OnInit { | ||
15 | totalItems: number | ||
16 | |||
17 | videoChannels: VideoChannel[] = [] | ||
18 | videoChannelsChartData: ChartData[] | ||
19 | videoChannelsMinimumDailyViews = 0 | ||
20 | videoChannelsMaximumDailyViews: number | ||
21 | |||
22 | channelsSearch: string | ||
23 | channelsSearchChanged = new Subject<string>() | ||
24 | |||
25 | private user: User | ||
26 | |||
27 | constructor ( | ||
28 | private authService: AuthService, | ||
29 | private notifier: Notifier, | ||
30 | private confirmService: ConfirmService, | ||
31 | private videoChannelService: VideoChannelService, | ||
32 | private screenService: ScreenService | ||
33 | ) {} | ||
34 | |||
35 | ngOnInit () { | ||
36 | this.user = this.authService.getUser() | ||
37 | |||
38 | this.loadVideoChannels() | ||
39 | |||
40 | this.channelsSearchChanged | ||
41 | .pipe(debounceTime(500)) | ||
42 | .subscribe(() => { | ||
43 | this.loadVideoChannels() | ||
44 | }) | ||
45 | } | ||
46 | |||
47 | get isInSmallView () { | ||
48 | return this.screenService.isInSmallView() | ||
49 | } | ||
50 | |||
51 | get chartOptions () { | ||
52 | return { | ||
53 | legend: { | ||
54 | display: false | ||
55 | }, | ||
56 | scales: { | ||
57 | xAxes: [{ | ||
58 | display: false | ||
59 | }], | ||
60 | yAxes: [{ | ||
61 | display: false, | ||
62 | ticks: { | ||
63 | min: Math.max(0, this.videoChannelsMinimumDailyViews - (3 * this.videoChannelsMaximumDailyViews / 100)), | ||
64 | max: Math.max(1, this.videoChannelsMaximumDailyViews) | ||
65 | } | ||
66 | }] | ||
67 | }, | ||
68 | layout: { | ||
69 | padding: { | ||
70 | left: 15, | ||
71 | right: 15, | ||
72 | top: 10, | ||
73 | bottom: 0 | ||
74 | } | ||
75 | }, | ||
76 | elements: { | ||
77 | point: { | ||
78 | radius: 0 | ||
79 | } | ||
80 | }, | ||
81 | tooltips: { | ||
82 | mode: 'index', | ||
83 | intersect: false, | ||
84 | custom: function (tooltip: any) { | ||
85 | if (!tooltip) return | ||
86 | // disable displaying the color box | ||
87 | tooltip.displayColors = false | ||
88 | }, | ||
89 | callbacks: { | ||
90 | label: (tooltip: any, data: any) => `${tooltip.value} views` | ||
91 | } | ||
92 | }, | ||
93 | hover: { | ||
94 | mode: 'index', | ||
95 | intersect: false | ||
96 | } | ||
97 | } | ||
98 | } | ||
99 | |||
100 | resetSearch () { | ||
101 | this.channelsSearch = '' | ||
102 | this.onChannelsSearchChanged() | ||
103 | } | ||
104 | |||
105 | onChannelsSearchChanged () { | ||
106 | this.channelsSearchChanged.next() | ||
107 | } | ||
108 | |||
109 | async deleteVideoChannel (videoChannel: VideoChannel) { | ||
110 | const res = await this.confirmService.confirmWithInput( | ||
111 | $localize`Do you really want to delete ${videoChannel.displayName}? | ||
112 | It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another | ||
113 | channel with the same name (${videoChannel.name})!`, | ||
114 | |||
115 | $localize`Please type the display name of the video channel (${videoChannel.displayName}) to confirm`, | ||
116 | |||
117 | videoChannel.displayName, | ||
118 | |||
119 | $localize`Delete` | ||
120 | ) | ||
121 | if (res === false) return | ||
122 | |||
123 | this.videoChannelService.removeVideoChannel(videoChannel) | ||
124 | .subscribe( | ||
125 | () => { | ||
126 | this.loadVideoChannels() | ||
127 | this.notifier.success($localize`Video channel ${videoChannel.displayName} deleted.`) | ||
128 | }, | ||
129 | |||
130 | error => this.notifier.error(error.message) | ||
131 | ) | ||
132 | } | ||
133 | |||
134 | private loadVideoChannels () { | ||
135 | this.authService.userInformationLoaded | ||
136 | .pipe(mergeMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true, this.channelsSearch))) | ||
137 | .subscribe(res => { | ||
138 | this.videoChannels = res.data | ||
139 | this.totalItems = res.total | ||
140 | |||
141 | // chart data | ||
142 | this.videoChannelsChartData = this.videoChannels.map(v => ({ | ||
143 | labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()), | ||
144 | datasets: [ | ||
145 | { | ||
146 | label: $localize`Views for the day`, | ||
147 | data: v.viewsPerDay.map(day => day.views), | ||
148 | fill: false, | ||
149 | borderColor: '#c6c6c6' | ||
150 | } | ||
151 | ] | ||
152 | } as ChartData)) | ||
153 | |||
154 | // chart options that depend on chart data: | ||
155 | // we don't want to skew values and have min at 0, so we define what the floor/ceiling is here | ||
156 | this.videoChannelsMinimumDailyViews = min( | ||
157 | // compute local minimum daily views for each channel, by their "views" attribute | ||
158 | this.videoChannels.map(v => minBy( | ||
159 | v.viewsPerDay, | ||
160 | day => day.views | ||
161 | ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute | ||
162 | ) | ||
163 | this.videoChannelsMaximumDailyViews = max( | ||
164 | // compute local maximum daily views for each channel, by their "views" attribute | ||
165 | this.videoChannels.map(v => maxBy( | ||
166 | v.viewsPerDay, | ||
167 | day => day.views | ||
168 | ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute | ||
169 | ) | ||
170 | }) | ||
171 | } | ||
172 | } | ||
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.module.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.module.ts deleted file mode 100644 index f8c6ad56b..000000000 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.module.ts +++ /dev/null | |||
@@ -1,31 +0,0 @@ | |||
1 | import { ChartModule } from 'primeng/chart' | ||
2 | import { NgModule } from '@angular/core' | ||
3 | import { SharedFormModule } from '@app/shared/shared-forms' | ||
4 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | ||
5 | import { SharedMainModule } from '@app/shared/shared-main' | ||
6 | import { MyAccountVideoChannelCreateComponent } from './my-account-video-channel-create.component' | ||
7 | import { MyAccountVideoChannelUpdateComponent } from './my-account-video-channel-update.component' | ||
8 | import { MyAccountVideoChannelsRoutingModule } from './my-account-video-channels-routing.module' | ||
9 | import { MyAccountVideoChannelsComponent } from './my-account-video-channels.component' | ||
10 | |||
11 | @NgModule({ | ||
12 | imports: [ | ||
13 | MyAccountVideoChannelsRoutingModule, | ||
14 | |||
15 | ChartModule, | ||
16 | |||
17 | SharedMainModule, | ||
18 | SharedFormModule, | ||
19 | SharedGlobalIconModule | ||
20 | ], | ||
21 | |||
22 | declarations: [ | ||
23 | MyAccountVideoChannelsComponent, | ||
24 | MyAccountVideoChannelCreateComponent, | ||
25 | MyAccountVideoChannelUpdateComponent | ||
26 | ], | ||
27 | |||
28 | exports: [], | ||
29 | providers: [] | ||
30 | }) | ||
31 | export class MyAccountVideoChannelsModule { } | ||
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.html b/client/src/app/+my-account/my-account-history/my-account-history.component.html deleted file mode 100644 index cff46a41d..000000000 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.html +++ /dev/null | |||
@@ -1,28 +0,0 @@ | |||
1 | <h1> | ||
2 | <my-global-icon iconName="history" aria-hidden="true"></my-global-icon> | ||
3 | <ng-container i18n>My history</ng-container> | ||
4 | </h1> | ||
5 | |||
6 | <div class="top-buttons"> | ||
7 | <div class="history-switch"> | ||
8 | <p-inputSwitch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></p-inputSwitch> | ||
9 | <label i18n>Video history</label> | ||
10 | </div> | ||
11 | |||
12 | <button class="delete-history" (click)="deleteHistory()" i18n> | ||
13 | <my-global-icon iconName="delete" aria-hidden="true"></my-global-icon> | ||
14 | Delete history | ||
15 | </button> | ||
16 | </div> | ||
17 | |||
18 | |||
19 | <div class="no-history" i18n *ngIf="pagination.totalItems === 0">You don't have any video history yet.</div> | ||
20 | |||
21 | <div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" class="videos"> | ||
22 | <div class="video" *ngFor="let video of videos"> | ||
23 | <my-video-miniature | ||
24 | [video]="video" [displayAsRow]="true" | ||
25 | (videoRemoved)="removeVideoFromArray(video)" (videoBlocked)="removeVideoFromArray(video)" | ||
26 | ></my-video-miniature> | ||
27 | </div> | ||
28 | </div> | ||
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.scss b/client/src/app/+my-account/my-account-history/my-account-history.component.scss deleted file mode 100644 index 9eeeaf310..000000000 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.scss +++ /dev/null | |||
@@ -1,59 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .no-history { | ||
5 | display: flex; | ||
6 | justify-content: center; | ||
7 | margin-top: 50px; | ||
8 | font-weight: $font-semibold; | ||
9 | font-size: 16px; | ||
10 | } | ||
11 | |||
12 | .top-buttons { | ||
13 | margin-bottom: 20px; | ||
14 | display: flex; | ||
15 | align-items: center; | ||
16 | flex-wrap: wrap; | ||
17 | |||
18 | .history-switch { | ||
19 | display: flex; | ||
20 | flex-grow: 1; | ||
21 | |||
22 | label { | ||
23 | margin: 0 0 0 5px; | ||
24 | } | ||
25 | } | ||
26 | |||
27 | .delete-history { | ||
28 | @include peertube-button; | ||
29 | @include grey-button; | ||
30 | @include button-with-icon; | ||
31 | |||
32 | font-size: 15px; | ||
33 | } | ||
34 | } | ||
35 | |||
36 | .video { | ||
37 | @include row-blocks; | ||
38 | |||
39 | .my-video-miniature { | ||
40 | flex-grow: 1; | ||
41 | } | ||
42 | } | ||
43 | |||
44 | @media screen and (max-width: $mobile-view) { | ||
45 | .top-buttons { | ||
46 | .history-switch label, .delete-history { | ||
47 | @include ellipsis; | ||
48 | } | ||
49 | |||
50 | .history-switch label { | ||
51 | width: 60%; | ||
52 | } | ||
53 | |||
54 | .delete-history { | ||
55 | margin-left: auto; | ||
56 | max-width: 32%; | ||
57 | } | ||
58 | } | ||
59 | } | ||
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 deleted file mode 100644 index 3298c56c7..000000000 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.ts +++ /dev/null | |||
@@ -1,103 +0,0 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { | ||
4 | AuthService, | ||
5 | ComponentPagination, | ||
6 | ConfirmService, | ||
7 | LocalStorageService, | ||
8 | Notifier, | ||
9 | ScreenService, | ||
10 | ServerService, | ||
11 | UserService | ||
12 | } from '@app/core' | ||
13 | import { immutableAssign } from '@app/helpers' | ||
14 | import { UserHistoryService } from '@app/shared/shared-main' | ||
15 | import { AbstractVideoList } from '@app/shared/shared-video-miniature' | ||
16 | |||
17 | @Component({ | ||
18 | selector: 'my-account-history', | ||
19 | templateUrl: './my-account-history.component.html', | ||
20 | styleUrls: [ './my-account-history.component.scss' ] | ||
21 | }) | ||
22 | export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy { | ||
23 | titlePage: string | ||
24 | pagination: ComponentPagination = { | ||
25 | currentPage: 1, | ||
26 | itemsPerPage: 5, | ||
27 | totalItems: null | ||
28 | } | ||
29 | videosHistoryEnabled: boolean | ||
30 | |||
31 | constructor ( | ||
32 | protected router: Router, | ||
33 | protected serverService: ServerService, | ||
34 | protected route: ActivatedRoute, | ||
35 | protected authService: AuthService, | ||
36 | protected userService: UserService, | ||
37 | protected notifier: Notifier, | ||
38 | protected screenService: ScreenService, | ||
39 | protected storageService: LocalStorageService, | ||
40 | private confirmService: ConfirmService, | ||
41 | private userHistoryService: UserHistoryService | ||
42 | ) { | ||
43 | super() | ||
44 | |||
45 | this.titlePage = $localize`My videos history` | ||
46 | } | ||
47 | |||
48 | ngOnInit () { | ||
49 | super.ngOnInit() | ||
50 | |||
51 | this.videosHistoryEnabled = this.authService.getUser().videosHistoryEnabled | ||
52 | } | ||
53 | |||
54 | ngOnDestroy () { | ||
55 | super.ngOnDestroy() | ||
56 | } | ||
57 | |||
58 | getVideosObservable (page: number) { | ||
59 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) | ||
60 | |||
61 | return this.userHistoryService.getUserVideosHistory(newPagination) | ||
62 | } | ||
63 | |||
64 | generateSyndicationList () { | ||
65 | throw new Error('Method not implemented.') | ||
66 | } | ||
67 | |||
68 | onVideosHistoryChange () { | ||
69 | this.userService.updateMyProfile({ videosHistoryEnabled: this.videosHistoryEnabled }) | ||
70 | .subscribe( | ||
71 | () => { | ||
72 | const message = this.videosHistoryEnabled === true ? | ||
73 | $localize`Videos history is enabled` : | ||
74 | $localize`Videos history is disabled` | ||
75 | |||
76 | this.notifier.success(message) | ||
77 | |||
78 | this.authService.refreshUserInformation() | ||
79 | }, | ||
80 | |||
81 | err => this.notifier.error(err.message) | ||
82 | ) | ||
83 | } | ||
84 | |||
85 | async deleteHistory () { | ||
86 | const title = $localize`Delete videos history` | ||
87 | const message = $localize`Are you sure you want to delete all your videos history?` | ||
88 | |||
89 | const res = await this.confirmService.confirm(message, title) | ||
90 | if (res !== true) return | ||
91 | |||
92 | this.userHistoryService.deleteUserVideosHistory() | ||
93 | .subscribe( | ||
94 | () => { | ||
95 | this.notifier.success($localize`Videos history deleted`) | ||
96 | |||
97 | this.reloadVideos() | ||
98 | }, | ||
99 | |||
100 | err => this.notifier.error(err.message) | ||
101 | ) | ||
102 | } | ||
103 | } | ||
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html deleted file mode 100644 index def1cbab6..000000000 --- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html +++ /dev/null | |||
@@ -1,36 +0,0 @@ | |||
1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> | ||
2 | <div class="modal-header"> | ||
3 | <h1 i18n class="modal-title">Accept ownership</h1> | ||
4 | |||
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> | ||
6 | </div> | ||
7 | |||
8 | <div class="modal-body" [formGroup]="form"> | ||
9 | <div class="form-group"> | ||
10 | <label i18n for="channel">Select a channel to receive the video</label> | ||
11 | <div class="peertube-select-container"> | ||
12 | <select formControlName="channel" id="channel" class="form-control"> | ||
13 | <option i18n value="undefined" disabled>Channel that will receive the video</option> | ||
14 | <option *ngFor="let channel of videoChannels" [value]="channel.id">{{ channel.displayName }} | ||
15 | </option> | ||
16 | </select> | ||
17 | </div> | ||
18 | <div *ngIf="formErrors.channel" class="form-error">{{ formErrors.channel }}</div> | ||
19 | </div> | ||
20 | </div> | ||
21 | |||
22 | <div class="modal-footer inputs"> | ||
23 | <div class="inputs"> | ||
24 | <input | ||
25 | type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" | ||
26 | (click)="dismiss()" (key.enter)="dismiss()" | ||
27 | > | ||
28 | |||
29 | <input | ||
30 | type="submit" i18n-value value="Accept" class="action-button-submit" | ||
31 | [disabled]="!form.valid" | ||
32 | (click)="close()" | ||
33 | > | ||
34 | </div> | ||
35 | </div> | ||
36 | </ng-template> | ||
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.scss b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.scss deleted file mode 100644 index c7357f62d..000000000 --- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.scss +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | select { | ||
5 | display: block; | ||
6 | } | ||
7 | |||
8 | .peertube-select-container { | ||
9 | @include peertube-select-container(350px); | ||
10 | } | ||
11 | |||
12 | .form-group { | ||
13 | margin: 20px 0; | ||
14 | } \ No newline at end of file | ||
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts deleted file mode 100644 index 4c4436755..000000000 --- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts +++ /dev/null | |||
@@ -1,72 +0,0 @@ | |||
1 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { AuthService, Notifier } from '@app/core' | ||
3 | import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' | ||
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | ||
5 | import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main' | ||
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
7 | import { VideoChangeOwnership, VideoChannel } from '@shared/models' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-account-accept-ownership', | ||
11 | templateUrl: './my-account-accept-ownership.component.html', | ||
12 | styleUrls: [ './my-account-accept-ownership.component.scss' ] | ||
13 | }) | ||
14 | export class MyAccountAcceptOwnershipComponent extends FormReactive implements OnInit { | ||
15 | @Output() accepted = new EventEmitter<void>() | ||
16 | |||
17 | @ViewChild('modal', { static: true }) modal: ElementRef | ||
18 | |||
19 | videoChangeOwnership: VideoChangeOwnership | undefined = undefined | ||
20 | |||
21 | videoChannels: VideoChannel[] | ||
22 | |||
23 | error: string = null | ||
24 | |||
25 | constructor ( | ||
26 | protected formValidatorService: FormValidatorService, | ||
27 | private videoOwnershipService: VideoOwnershipService, | ||
28 | private notifier: Notifier, | ||
29 | private authService: AuthService, | ||
30 | private videoChannelService: VideoChannelService, | ||
31 | private modalService: NgbModal | ||
32 | ) { | ||
33 | super() | ||
34 | } | ||
35 | |||
36 | ngOnInit () { | ||
37 | this.videoChannels = [] | ||
38 | |||
39 | this.videoChannelService.listAccountVideoChannels(this.authService.getUser().account) | ||
40 | .subscribe(videoChannels => this.videoChannels = videoChannels.data) | ||
41 | |||
42 | this.buildForm({ | ||
43 | channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR | ||
44 | }) | ||
45 | } | ||
46 | |||
47 | show (videoChangeOwnership: VideoChangeOwnership) { | ||
48 | this.videoChangeOwnership = videoChangeOwnership | ||
49 | this.modalService | ||
50 | .open(this.modal, { centered: true }) | ||
51 | .result | ||
52 | .then(() => this.acceptOwnership()) | ||
53 | .catch(() => this.videoChangeOwnership = undefined) | ||
54 | } | ||
55 | |||
56 | acceptOwnership () { | ||
57 | const channel = this.form.value['channel'] | ||
58 | |||
59 | const videoChangeOwnership = this.videoChangeOwnership | ||
60 | this.videoOwnershipService | ||
61 | .acceptOwnership(videoChangeOwnership.id, { channelId: channel }) | ||
62 | .subscribe( | ||
63 | () => { | ||
64 | this.notifier.success($localize`Ownership accepted`) | ||
65 | if (this.accepted) this.accepted.emit() | ||
66 | this.videoChangeOwnership = undefined | ||
67 | }, | ||
68 | |||
69 | err => this.notifier.error(err.message) | ||
70 | ) | ||
71 | } | ||
72 | } | ||
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html deleted file mode 100644 index fd2163fb4..000000000 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html +++ /dev/null | |||
@@ -1,90 +0,0 @@ | |||
1 | <h1> | ||
2 | <my-global-icon iconName="download" aria-hidden="true"></my-global-icon> | ||
3 | <ng-container i18n>My ownership changes</ng-container> | ||
4 | </h1> | ||
5 | |||
6 | <p-table | ||
7 | [value]="videoChangeOwnerships" | ||
8 | [lazy]="true" | ||
9 | [paginator]="totalRecords > 0" | ||
10 | [totalRecords]="totalRecords" | ||
11 | [rows]="rowsPerPage" | ||
12 | [sortField]="sort.field" | ||
13 | [sortOrder]="sort.order" | ||
14 | (onLazyLoad)="loadLazy($event)" | ||
15 | > | ||
16 | <ng-template pTemplate="header"> | ||
17 | <tr> | ||
18 | <th style="width: 150px;" i18n>Actions</th> | ||
19 | <th style="width: 35%;" i18n>Initiator</th> | ||
20 | <th style="width: 65%;" i18n>Video</th> | ||
21 | <th style="width: 150px;" i18n pSortableColumn="createdAt"> | ||
22 | Created | ||
23 | <p-sortIcon field="createdAt"></p-sortIcon> | ||
24 | </th> | ||
25 | <th style="width: 100px;" i18n>Status</th> | ||
26 | </tr> | ||
27 | </ng-template> | ||
28 | |||
29 | <ng-template pTemplate="body" let-videoChangeOwnership> | ||
30 | <tr> | ||
31 | <td class="action-cell"> | ||
32 | <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'"> | ||
33 | <my-button i18n-title title="Accept" icon="tick" (click)="openAcceptModal(videoChangeOwnership)"></my-button> | ||
34 | <my-button i18n-title title="Refuse" icon="cross" (click)="refuse(videoChangeOwnership)"></my-button> | ||
35 | </ng-container> | ||
36 | </td> | ||
37 | <td> | ||
38 | <a [href]="videoChangeOwnership.initiatorAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | ||
39 | <div class="chip two-lines"> | ||
40 | <img | ||
41 | class="avatar" | ||
42 | [src]="videoChangeOwnership.initiatorAccount.avatar?.path" | ||
43 | (error)="switchToDefaultAvatar($event)" | ||
44 | alt="Avatar" | ||
45 | > | ||
46 | <div> | ||
47 | {{ videoChangeOwnership.initiatorAccount.displayName }} | ||
48 | <span class="text-muted">{{ videoChangeOwnership.initiatorAccount.nameWithHost }}</span> | ||
49 | </div> | ||
50 | </div> | ||
51 | </a> | ||
52 | </td> | ||
53 | |||
54 | <td> | ||
55 | <a [href]="videoChangeOwnership.video.url" class="video-table-video-link" [title]="videoChangeOwnership.video.name" target="_blank" rel="noopener noreferrer"> | ||
56 | <div class="video-table-video"> | ||
57 | <div class="video-table-video-image"> | ||
58 | <img [src]="videoChangeOwnership.video.thumbnailPath"> | ||
59 | </div> | ||
60 | <div class="video-table-video-text"> | ||
61 | <div> | ||
62 | {{ videoChangeOwnership.video.name }} | ||
63 | </div> | ||
64 | <div class="text-muted">by {{ videoChangeOwnership.video.channel?.displayName }} </div> | ||
65 | </div> | ||
66 | </div> | ||
67 | </a> | ||
68 | </td> | ||
69 | |||
70 | <td>{{ videoChangeOwnership.createdAt | date: 'short' }}</td> | ||
71 | |||
72 | <td> | ||
73 | <span class="badge" | ||
74 | [ngClass]="getStatusClass(videoChangeOwnership.status)">{{ videoChangeOwnership.status }}</span> | ||
75 | </td> | ||
76 | </tr> | ||
77 | </ng-template> | ||
78 | |||
79 | <ng-template pTemplate="emptymessage"> | ||
80 | <tr> | ||
81 | <td colspan="6"> | ||
82 | <div class="no-results"> | ||
83 | <ng-container i18n>No ownership change request found.</ng-container> | ||
84 | </div> | ||
85 | </td> | ||
86 | </tr> | ||
87 | </ng-template> | ||
88 | </p-table> | ||
89 | |||
90 | <my-account-accept-ownership #myAccountAcceptOwnershipComponent (accepted)="accepted()"></my-account-accept-ownership> | ||
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss deleted file mode 100644 index 7cac9c9f3..000000000 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss +++ /dev/null | |||
@@ -1,71 +0,0 @@ | |||
1 | @import 'miniature'; | ||
2 | @import 'mixins'; | ||
3 | |||
4 | .chip { | ||
5 | @include chip; | ||
6 | } | ||
7 | |||
8 | .badge { | ||
9 | @include table-badge; | ||
10 | } | ||
11 | |||
12 | .video-table-video { | ||
13 | display: inline-flex; | ||
14 | |||
15 | .video-table-video-image { | ||
16 | @include miniature-thumbnail; | ||
17 | |||
18 | $image-height: 45px; | ||
19 | |||
20 | height: $image-height; | ||
21 | width: #{(16/9) * $image-height}; | ||
22 | margin-right: 0.5rem; | ||
23 | border-radius: 2px; | ||
24 | border: none; | ||
25 | background: transparent; | ||
26 | display: inline-flex; | ||
27 | justify-content: center; | ||
28 | align-items: center; | ||
29 | position: relative; | ||
30 | |||
31 | img { | ||
32 | height: 100%; | ||
33 | width: 100%; | ||
34 | border-radius: 2px; | ||
35 | } | ||
36 | |||
37 | span { | ||
38 | color: pvar(--inputPlaceholderColor); | ||
39 | } | ||
40 | |||
41 | .video-table-video-image-label { | ||
42 | @include static-thumbnail-overlay; | ||
43 | position: absolute; | ||
44 | border-radius: 3px; | ||
45 | font-size: 10px; | ||
46 | padding: 0 3px; | ||
47 | line-height: 1.3; | ||
48 | bottom: 2px; | ||
49 | right: 2px; | ||
50 | } | ||
51 | } | ||
52 | |||
53 | .video-table-video-text { | ||
54 | display: inline-flex; | ||
55 | flex-direction: column; | ||
56 | justify-content: center; | ||
57 | font-size: 90%; | ||
58 | color: pvar(--mainForegroundColor); | ||
59 | line-height: 1rem; | ||
60 | |||
61 | div .glyphicon { | ||
62 | font-size: 80%; | ||
63 | color: gray; | ||
64 | margin-left: 0.1rem; | ||
65 | } | ||
66 | |||
67 | div + div { | ||
68 | font-size: 80%; | ||
69 | } | ||
70 | } | ||
71 | } | ||
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts deleted file mode 100644 index 7473470aa..000000000 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts +++ /dev/null | |||
@@ -1,83 +0,0 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | ||
3 | import { Notifier, RestPagination, RestTable } from '@app/core' | ||
4 | import { VideoOwnershipService, Actor, Video, Account } from '@app/shared/shared-main' | ||
5 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '@shared/models' | ||
6 | import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component' | ||
7 | import { getAbsoluteAPIUrl } from '@app/helpers' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-account-ownership', | ||
11 | templateUrl: './my-account-ownership.component.html', | ||
12 | styleUrls: [ './my-account-ownership.component.scss' ] | ||
13 | }) | ||
14 | export class MyAccountOwnershipComponent extends RestTable implements OnInit { | ||
15 | videoChangeOwnerships: VideoChangeOwnership[] = [] | ||
16 | totalRecords = 0 | ||
17 | sort: SortMeta = { field: 'createdAt', order: -1 } | ||
18 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
19 | |||
20 | @ViewChild('myAccountAcceptOwnershipComponent', { static: true }) myAccountAcceptOwnershipComponent: MyAccountAcceptOwnershipComponent | ||
21 | |||
22 | constructor ( | ||
23 | private notifier: Notifier, | ||
24 | private videoOwnershipService: VideoOwnershipService | ||
25 | ) { | ||
26 | super() | ||
27 | } | ||
28 | |||
29 | ngOnInit () { | ||
30 | this.initialize() | ||
31 | } | ||
32 | |||
33 | getIdentifier () { | ||
34 | return 'MyAccountOwnershipComponent' | ||
35 | } | ||
36 | |||
37 | getStatusClass (status: VideoChangeOwnershipStatus) { | ||
38 | switch (status) { | ||
39 | case VideoChangeOwnershipStatus.ACCEPTED: | ||
40 | return 'badge-green' | ||
41 | case VideoChangeOwnershipStatus.REFUSED: | ||
42 | return 'badge-red' | ||
43 | default: | ||
44 | return 'badge-yellow' | ||
45 | } | ||
46 | } | ||
47 | |||
48 | switchToDefaultAvatar ($event: Event) { | ||
49 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() | ||
50 | } | ||
51 | |||
52 | openAcceptModal (videoChangeOwnership: VideoChangeOwnership) { | ||
53 | this.myAccountAcceptOwnershipComponent.show(videoChangeOwnership) | ||
54 | } | ||
55 | |||
56 | accepted () { | ||
57 | this.loadData() | ||
58 | } | ||
59 | |||
60 | refuse (videoChangeOwnership: VideoChangeOwnership) { | ||
61 | this.videoOwnershipService.refuseOwnership(videoChangeOwnership.id) | ||
62 | .subscribe( | ||
63 | () => this.loadData(), | ||
64 | err => this.notifier.error(err.message) | ||
65 | ) | ||
66 | } | ||
67 | |||
68 | protected loadData () { | ||
69 | return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort) | ||
70 | .subscribe( | ||
71 | resultList => { | ||
72 | this.videoChangeOwnerships = resultList.data.map(change => ({ | ||
73 | ...change, | ||
74 | initiatorAccount: new Account(change.initiatorAccount), | ||
75 | nextOwnerAccount: new Account(change.nextOwnerAccount) | ||
76 | })) | ||
77 | this.totalRecords = resultList.total | ||
78 | }, | ||
79 | |||
80 | err => this.notifier.error(err.message) | ||
81 | ) | ||
82 | } | ||
83 | } | ||
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 0bcb38ef5..81380ec6e 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts | |||
@@ -2,21 +2,12 @@ import { NgModule } from '@angular/core' | |||
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | 3 | import { MetaGuard } from '@ngx-meta/core' |
4 | import { LoginGuard } from '../core' | 4 | import { LoginGuard } from '../core' |
5 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' | ||
5 | import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' | 6 | import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' |
6 | import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' | 7 | import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' |
7 | import { MyAccountHistoryComponent } from './my-account-history/my-account-history.component' | ||
8 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' | 8 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' |
9 | import { MyAccountOwnershipComponent } from './my-account-ownership/my-account-ownership.component' | ||
10 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' | 9 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' |
11 | import { MyAccountSubscriptionsComponent } from './my-account-subscriptions/my-account-subscriptions.component' | ||
12 | import { MyAccountVideoImportsComponent } from './my-account-video-imports/my-account-video-imports.component' | ||
13 | import { MyAccountVideoPlaylistCreateComponent } from './my-account-video-playlists/my-account-video-playlist-create.component' | ||
14 | import { MyAccountVideoPlaylistElementsComponent } from './my-account-video-playlists/my-account-video-playlist-elements.component' | ||
15 | import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playlists/my-account-video-playlist-update.component' | ||
16 | import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component' | ||
17 | import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component' | ||
18 | import { MyAccountComponent } from './my-account.component' | 10 | import { MyAccountComponent } from './my-account.component' |
19 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' | ||
20 | 11 | ||
21 | const myAccountRoutes: Routes = [ | 12 | const myAccountRoutes: Routes = [ |
22 | { | 13 | { |
@@ -41,88 +32,50 @@ const myAccountRoutes: Routes = [ | |||
41 | 32 | ||
42 | { | 33 | { |
43 | path: 'video-channels', | 34 | path: 'video-channels', |
44 | loadChildren: () => { | 35 | redirectTo: '/my-library/video-channels', |
45 | return import('./+my-account-video-channels/my-account-video-channels.module') | 36 | pathMatch: 'full' |
46 | .then(m => m.MyAccountVideoChannelsModule) | ||
47 | } | ||
48 | }, | 37 | }, |
49 | 38 | ||
50 | { | 39 | { |
51 | path: 'video-playlists', | 40 | path: 'video-playlists', |
52 | component: MyAccountVideoPlaylistsComponent, | 41 | redirectTo: '/my-library/video-playlists', |
53 | data: { | 42 | pathMatch: 'full' |
54 | meta: { | ||
55 | title: $localize`Account playlists` | ||
56 | } | ||
57 | } | ||
58 | }, | 43 | }, |
59 | { | 44 | { |
60 | path: 'video-playlists/create', | 45 | path: 'video-playlists/create', |
61 | component: MyAccountVideoPlaylistCreateComponent, | 46 | redirectTo: '/my-library/video-playlists/create', |
62 | data: { | 47 | pathMatch: 'full' |
63 | meta: { | ||
64 | title: $localize`Create new playlist` | ||
65 | } | ||
66 | } | ||
67 | }, | 48 | }, |
68 | { | 49 | { |
69 | path: 'video-playlists/:videoPlaylistId', | 50 | path: 'video-playlists/:videoPlaylistId', |
70 | component: MyAccountVideoPlaylistElementsComponent, | 51 | redirectTo: '/my-library/video-playlists/:videoPlaylistId', |
71 | data: { | 52 | pathMatch: 'full' |
72 | meta: { | ||
73 | title: $localize`Playlist elements` | ||
74 | } | ||
75 | } | ||
76 | }, | 53 | }, |
77 | { | 54 | { |
78 | path: 'video-playlists/update/:videoPlaylistId', | 55 | path: 'video-playlists/update/:videoPlaylistId', |
79 | component: MyAccountVideoPlaylistUpdateComponent, | 56 | redirectTo: '/my-library/video-playlists/update/:videoPlaylistId', |
80 | data: { | 57 | pathMatch: 'full' |
81 | meta: { | ||
82 | title: $localize`Update playlist` | ||
83 | } | ||
84 | } | ||
85 | }, | 58 | }, |
86 | 59 | ||
87 | { | 60 | { |
88 | path: 'videos', | 61 | path: 'videos', |
89 | component: MyAccountVideosComponent, | 62 | redirectTo: '/my-library/videos', |
90 | data: { | 63 | pathMatch: 'full' |
91 | meta: { | ||
92 | title: $localize`Account videos` | ||
93 | }, | ||
94 | reuse: { | ||
95 | enabled: true, | ||
96 | key: 'my-account-videos-list' | ||
97 | } | ||
98 | } | ||
99 | }, | 64 | }, |
100 | { | 65 | { |
101 | path: 'video-imports', | 66 | path: 'video-imports', |
102 | component: MyAccountVideoImportsComponent, | 67 | redirectTo: '/my-library/video-imports', |
103 | data: { | 68 | pathMatch: 'full' |
104 | meta: { | ||
105 | title: $localize`Account video imports` | ||
106 | } | ||
107 | } | ||
108 | }, | 69 | }, |
109 | { | 70 | { |
110 | path: 'subscriptions', | 71 | path: 'subscriptions', |
111 | component: MyAccountSubscriptionsComponent, | 72 | redirectTo: '/my-library/subscriptions', |
112 | data: { | 73 | pathMatch: 'full' |
113 | meta: { | ||
114 | title: $localize`Account subscriptions` | ||
115 | } | ||
116 | } | ||
117 | }, | 74 | }, |
118 | { | 75 | { |
119 | path: 'ownership', | 76 | path: 'ownership', |
120 | component: MyAccountOwnershipComponent, | 77 | redirectTo: '/my-library/ownership', |
121 | data: { | 78 | pathMatch: 'full' |
122 | meta: { | ||
123 | title: $localize`Ownership changes` | ||
124 | } | ||
125 | } | ||
126 | }, | 79 | }, |
127 | { | 80 | { |
128 | path: 'blocklist/accounts', | 81 | path: 'blocklist/accounts', |
@@ -144,16 +97,8 @@ const myAccountRoutes: Routes = [ | |||
144 | }, | 97 | }, |
145 | { | 98 | { |
146 | path: 'history/videos', | 99 | path: 'history/videos', |
147 | component: MyAccountHistoryComponent, | 100 | redirectTo: '/my-library/history/videos', |
148 | data: { | 101 | pathMatch: 'full' |
149 | meta: { | ||
150 | title: $localize`Videos history` | ||
151 | }, | ||
152 | reuse: { | ||
153 | enabled: true, | ||
154 | key: 'my-videos-history-list' | ||
155 | } | ||
156 | } | ||
157 | }, | 102 | }, |
158 | { | 103 | { |
159 | path: 'notifications', | 104 | path: 'notifications', |
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 deleted file mode 100644 index 6ab3826ba..000000000 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html +++ /dev/null | |||
@@ -1,42 +0,0 @@ | |||
1 | <h1> | ||
2 | <span> | ||
3 | <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon> | ||
4 | <ng-container i18n>My subscriptions</ng-container> | ||
5 | <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> | ||
6 | </span> | ||
7 | </h1> | ||
8 | |||
9 | <div class="video-subscriptions-header d-flex justify-content-between"> | ||
10 | <div class="has-feedback has-clear"> | ||
11 | <input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch" | ||
12 | (ngModelChange)="onSubscriptionsSearchChanged()" /> | ||
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 | </div> | ||
17 | |||
18 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscriptions yet.</div> | ||
19 | |||
20 | <div class="video-channels" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> | ||
21 | <div *ngFor="let videoChannel of videoChannels" class="video-channel"> | ||
22 | <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]"> | ||
23 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> | ||
24 | </a> | ||
25 | |||
26 | <div class="video-channel-info"> | ||
27 | <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page"> | ||
28 | <div class="video-channel-display-name">{{ videoChannel.displayName }}</div> | ||
29 | <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div> | ||
30 | </a> | ||
31 | |||
32 | <div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div> | ||
33 | |||
34 | <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner"> | ||
35 | <span i18n>Created by {{ videoChannel.ownerBy }}</span> | ||
36 | <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> | ||
37 | </a> | ||
38 | </div> | ||
39 | |||
40 | <my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button> | ||
41 | </div> | ||
42 | </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 deleted file mode 100644 index 5ead45dd8..000000000 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss +++ /dev/null | |||
@@ -1,81 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | input[type=text] { | ||
5 | @include peertube-input-text(300px); | ||
6 | } | ||
7 | |||
8 | .video-channel { | ||
9 | @include row-blocks; | ||
10 | |||
11 | img { | ||
12 | @include avatar(80px); | ||
13 | |||
14 | margin-right: 10px; | ||
15 | } | ||
16 | |||
17 | .video-channel-info { | ||
18 | flex-grow: 1; | ||
19 | |||
20 | a.video-channel-names { | ||
21 | @include disable-default-a-behaviour; | ||
22 | |||
23 | width: fit-content; | ||
24 | display: flex; | ||
25 | align-items: baseline; | ||
26 | color: pvar(--mainForegroundColor); | ||
27 | |||
28 | .video-channel-display-name { | ||
29 | font-weight: $font-semibold; | ||
30 | font-size: 18px; | ||
31 | } | ||
32 | |||
33 | .video-channel-name { | ||
34 | font-size: 14px; | ||
35 | color: $grey-actor-name; | ||
36 | margin-left: 5px; | ||
37 | } | ||
38 | } | ||
39 | } | ||
40 | |||
41 | .actor-owner { | ||
42 | @include actor-owner; | ||
43 | |||
44 | margin-top: 0; | ||
45 | } | ||
46 | } | ||
47 | |||
48 | .video-subscriptions-header { | ||
49 | margin-bottom: 30px; | ||
50 | } | ||
51 | |||
52 | @media screen and (max-width: $small-view) { | ||
53 | .video-channel { | ||
54 | .video-channel-info { | ||
55 | padding-bottom: 10px; | ||
56 | text-align: center; | ||
57 | |||
58 | .video-channel-names { | ||
59 | flex-direction: column; | ||
60 | align-items: center !important; | ||
61 | margin: auto; | ||
62 | } | ||
63 | } | ||
64 | |||
65 | img { | ||
66 | margin-right: 0; | ||
67 | } | ||
68 | } | ||
69 | } | ||
70 | |||
71 | @media screen and (max-width: $mobile-view) { | ||
72 | .video-subscriptions-header { | ||
73 | flex-direction: column; | ||
74 | |||
75 | input[type=text] { | ||
76 | width: 100% !important; | ||
77 | } | ||
78 | } | ||
79 | } | ||
80 | |||
81 | |||
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 deleted file mode 100644 index 994fe5142..000000000 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts +++ /dev/null | |||
@@ -1,75 +0,0 @@ | |||
1 | import { Subject } from 'rxjs' | ||
2 | import { Component, OnInit } from '@angular/core' | ||
3 | import { ComponentPagination, Notifier } from '@app/core' | ||
4 | import { VideoChannel } from '@app/shared/shared-main' | ||
5 | import { UserSubscriptionService } from '@app/shared/shared-user-subscription' | ||
6 | import { debounceTime } from 'rxjs/operators' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-account-subscriptions', | ||
10 | templateUrl: './my-account-subscriptions.component.html', | ||
11 | styleUrls: [ './my-account-subscriptions.component.scss' ] | ||
12 | }) | ||
13 | export class MyAccountSubscriptionsComponent implements OnInit { | ||
14 | videoChannels: VideoChannel[] = [] | ||
15 | |||
16 | pagination: ComponentPagination = { | ||
17 | currentPage: 1, | ||
18 | itemsPerPage: 10, | ||
19 | totalItems: null | ||
20 | } | ||
21 | |||
22 | onDataSubject = new Subject<any[]>() | ||
23 | |||
24 | subscriptionsSearch: string | ||
25 | subscriptionsSearchChanged = new Subject<string>() | ||
26 | |||
27 | constructor ( | ||
28 | private userSubscriptionService: UserSubscriptionService, | ||
29 | private notifier: Notifier | ||
30 | ) {} | ||
31 | |||
32 | ngOnInit () { | ||
33 | this.loadSubscriptions() | ||
34 | |||
35 | this.subscriptionsSearchChanged | ||
36 | .pipe(debounceTime(500)) | ||
37 | .subscribe(() => { | ||
38 | this.pagination.currentPage = 1 | ||
39 | this.loadSubscriptions(false) | ||
40 | }) | ||
41 | } | ||
42 | |||
43 | resetSearch () { | ||
44 | this.subscriptionsSearch = '' | ||
45 | this.onSubscriptionsSearchChanged() | ||
46 | } | ||
47 | |||
48 | onSubscriptionsSearchChanged () { | ||
49 | this.subscriptionsSearchChanged.next() | ||
50 | } | ||
51 | |||
52 | onNearOfBottom () { | ||
53 | // Last page | ||
54 | if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return | ||
55 | |||
56 | this.pagination.currentPage += 1 | ||
57 | this.loadSubscriptions() | ||
58 | } | ||
59 | |||
60 | private loadSubscriptions (more = true) { | ||
61 | this.userSubscriptionService.listSubscriptions({ pagination: this.pagination, search: this.subscriptionsSearch }) | ||
62 | .subscribe( | ||
63 | res => { | ||
64 | this.videoChannels = more | ||
65 | ? this.videoChannels.concat(res.data) | ||
66 | : res.data | ||
67 | this.pagination.totalItems = res.total | ||
68 | |||
69 | this.onDataSubject.next(res.data) | ||
70 | }, | ||
71 | |||
72 | error => this.notifier.error(error.message) | ||
73 | ) | ||
74 | } | ||
75 | } | ||
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html deleted file mode 100644 index 1d3a45f76..000000000 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html +++ /dev/null | |||
@@ -1,70 +0,0 @@ | |||
1 | <h1> | ||
2 | <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon> | ||
3 | <ng-container i18n>My imports</ng-container> | ||
4 | </h1> | ||
5 | |||
6 | <p-table | ||
7 | [value]="videoImports" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" | ||
8 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" | ||
9 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | ||
10 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} imports" | ||
11 | (onPage)="onPage($event)" [expandedRowKeys]="expandedRows" | ||
12 | > | ||
13 | <ng-template pTemplate="header"> | ||
14 | <tr> | ||
15 | <th style="width: 40px;"></th> | ||
16 | <th style="width: 70px">Action</th> | ||
17 | <th style="width: 45%" i18n>Target</th> | ||
18 | <th style="width: 55%" i18n>Video</th> | ||
19 | <th style="width: 150px" i18n>State</th> | ||
20 | <th style="width: 150px" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
21 | </tr> | ||
22 | </ng-template> | ||
23 | |||
24 | <ng-template pTemplate="body" let-expanded="expanded" let-videoImport> | ||
25 | <tr> | ||
26 | <td class="expand-cell"> | ||
27 | <span *ngIf="videoImport.error" class="expander" [pRowToggler]="videoImport" i18n-ngbTooltip ngbTooltip="See the error"> | ||
28 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> | ||
29 | </span> | ||
30 | </td> | ||
31 | |||
32 | <td class="action-cell"> | ||
33 | <my-edit-button *ngIf="isVideoImportSuccess(videoImport) && videoImport.video" | ||
34 | [routerLink]="getEditVideoUrl(videoImport.video)"></my-edit-button> | ||
35 | </td> | ||
36 | |||
37 | <td> | ||
38 | <a *ngIf="videoImport.targetUrl; else torrent" [href]="videoImport.targetUrl" target="_blank" rel="noopener noreferrer">{{ videoImport.targetUrl }}</a> | ||
39 | <ng-template #torrent> | ||
40 | <span [title]="videoImport.torrentName || videoImport.magnetUri">{{ videoImport.torrentName || videoImport.magnetUri }}</span> | ||
41 | </ng-template> | ||
42 | </td> | ||
43 | |||
44 | <td> | ||
45 | <ng-container *ngIf="isVideoImportPending(videoImport)">{{ videoImport.video?.name }}</ng-container> | ||
46 | <ng-container *ngIf="isVideoImportSuccess(videoImport) && videoImport.video"> | ||
47 | <a [href]="getVideoUrl(videoImport.video)" target="_blank" rel="noopener noreferrer">{{ videoImport.video?.name }}</a> | ||
48 | </ng-container> | ||
49 | <ng-container *ngIf="isVideoImportSuccess(videoImport) && !videoImport.video" i18n>This video was deleted</ng-container> | ||
50 | <ng-container *ngIf="isVideoImportFailed(videoImport)"></ng-container> | ||
51 | </td> | ||
52 | |||
53 | <td> | ||
54 | <span class="badge" [ngClass]="getVideoImportStateClass(videoImport.state)"> | ||
55 | {{ videoImport.state.label }} | ||
56 | </span> | ||
57 | </td> | ||
58 | |||
59 | <td>{{ videoImport.createdAt | date: 'short' }}</td> | ||
60 | </tr> | ||
61 | </ng-template> | ||
62 | |||
63 | <ng-template pTemplate="rowexpansion" let-videoImport> | ||
64 | <tr class="video-import-error" *ngIf="videoImport.error"> | ||
65 | <td colspan="6"> | ||
66 | <pre>{{ videoImport.error }}</pre> | ||
67 | </td> | ||
68 | </tr> | ||
69 | </ng-template> | ||
70 | </p-table> | ||
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss deleted file mode 100644 index a93c28028..000000000 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | pre { | ||
5 | font-size: 11px; | ||
6 | } | ||
7 | |||
8 | .video-import-error { | ||
9 | color: red; | ||
10 | } | ||
11 | |||
12 | .badge { | ||
13 | @include table-badge; | ||
14 | } | ||
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts deleted file mode 100644 index 9dd5ef142..000000000 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts +++ /dev/null | |||
@@ -1,77 +0,0 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { Component, OnInit } from '@angular/core' | ||
3 | import { Notifier, RestPagination, RestTable } from '@app/core' | ||
4 | import { VideoImportService } from '@app/shared/shared-main' | ||
5 | import { VideoImport, VideoImportState } from '@shared/models' | ||
6 | |||
7 | @Component({ | ||
8 | selector: 'my-account-video-imports', | ||
9 | templateUrl: './my-account-video-imports.component.html', | ||
10 | styleUrls: [ './my-account-video-imports.component.scss' ] | ||
11 | }) | ||
12 | export class MyAccountVideoImportsComponent extends RestTable implements OnInit { | ||
13 | videoImports: VideoImport[] = [] | ||
14 | totalRecords = 0 | ||
15 | sort: SortMeta = { field: 'createdAt', order: 1 } | ||
16 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
17 | |||
18 | constructor ( | ||
19 | private notifier: Notifier, | ||
20 | private videoImportService: VideoImportService | ||
21 | ) { | ||
22 | super() | ||
23 | } | ||
24 | |||
25 | ngOnInit () { | ||
26 | this.initialize() | ||
27 | } | ||
28 | |||
29 | getIdentifier () { | ||
30 | return 'MyAccountVideoImportsComponent' | ||
31 | } | ||
32 | |||
33 | getVideoImportStateClass (state: VideoImportState) { | ||
34 | switch (state) { | ||
35 | case VideoImportState.FAILED: | ||
36 | return 'badge-red' | ||
37 | case VideoImportState.REJECTED: | ||
38 | return 'badge-banned' | ||
39 | case VideoImportState.PENDING: | ||
40 | return 'badge-yellow' | ||
41 | default: | ||
42 | return 'badge-green' | ||
43 | } | ||
44 | } | ||
45 | |||
46 | isVideoImportSuccess (videoImport: VideoImport) { | ||
47 | return videoImport.state.id === VideoImportState.SUCCESS | ||
48 | } | ||
49 | |||
50 | isVideoImportPending (videoImport: VideoImport) { | ||
51 | return videoImport.state.id === VideoImportState.PENDING | ||
52 | } | ||
53 | |||
54 | isVideoImportFailed (videoImport: VideoImport) { | ||
55 | return videoImport.state.id === VideoImportState.FAILED | ||
56 | } | ||
57 | |||
58 | getVideoUrl (video: { uuid: string }) { | ||
59 | return '/videos/watch/' + video.uuid | ||
60 | } | ||
61 | |||
62 | getEditVideoUrl (video: { uuid: string }) { | ||
63 | return '/videos/update/' + video.uuid | ||
64 | } | ||
65 | |||
66 | protected loadData () { | ||
67 | this.videoImportService.getMyVideoImports(this.pagination, this.sort) | ||
68 | .subscribe( | ||
69 | resultList => { | ||
70 | this.videoImports = resultList.data | ||
71 | this.totalRecords = resultList.total | ||
72 | }, | ||
73 | |||
74 | err => this.notifier.error(err.message) | ||
75 | ) | ||
76 | } | ||
77 | } | ||
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 deleted file mode 100644 index 7a80aaa92..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts +++ /dev/null | |||
@@ -1,92 +0,0 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Router } from '@angular/router' | ||
3 | import { AuthService, Notifier, ServerService } from '@app/core' | ||
4 | import { populateAsyncUserVideoChannels } from '@app/helpers' | ||
5 | import { | ||
6 | setPlaylistChannelValidator, | ||
7 | VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, | ||
8 | VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR, | ||
9 | VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, | ||
10 | VIDEO_PLAYLIST_PRIVACY_VALIDATOR | ||
11 | } from '@app/shared/form-validators/video-playlist-validators' | ||
12 | import { FormValidatorService } from '@app/shared/shared-forms' | ||
13 | import { VideoPlaylistService } from '@app/shared/shared-video-playlist' | ||
14 | import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' | ||
15 | import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' | ||
16 | import { MyAccountVideoPlaylistEdit } from './my-account-video-playlist-edit' | ||
17 | |||
18 | @Component({ | ||
19 | selector: 'my-account-video-playlist-create', | ||
20 | templateUrl: './my-account-video-playlist-edit.component.html', | ||
21 | styleUrls: [ './my-account-video-playlist-edit.component.scss' ] | ||
22 | }) | ||
23 | export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylistEdit implements OnInit { | ||
24 | error: string | ||
25 | |||
26 | constructor ( | ||
27 | protected formValidatorService: FormValidatorService, | ||
28 | private authService: AuthService, | ||
29 | private notifier: Notifier, | ||
30 | private router: Router, | ||
31 | private videoPlaylistService: VideoPlaylistService, | ||
32 | private serverService: ServerService | ||
33 | ) { | ||
34 | super() | ||
35 | } | ||
36 | |||
37 | ngOnInit () { | ||
38 | this.buildForm({ | ||
39 | displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, | ||
40 | privacy: VIDEO_PLAYLIST_PRIVACY_VALIDATOR, | ||
41 | description: VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR, | ||
42 | videoChannelId: VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, | ||
43 | thumbnailfile: null | ||
44 | }) | ||
45 | |||
46 | this.form.get('privacy').valueChanges.subscribe(privacy => { | ||
47 | setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) | ||
48 | }) | ||
49 | |||
50 | populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) | ||
51 | .catch(err => console.error('Cannot populate user video channels.', err)) | ||
52 | |||
53 | this.serverService.getVideoPlaylistPrivacies() | ||
54 | .subscribe(videoPlaylistPrivacies => { | ||
55 | this.videoPlaylistPrivacies = videoPlaylistPrivacies | ||
56 | |||
57 | this.form.patchValue({ | ||
58 | privacy: VideoPlaylistPrivacy.PRIVATE | ||
59 | }) | ||
60 | }) | ||
61 | } | ||
62 | |||
63 | formValidated () { | ||
64 | this.error = undefined | ||
65 | |||
66 | const body = this.form.value | ||
67 | const videoPlaylistCreate: VideoPlaylistCreate = { | ||
68 | displayName: body.displayName, | ||
69 | privacy: body.privacy, | ||
70 | description: body.description || null, | ||
71 | videoChannelId: body.videoChannelId || null, | ||
72 | thumbnailfile: body.thumbnailfile || null | ||
73 | } | ||
74 | |||
75 | this.videoPlaylistService.createVideoPlaylist(videoPlaylistCreate).subscribe( | ||
76 | () => { | ||
77 | this.notifier.success($localize`Playlist ${videoPlaylistCreate.displayName} created.`) | ||
78 | this.router.navigate([ '/my-account', 'video-playlists' ]) | ||
79 | }, | ||
80 | |||
81 | err => this.error = err.message | ||
82 | ) | ||
83 | } | ||
84 | |||
85 | isCreation () { | ||
86 | return true | ||
87 | } | ||
88 | |||
89 | getFormButtonTitle () { | ||
90 | return $localize`Create` | ||
91 | } | ||
92 | } | ||
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 deleted file mode 100644 index 56060359a..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html +++ /dev/null | |||
@@ -1,100 +0,0 @@ | |||
1 | <nav aria-label="breadcrumb"> | ||
2 | <ol class="breadcrumb"> | ||
3 | <li class="breadcrumb-item"> | ||
4 | <a routerLink="/my-account/video-playlists" i18n>My Playlists</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="videoPlaylistToUpdate" [routerLink]="[ '/my-account/video-playlists/update', videoPlaylistToUpdate?.uuid ]">{{ videoPlaylistToUpdate?.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"> <!-- playlist grid --> | ||
24 | <div class="form-group col-12 col-lg-4 col-xl-3"> | ||
25 | <div *ngIf="isCreation()" class="video-playlist-title" i18n>NEW PLAYLIST</div> | ||
26 | <div *ngIf="!isCreation() && videoPlaylistToUpdate" class="video-playlist-title" i18n>PLAYLIST</div> | ||
27 | </div> | ||
28 | |||
29 | <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> | ||
30 | |||
31 | <div class="col-md-12 col-xl-6"> | ||
32 | <div class="form-group"> | ||
33 | <label i18n for="displayName">Display name</label> | ||
34 | <input | ||
35 | type="text" id="displayName" class="form-control" | ||
36 | formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }" | ||
37 | > | ||
38 | <div *ngIf="formErrors['displayName']" class="form-error"> | ||
39 | {{ formErrors['displayName'] }} | ||
40 | </div> | ||
41 | </div> | ||
42 | |||
43 | <div class="form-group"> | ||
44 | <label i18n for="description">Description</label> | ||
45 | <textarea | ||
46 | id="description" formControlName="description" | ||
47 | class="form-control" [ngClass]="{ 'input-error': formErrors['description'] }" | ||
48 | ></textarea> | ||
49 | <div *ngIf="formErrors.description" class="form-error"> | ||
50 | {{ formErrors.description }} | ||
51 | </div> | ||
52 | </div> | ||
53 | </div> | ||
54 | |||
55 | <div class="col-md-12 col-xl-6"> | ||
56 | <div class="form-group"> | ||
57 | <label i18n for="privacy">Privacy</label> | ||
58 | <div class="peertube-select-container"> | ||
59 | <select id="privacy" formControlName="privacy" class="form-control"> | ||
60 | <option *ngFor="let privacy of videoPlaylistPrivacies" [value]="privacy.id">{{ privacy.label }}</option> | ||
61 | </select> | ||
62 | </div> | ||
63 | |||
64 | <div *ngIf="formErrors.privacy" class="form-error"> | ||
65 | {{ formErrors.privacy }} | ||
66 | </div> | ||
67 | </div> | ||
68 | |||
69 | <div class="form-group"> | ||
70 | <label i18n>Channel</label> | ||
71 | |||
72 | <my-select-channel | ||
73 | labelForId="videoChannelIdl" [items]="userVideoChannels" formControlName="videoChannelId" | ||
74 | ></my-select-channel> | ||
75 | |||
76 | <div *ngIf="formErrors['videoChannelId']" class="form-error"> | ||
77 | {{ formErrors['videoChannelId'] }} | ||
78 | </div> | ||
79 | </div> | ||
80 | |||
81 | <div class="form-group"> | ||
82 | <label i18n>Playlist thumbnail</label> | ||
83 | |||
84 | <my-preview-upload | ||
85 | i18n-inputLabel inputLabel="Edit" inputName="thumbnailfile" formControlName="thumbnailfile" | ||
86 | previewWidth="223px" previewHeight="122px" | ||
87 | ></my-preview-upload> | ||
88 | </div> | ||
89 | </div> | ||
90 | |||
91 | <div class="form-row"> <!-- submit placement block --> | ||
92 | <div class="col-md-7 col-xl-5"></div> | ||
93 | <div class="col-md-5 col-xl-5 d-inline-flex"> | ||
94 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | ||
95 | </div> | ||
96 | </div> | ||
97 | </div> | ||
98 | </div> | ||
99 | |||
100 | </form> | ||
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.scss b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.scss deleted file mode 100644 index 08fab1101..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.scss +++ /dev/null | |||
@@ -1,36 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | label { | ||
5 | font-weight: $font-regular; | ||
6 | font-size: 100%; | ||
7 | } | ||
8 | |||
9 | .video-playlist-title { | ||
10 | @include settings-big-title; | ||
11 | } | ||
12 | |||
13 | input[type=text] { | ||
14 | @include peertube-input-text(340px); | ||
15 | |||
16 | display: block; | ||
17 | } | ||
18 | |||
19 | textarea { | ||
20 | @include peertube-textarea(500px, 150px); | ||
21 | |||
22 | display: block; | ||
23 | } | ||
24 | |||
25 | .peertube-select-container { | ||
26 | @include peertube-select-container(340px); | ||
27 | } | ||
28 | |||
29 | input[type=submit] { | ||
30 | @include peertube-button; | ||
31 | @include orange-button; | ||
32 | } | ||
33 | |||
34 | .breadcrumb { | ||
35 | @include breadcrumb; | ||
36 | } | ||
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 deleted file mode 100644 index 774d58c90..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts +++ /dev/null | |||
@@ -1,13 +0,0 @@ | |||
1 | import { FormReactive, SelectChannelItem } from '@app/shared/shared-forms' | ||
2 | import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models' | ||
3 | import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' | ||
4 | |||
5 | export abstract class MyAccountVideoPlaylistEdit extends FormReactive { | ||
6 | // Declare it here to avoid errors in create template | ||
7 | videoPlaylistToUpdate: VideoPlaylist | ||
8 | userVideoChannels: SelectChannelItem[] = [] | ||
9 | videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = [] | ||
10 | |||
11 | abstract isCreation (): boolean | ||
12 | abstract getFormButtonTitle (): string | ||
13 | } | ||
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html deleted file mode 100644 index 09b4c8a1b..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html +++ /dev/null | |||
@@ -1,51 +0,0 @@ | |||
1 | <div class="row"> | ||
2 | |||
3 | <div class="playlist-info col-xs-12 col-md-5 col-xl-3"> | ||
4 | <my-video-playlist-miniature | ||
5 | *ngIf="playlist" [playlist]="playlist" [toManage]="false" [displayChannel]="true" | ||
6 | [displayDescription]="true" [displayPrivacy]="true" | ||
7 | ></my-video-playlist-miniature> | ||
8 | |||
9 | <div class="playlist-buttons"> | ||
10 | <button (click)="showShareModal()" class="action-button share-button"> | ||
11 | <my-global-icon iconName="share" aria-hidden="true"></my-global-icon> | ||
12 | <span class="icon-text" i18n>Share</span> | ||
13 | </button> | ||
14 | |||
15 | <my-action-dropdown | ||
16 | *ngIf="isRegularPlaylist(playlist)" | ||
17 | [entry]="playlist" [actions]="playlistActions" label="More" | ||
18 | ></my-action-dropdown> | ||
19 | </div> | ||
20 | |||
21 | </div> | ||
22 | |||
23 | <div class="playlist-elements col-xs-12 col-md-7 col-xl-9"> | ||
24 | <div class="no-results" *ngIf="pagination.totalItems === 0"> | ||
25 | <div i18n>No videos in this playlist.</div> | ||
26 | |||
27 | <div i18n> | ||
28 | Browse videos on PeerTube to add them in your playlist. | ||
29 | </div> | ||
30 | |||
31 | <div i18n> | ||
32 | See the <a target="_blank" href="https://docs.joinpeertube.org/#/use-library?id=playlist">documentation</a> for more information. | ||
33 | </div> | ||
34 | </div> | ||
35 | |||
36 | <div | ||
37 | class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" | ||
38 | cdkDropList (cdkDropListDropped)="drop($event)" [dataObservable]="onDataSubject.asObservable()" | ||
39 | > | ||
40 | <div class="video" *ngFor="let playlistElement of playlistElements; trackBy: trackByFn" cdkDrag [cdkDragStartDelay]="getDragStartDelay()"> | ||
41 | <my-video-playlist-element-miniature | ||
42 | [playlistElement]="playlistElement" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)" | ||
43 | [position]="playlistElement.position" | ||
44 | > | ||
45 | </my-video-playlist-element-miniature> | ||
46 | </div> | ||
47 | </div> | ||
48 | </div> | ||
49 | </div> | ||
50 | |||
51 | <my-video-share #videoShareModal [playlist]="playlist"></my-video-share> | ||
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss deleted file mode 100644 index de7e1993f..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss +++ /dev/null | |||
@@ -1,83 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | @import '_miniature'; | ||
4 | |||
5 | .playlist-info { | ||
6 | background-color: pvar(--submenuColor); | ||
7 | margin-left: -$not-expanded-horizontal-margins; | ||
8 | margin-top: -$sub-menu-margin-bottom; | ||
9 | |||
10 | padding: 10px; | ||
11 | |||
12 | display: flex; | ||
13 | flex-direction: column; | ||
14 | justify-content: flex-start; | ||
15 | align-items: center; | ||
16 | |||
17 | /* fix ellipsis dots background color */ | ||
18 | ::ng-deep .miniature-name::after { | ||
19 | background-color: pvar(--submenuColor) !important; | ||
20 | } | ||
21 | } | ||
22 | |||
23 | .playlist-buttons { | ||
24 | display:flex; | ||
25 | margin: 30px 0 10px 0; | ||
26 | |||
27 | .share-button { | ||
28 | @include peertube-button; | ||
29 | @include button-with-icon(17px, 3px, -1px); | ||
30 | @include grey-button; | ||
31 | @include apply-svg-color(pvar(--actionButtonColor)); | ||
32 | |||
33 | margin-right: 10px; | ||
34 | } | ||
35 | } | ||
36 | |||
37 | // Thanks Angular CDK <3 https://material.angular.io/cdk/drag-drop/examples | ||
38 | .cdk-drag-preview { | ||
39 | box-sizing: border-box; | ||
40 | border-radius: 4px; | ||
41 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), | ||
42 | 0 8px 10px 1px rgba(0, 0, 0, 0.14), | ||
43 | 0 3px 14px 2px rgba(0, 0, 0, 0.12); | ||
44 | } | ||
45 | |||
46 | .cdk-drag-placeholder { | ||
47 | opacity: 0; | ||
48 | } | ||
49 | |||
50 | .cdk-drag-animating { | ||
51 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); | ||
52 | } | ||
53 | |||
54 | .video:last-child { | ||
55 | border: none; | ||
56 | } | ||
57 | |||
58 | .videos.cdk-drop-list-dragging .video:not(.cdk-drag-placeholder) { | ||
59 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); | ||
60 | } | ||
61 | |||
62 | @media screen and (max-width: $small-view) { | ||
63 | .playlist-info { | ||
64 | width: 100vw; | ||
65 | padding-top: 20px; | ||
66 | margin-left: calc(#{var(--expanded-horizontal-margin-content)} * -1); | ||
67 | } | ||
68 | |||
69 | .playlist-elements { | ||
70 | padding: 0 !important; | ||
71 | } | ||
72 | |||
73 | ::ng-deep my-video-playlist-element-miniature { | ||
74 | |||
75 | .video { | ||
76 | padding: 5px !important; | ||
77 | } | ||
78 | |||
79 | .position { | ||
80 | margin-right: 5px !important; | ||
81 | } | ||
82 | } | ||
83 | } | ||
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts deleted file mode 100644 index f6cdf1067..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts +++ /dev/null | |||
@@ -1,198 +0,0 @@ | |||
1 | import { Subject, Subscription } from 'rxjs' | ||
2 | import { CdkDragDrop } from '@angular/cdk/drag-drop' | ||
3 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' | ||
4 | import { ActivatedRoute, Router } from '@angular/router' | ||
5 | import { ComponentPagination, ConfirmService, Notifier, ScreenService } from '@app/core' | ||
6 | import { DropdownAction } from '@app/shared/shared-main' | ||
7 | import { VideoShareComponent } from '@app/shared/shared-share-modal' | ||
8 | import { VideoPlaylist, VideoPlaylistElement, VideoPlaylistService } from '@app/shared/shared-video-playlist' | ||
9 | import { VideoPlaylistType } from '@shared/models' | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-account-video-playlist-elements', | ||
13 | templateUrl: './my-account-video-playlist-elements.component.html', | ||
14 | styleUrls: [ './my-account-video-playlist-elements.component.scss' ] | ||
15 | }) | ||
16 | export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy { | ||
17 | @ViewChild('videoShareModal') videoShareModal: VideoShareComponent | ||
18 | |||
19 | playlistElements: VideoPlaylistElement[] = [] | ||
20 | playlist: VideoPlaylist | ||
21 | |||
22 | playlistActions: DropdownAction<VideoPlaylist>[][] = [] | ||
23 | |||
24 | pagination: ComponentPagination = { | ||
25 | currentPage: 1, | ||
26 | itemsPerPage: 10, | ||
27 | totalItems: null | ||
28 | } | ||
29 | |||
30 | onDataSubject = new Subject<any[]>() | ||
31 | |||
32 | private videoPlaylistId: string | number | ||
33 | private paramsSub: Subscription | ||
34 | |||
35 | constructor ( | ||
36 | private notifier: Notifier, | ||
37 | private router: Router, | ||
38 | private confirmService: ConfirmService, | ||
39 | private route: ActivatedRoute, | ||
40 | private screenService: ScreenService, | ||
41 | private videoPlaylistService: VideoPlaylistService | ||
42 | ) {} | ||
43 | |||
44 | ngOnInit () { | ||
45 | this.playlistActions = [ | ||
46 | [ | ||
47 | { | ||
48 | label: $localize`Update playlist`, | ||
49 | iconName: 'edit', | ||
50 | linkBuilder: playlist => [ '/my-account', 'video-playlists', 'update', playlist.uuid ] | ||
51 | }, | ||
52 | { | ||
53 | label: $localize`Delete playlist`, | ||
54 | iconName: 'delete', | ||
55 | handler: playlist => this.deleteVideoPlaylist(playlist) | ||
56 | } | ||
57 | ] | ||
58 | ] | ||
59 | |||
60 | this.paramsSub = this.route.params.subscribe(routeParams => { | ||
61 | this.videoPlaylistId = routeParams[ 'videoPlaylistId' ] | ||
62 | this.loadElements() | ||
63 | |||
64 | this.loadPlaylistInfo() | ||
65 | }) | ||
66 | } | ||
67 | |||
68 | ngOnDestroy () { | ||
69 | if (this.paramsSub) this.paramsSub.unsubscribe() | ||
70 | } | ||
71 | |||
72 | drop (event: CdkDragDrop<any>) { | ||
73 | const previousIndex = event.previousIndex | ||
74 | const newIndex = event.currentIndex | ||
75 | |||
76 | if (previousIndex === newIndex) return | ||
77 | |||
78 | const oldPosition = this.playlistElements[previousIndex].position | ||
79 | let insertAfter = this.playlistElements[newIndex].position | ||
80 | |||
81 | if (oldPosition > insertAfter) insertAfter-- | ||
82 | |||
83 | const element = this.playlistElements[previousIndex] | ||
84 | |||
85 | this.playlistElements.splice(previousIndex, 1) | ||
86 | this.playlistElements.splice(newIndex, 0, element) | ||
87 | |||
88 | this.videoPlaylistService.reorderPlaylist(this.playlist.id, oldPosition, insertAfter) | ||
89 | .subscribe( | ||
90 | () => { | ||
91 | this.reorderClientPositions() | ||
92 | }, | ||
93 | |||
94 | err => this.notifier.error(err.message) | ||
95 | ) | ||
96 | } | ||
97 | |||
98 | onElementRemoved (element: VideoPlaylistElement) { | ||
99 | const oldFirst = this.findFirst() | ||
100 | |||
101 | this.playlistElements = this.playlistElements.filter(v => v.id !== element.id) | ||
102 | this.reorderClientPositions(oldFirst) | ||
103 | } | ||
104 | |||
105 | onNearOfBottom () { | ||
106 | // Last page | ||
107 | if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return | ||
108 | |||
109 | this.pagination.currentPage += 1 | ||
110 | this.loadElements() | ||
111 | } | ||
112 | |||
113 | trackByFn (index: number, elem: VideoPlaylistElement) { | ||
114 | return elem.id | ||
115 | } | ||
116 | |||
117 | isRegularPlaylist (playlist: VideoPlaylist) { | ||
118 | return playlist?.type.id === VideoPlaylistType.REGULAR | ||
119 | } | ||
120 | |||
121 | showShareModal () { | ||
122 | this.videoShareModal.show() | ||
123 | } | ||
124 | |||
125 | async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) { | ||
126 | const res = await this.confirmService.confirm( | ||
127 | $localize`Do you really want to delete ${videoPlaylist.displayName}?`, | ||
128 | $localize`Delete` | ||
129 | ) | ||
130 | if (res === false) return | ||
131 | |||
132 | this.videoPlaylistService.removeVideoPlaylist(videoPlaylist) | ||
133 | .subscribe( | ||
134 | () => { | ||
135 | this.router.navigate([ '/my-account', 'video-playlists' ]) | ||
136 | this.notifier.success($localize`Playlist ${videoPlaylist.displayName} deleted.`) | ||
137 | }, | ||
138 | |||
139 | error => this.notifier.error(error.message) | ||
140 | ) | ||
141 | } | ||
142 | |||
143 | /** | ||
144 | * Returns null to not have drag and drop delay. | ||
145 | * In small views, where elements are about 100% wide, | ||
146 | * we add a delay to prevent unwanted drag&drop. | ||
147 | * | ||
148 | * @see {@link https://github.com/Chocobozzz/PeerTube/issues/2078} | ||
149 | * | ||
150 | * @returns {null|number} Null for no delay, or a number in milliseconds. | ||
151 | */ | ||
152 | getDragStartDelay (): null | number { | ||
153 | if (this.screenService.isInTouchScreen()) { | ||
154 | return 500 | ||
155 | } | ||
156 | |||
157 | return null | ||
158 | } | ||
159 | |||
160 | private loadElements () { | ||
161 | this.videoPlaylistService.getPlaylistVideos(this.videoPlaylistId, this.pagination) | ||
162 | .subscribe(({ total, data }) => { | ||
163 | this.playlistElements = this.playlistElements.concat(data) | ||
164 | this.pagination.totalItems = total | ||
165 | |||
166 | this.onDataSubject.next(data) | ||
167 | }) | ||
168 | } | ||
169 | |||
170 | private loadPlaylistInfo () { | ||
171 | this.videoPlaylistService.getVideoPlaylist(this.videoPlaylistId) | ||
172 | .subscribe(playlist => { | ||
173 | this.playlist = playlist | ||
174 | }) | ||
175 | } | ||
176 | |||
177 | private reorderClientPositions (first?: VideoPlaylistElement) { | ||
178 | if (this.playlistElements.length === 0) return | ||
179 | |||
180 | const oldFirst = first || this.findFirst() | ||
181 | let i = 1 | ||
182 | |||
183 | for (const element of this.playlistElements) { | ||
184 | element.position = i | ||
185 | i++ | ||
186 | } | ||
187 | |||
188 | // Reload playlist thumbnail if the first element changed | ||
189 | const newFirst = this.findFirst() | ||
190 | if (oldFirst && newFirst && oldFirst.id !== newFirst.id) { | ||
191 | this.playlist.refreshThumbnail() | ||
192 | } | ||
193 | } | ||
194 | |||
195 | private findFirst () { | ||
196 | return this.playlistElements.find(e => e.position === 1) | ||
197 | } | ||
198 | } | ||
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 deleted file mode 100644 index fefc6d607..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts +++ /dev/null | |||
@@ -1,130 +0,0 @@ | |||
1 | import { forkJoin, Subscription } from 'rxjs' | ||
2 | import { map, switchMap } from 'rxjs/operators' | ||
3 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
4 | import { ActivatedRoute, Router } from '@angular/router' | ||
5 | import { AuthService, Notifier, ServerService } from '@app/core' | ||
6 | import { populateAsyncUserVideoChannels } from '@app/helpers' | ||
7 | import { | ||
8 | setPlaylistChannelValidator, | ||
9 | VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, | ||
10 | VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR, | ||
11 | VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, | ||
12 | VIDEO_PLAYLIST_PRIVACY_VALIDATOR | ||
13 | } from '@app/shared/form-validators/video-playlist-validators' | ||
14 | import { FormValidatorService } from '@app/shared/shared-forms' | ||
15 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | ||
16 | import { VideoPlaylistUpdate } from '@shared/models' | ||
17 | import { MyAccountVideoPlaylistEdit } from './my-account-video-playlist-edit' | ||
18 | |||
19 | @Component({ | ||
20 | selector: 'my-account-video-playlist-update', | ||
21 | templateUrl: './my-account-video-playlist-edit.component.html', | ||
22 | styleUrls: [ './my-account-video-playlist-edit.component.scss' ] | ||
23 | }) | ||
24 | export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylistEdit implements OnInit, OnDestroy { | ||
25 | error: string | ||
26 | videoPlaylistToUpdate: VideoPlaylist | ||
27 | |||
28 | private paramsSub: Subscription | ||
29 | |||
30 | constructor ( | ||
31 | protected formValidatorService: FormValidatorService, | ||
32 | private authService: AuthService, | ||
33 | private notifier: Notifier, | ||
34 | private router: Router, | ||
35 | private route: ActivatedRoute, | ||
36 | private videoPlaylistService: VideoPlaylistService, | ||
37 | private serverService: ServerService | ||
38 | ) { | ||
39 | super() | ||
40 | } | ||
41 | |||
42 | ngOnInit () { | ||
43 | this.buildForm({ | ||
44 | displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, | ||
45 | privacy: VIDEO_PLAYLIST_PRIVACY_VALIDATOR, | ||
46 | description: VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR, | ||
47 | videoChannelId: VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, | ||
48 | thumbnailfile: null | ||
49 | }) | ||
50 | |||
51 | this.form.get('privacy').valueChanges.subscribe(privacy => { | ||
52 | setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) | ||
53 | }) | ||
54 | |||
55 | populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) | ||
56 | .catch(err => console.error('Cannot populate user video channels.', err)) | ||
57 | |||
58 | this.paramsSub = this.route.params | ||
59 | .pipe( | ||
60 | map(routeParams => routeParams['videoPlaylistId']), | ||
61 | switchMap(videoPlaylistId => { | ||
62 | return forkJoin([ | ||
63 | this.videoPlaylistService.getVideoPlaylist(videoPlaylistId), | ||
64 | this.serverService.getVideoPlaylistPrivacies() | ||
65 | ]) | ||
66 | }) | ||
67 | ) | ||
68 | .subscribe( | ||
69 | ([ videoPlaylistToUpdate, videoPlaylistPrivacies]) => { | ||
70 | this.videoPlaylistToUpdate = videoPlaylistToUpdate | ||
71 | this.videoPlaylistPrivacies = videoPlaylistPrivacies | ||
72 | |||
73 | this.hydrateFormFromPlaylist() | ||
74 | }, | ||
75 | |||
76 | err => this.error = err.message | ||
77 | ) | ||
78 | } | ||
79 | |||
80 | ngOnDestroy () { | ||
81 | if (this.paramsSub) this.paramsSub.unsubscribe() | ||
82 | } | ||
83 | |||
84 | formValidated () { | ||
85 | this.error = undefined | ||
86 | |||
87 | const body = this.form.value | ||
88 | const videoPlaylistUpdate: VideoPlaylistUpdate = { | ||
89 | displayName: body.displayName, | ||
90 | privacy: body.privacy, | ||
91 | description: body.description || null, | ||
92 | videoChannelId: body.videoChannelId || null, | ||
93 | thumbnailfile: body.thumbnailfile || undefined | ||
94 | } | ||
95 | |||
96 | this.videoPlaylistService.updateVideoPlaylist(this.videoPlaylistToUpdate, videoPlaylistUpdate).subscribe( | ||
97 | () => { | ||
98 | this.notifier.success($localize`Playlist ${videoPlaylistUpdate.displayName} updated.`) | ||
99 | this.router.navigate([ '/my-account', 'video-playlists' ]) | ||
100 | }, | ||
101 | |||
102 | err => this.error = err.message | ||
103 | ) | ||
104 | } | ||
105 | |||
106 | isCreation () { | ||
107 | return false | ||
108 | } | ||
109 | |||
110 | getFormButtonTitle () { | ||
111 | return $localize`Update` | ||
112 | } | ||
113 | |||
114 | private hydrateFormFromPlaylist () { | ||
115 | this.form.patchValue({ | ||
116 | displayName: this.videoPlaylistToUpdate.displayName, | ||
117 | privacy: this.videoPlaylistToUpdate.privacy.id, | ||
118 | description: this.videoPlaylistToUpdate.description, | ||
119 | videoChannelId: this.videoPlaylistToUpdate.videoChannel ? this.videoPlaylistToUpdate.videoChannel.id : null | ||
120 | }) | ||
121 | |||
122 | fetch(this.videoPlaylistToUpdate.thumbnailUrl) | ||
123 | .then(response => response.blob()) | ||
124 | .then(data => { | ||
125 | this.form.patchValue({ | ||
126 | thumbnailfile: data | ||
127 | }) | ||
128 | }) | ||
129 | } | ||
130 | } | ||
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html deleted file mode 100644 index afcf6a084..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html +++ /dev/null | |||
@@ -1,35 +0,0 @@ | |||
1 | <h1> | ||
2 | <span> | ||
3 | <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> | ||
4 | <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> | ||
5 | </span> | ||
6 | </h1> | ||
7 | |||
8 | <div class="video-playlists-header d-flex justify-content-between"> | ||
9 | <div class="has-feedback has-clear"> | ||
10 | <input type="text" placeholder="Search your playlists" i18n-placeholder [(ngModel)]="videoPlaylistsSearch" | ||
11 | (ngModelChange)="onVideoPlaylistSearchChanged()" /> | ||
12 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> | ||
13 | <span class="sr-only" i18n>Clear filters</span> | ||
14 | </div> | ||
15 | |||
16 | <a class="create-button" routerLink="create"> | ||
17 | <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> | ||
18 | <ng-container i18n>Create playlist</ng-container> | ||
19 | </a> | ||
20 | </div> | ||
21 | |||
22 | <div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> | ||
23 | <div *ngFor="let playlist of videoPlaylists" class="video-playlist"> | ||
24 | <div class="miniature-wrapper"> | ||
25 | <my-video-playlist-miniature [playlist]="playlist" [toManage]="true" [displayChannel]="true" [displayDescription]="true" [displayPrivacy]="true" | ||
26 | ></my-video-playlist-miniature> | ||
27 | </div> | ||
28 | |||
29 | <div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons"> | ||
30 | <my-delete-button label (click)="deleteVideoPlaylist(playlist)"></my-delete-button> | ||
31 | |||
32 | <my-edit-button label [routerLink]="[ 'update', playlist.uuid ]"></my-edit-button> | ||
33 | </div> | ||
34 | </div> | ||
35 | </div> | ||
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss deleted file mode 100644 index 2b7c88246..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss +++ /dev/null | |||
@@ -1,78 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .create-button { | ||
5 | @include create-button; | ||
6 | } | ||
7 | |||
8 | input[type=text] { | ||
9 | @include peertube-input-text(300px); | ||
10 | } | ||
11 | |||
12 | ::ng-deep .action-button { | ||
13 | &.action-button-delete { | ||
14 | margin-right: 10px; | ||
15 | } | ||
16 | } | ||
17 | |||
18 | .video-playlist { | ||
19 | @include row-blocks; | ||
20 | |||
21 | .miniature-wrapper { | ||
22 | flex-grow: 1; | ||
23 | |||
24 | ::ng-deep .miniature { | ||
25 | display: flex; | ||
26 | |||
27 | .miniature-info { | ||
28 | margin-left: 10px; | ||
29 | width: auto; | ||
30 | } | ||
31 | } | ||
32 | } | ||
33 | |||
34 | .video-playlist-buttons { | ||
35 | min-width: 190px; | ||
36 | height: max-content; | ||
37 | } | ||
38 | } | ||
39 | |||
40 | .video-playlists-header { | ||
41 | margin-bottom: 30px; | ||
42 | } | ||
43 | |||
44 | @media screen and (max-width: $small-view) { | ||
45 | .video-playlists-header { | ||
46 | text-align: center; | ||
47 | } | ||
48 | |||
49 | .video-playlist { | ||
50 | |||
51 | .video-playlist-buttons { | ||
52 | margin-top: 10px; | ||
53 | } | ||
54 | } | ||
55 | |||
56 | my-video-playlist-miniature ::ng-deep .miniature { | ||
57 | flex-direction: column; | ||
58 | |||
59 | .miniature-info { | ||
60 | margin-left: 0 !important; | ||
61 | } | ||
62 | |||
63 | .miniature-name { | ||
64 | max-width: $video-thumbnail-width; | ||
65 | } | ||
66 | } | ||
67 | } | ||
68 | |||
69 | @media screen and (max-width: $mobile-view) { | ||
70 | .video-playlists-header { | ||
71 | flex-direction: column; | ||
72 | |||
73 | input[type=text] { | ||
74 | width: 100% !important; | ||
75 | margin-bottom: 12px; | ||
76 | } | ||
77 | } | ||
78 | } | ||
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts deleted file mode 100644 index 1e569c0b6..000000000 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts +++ /dev/null | |||
@@ -1,102 +0,0 @@ | |||
1 | import { Subject } from 'rxjs' | ||
2 | import { debounceTime, mergeMap } from 'rxjs/operators' | ||
3 | import { Component, OnInit } from '@angular/core' | ||
4 | import { AuthService, ComponentPagination, ConfirmService, Notifier, User } from '@app/core' | ||
5 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | ||
6 | import { VideoPlaylistType } from '@shared/models' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-account-video-playlists', | ||
10 | templateUrl: './my-account-video-playlists.component.html', | ||
11 | styleUrls: [ './my-account-video-playlists.component.scss' ] | ||
12 | }) | ||
13 | export class MyAccountVideoPlaylistsComponent implements OnInit { | ||
14 | videoPlaylistsSearch: string | ||
15 | videoPlaylists: VideoPlaylist[] = [] | ||
16 | videoPlaylistSearchChanged = new Subject<string>() | ||
17 | |||
18 | pagination: ComponentPagination = { | ||
19 | currentPage: 1, | ||
20 | itemsPerPage: 5, | ||
21 | totalItems: null | ||
22 | } | ||
23 | |||
24 | onDataSubject = new Subject<any[]>() | ||
25 | |||
26 | private user: User | ||
27 | |||
28 | constructor ( | ||
29 | private authService: AuthService, | ||
30 | private notifier: Notifier, | ||
31 | private confirmService: ConfirmService, | ||
32 | private videoPlaylistService: VideoPlaylistService | ||
33 | ) {} | ||
34 | |||
35 | ngOnInit () { | ||
36 | this.user = this.authService.getUser() | ||
37 | |||
38 | this.loadVideoPlaylists() | ||
39 | |||
40 | this.videoPlaylistSearchChanged | ||
41 | .pipe( | ||
42 | debounceTime(500)) | ||
43 | .subscribe(() => { | ||
44 | this.loadVideoPlaylists(true) | ||
45 | }) | ||
46 | } | ||
47 | |||
48 | async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) { | ||
49 | const res = await this.confirmService.confirm( | ||
50 | $localize`Do you really want to delete ${videoPlaylist.displayName}?`, | ||
51 | $localize`Delete` | ||
52 | ) | ||
53 | if (res === false) return | ||
54 | |||
55 | this.videoPlaylistService.removeVideoPlaylist(videoPlaylist) | ||
56 | .subscribe( | ||
57 | () => { | ||
58 | this.videoPlaylists = this.videoPlaylists | ||
59 | .filter(p => p.id !== videoPlaylist.id) | ||
60 | |||
61 | this.notifier.success($localize`Playlist ${videoPlaylist.displayName}} deleted.`) | ||
62 | }, | ||
63 | |||
64 | error => this.notifier.error(error.message) | ||
65 | ) | ||
66 | } | ||
67 | |||
68 | isRegularPlaylist (playlist: VideoPlaylist) { | ||
69 | return playlist.type.id === VideoPlaylistType.REGULAR | ||
70 | } | ||
71 | |||
72 | onNearOfBottom () { | ||
73 | // Last page | ||
74 | if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return | ||
75 | |||
76 | this.pagination.currentPage += 1 | ||
77 | this.loadVideoPlaylists() | ||
78 | } | ||
79 | |||
80 | resetSearch () { | ||
81 | this.videoPlaylistsSearch = '' | ||
82 | this.onVideoPlaylistSearchChanged() | ||
83 | } | ||
84 | |||
85 | onVideoPlaylistSearchChanged () { | ||
86 | this.videoPlaylistSearchChanged.next() | ||
87 | } | ||
88 | |||
89 | private loadVideoPlaylists (reset = false) { | ||
90 | this.authService.userInformationLoaded | ||
91 | .pipe(mergeMap(() => { | ||
92 | return this.videoPlaylistService.listAccountPlaylists(this.user.account, this.pagination, '-updatedAt', this.videoPlaylistsSearch) | ||
93 | })) | ||
94 | .subscribe(res => { | ||
95 | if (reset) this.videoPlaylists = [] | ||
96 | this.videoPlaylists = this.videoPlaylists.concat(res.data) | ||
97 | this.pagination.totalItems = res.total | ||
98 | |||
99 | this.onDataSubject.next(res.data) | ||
100 | }) | ||
101 | } | ||
102 | } | ||
diff --git a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.html b/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.html deleted file mode 100644 index c7c5a0b69..000000000 --- a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.html +++ /dev/null | |||
@@ -1,33 +0,0 @@ | |||
1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> | ||
2 | <div class="modal-header"> | ||
3 | <h4 i18n class="modal-title">Change ownership</h4> | ||
4 | |||
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> | ||
6 | </div> | ||
7 | |||
8 | <div class="modal-body" [formGroup]="form"> | ||
9 | <div class="form-group"> | ||
10 | <label i18n for="next-ownership-username">Select the next owner</label> | ||
11 | <p-autoComplete formControlName="username" [suggestions]="usernamePropositions" | ||
12 | (completeMethod)="search($event)" id="next-ownership-username"></p-autoComplete> | ||
13 | <div *ngIf="formErrors.username" class="form-error"> | ||
14 | {{ formErrors.username }} | ||
15 | </div> | ||
16 | </div> | ||
17 | </div> | ||
18 | |||
19 | <div class="modal-footer"> | ||
20 | <div class="form-group inputs"> | ||
21 | <input | ||
22 | type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" | ||
23 | (click)="dismiss()" (key.enter)="dismiss()" | ||
24 | > | ||
25 | |||
26 | <input | ||
27 | type="submit" i18n-value value="Submit" class="action-button-submit" | ||
28 | [disabled]="!form.valid" | ||
29 | (click)="close()" | ||
30 | /> | ||
31 | </div> | ||
32 | </div> | ||
33 | </ng-template> | ||
diff --git a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.scss b/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.scss deleted file mode 100644 index a79fec179..000000000 --- a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.scss +++ /dev/null | |||
@@ -1,10 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | p-autocomplete { | ||
5 | display: block; | ||
6 | } | ||
7 | |||
8 | .form-group { | ||
9 | margin: 20px 0; | ||
10 | } \ No newline at end of file | ||
diff --git a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.ts b/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.ts deleted file mode 100644 index 84237dee1..000000000 --- a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.ts +++ /dev/null | |||
@@ -1,69 +0,0 @@ | |||
1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' | ||
2 | import { Notifier, UserService } from '@app/core' | ||
3 | import { OWNERSHIP_CHANGE_USERNAME_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' | ||
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | ||
5 | import { Video, VideoOwnershipService } from '@app/shared/shared-main' | ||
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-video-change-ownership', | ||
10 | templateUrl: './video-change-ownership.component.html', | ||
11 | styleUrls: [ './video-change-ownership.component.scss' ] | ||
12 | }) | ||
13 | export class VideoChangeOwnershipComponent extends FormReactive implements OnInit { | ||
14 | @ViewChild('modal', { static: true }) modal: ElementRef | ||
15 | |||
16 | usernamePropositions: string[] | ||
17 | |||
18 | error: string = null | ||
19 | |||
20 | private video: Video | undefined = undefined | ||
21 | |||
22 | constructor ( | ||
23 | protected formValidatorService: FormValidatorService, | ||
24 | private videoOwnershipService: VideoOwnershipService, | ||
25 | private notifier: Notifier, | ||
26 | private userService: UserService, | ||
27 | private modalService: NgbModal | ||
28 | ) { | ||
29 | super() | ||
30 | } | ||
31 | |||
32 | ngOnInit () { | ||
33 | this.buildForm({ | ||
34 | username: OWNERSHIP_CHANGE_USERNAME_VALIDATOR | ||
35 | }) | ||
36 | this.usernamePropositions = [] | ||
37 | } | ||
38 | |||
39 | show (video: Video) { | ||
40 | this.video = video | ||
41 | this.modalService | ||
42 | .open(this.modal, { centered: true }) | ||
43 | .result | ||
44 | .then(() => this.changeOwnership()) | ||
45 | .catch((_) => _) // Called when closing (cancel) the modal without validating, do nothing | ||
46 | } | ||
47 | |||
48 | search (event: { query: string }) { | ||
49 | const query = event.query | ||
50 | this.userService.autocomplete(query) | ||
51 | .subscribe( | ||
52 | usernames => this.usernamePropositions = usernames, | ||
53 | |||
54 | err => this.notifier.error(err.message) | ||
55 | ) | ||
56 | } | ||
57 | |||
58 | changeOwnership () { | ||
59 | const username = this.form.value['username'] | ||
60 | |||
61 | this.videoOwnershipService | ||
62 | .changeOwnership(this.video.id, username) | ||
63 | .subscribe( | ||
64 | () => this.notifier.success($localize`Ownership change request sent.`), | ||
65 | |||
66 | err => this.notifier.error(err.message) | ||
67 | ) | ||
68 | } | ||
69 | } | ||
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 deleted file mode 100644 index aa5b284e7..000000000 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ /dev/null | |||
@@ -1,46 +0,0 @@ | |||
1 | <h1> | ||
2 | <span> | ||
3 | <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon> | ||
4 | <ng-container i18n>My videos</ng-container> | ||
5 | <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> | ||
6 | </span> | ||
7 | </h1> | ||
8 | |||
9 | <div class="videos-header d-flex justify-content-between"> | ||
10 | <div class="has-feedback has-clear"> | ||
11 | <input type="text" placeholder="Search your videos" i18n-placeholder [(ngModel)]="videosSearch" | ||
12 | (ngModelChange)="onVideosSearchChanged()" /> | ||
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 | </div> | ||
17 | |||
18 | <my-videos-selection | ||
19 | [pagination]="pagination" | ||
20 | [(selection)]="selection" | ||
21 | [(videosModel)]="videos" | ||
22 | [miniatureDisplayOptions]="miniatureDisplayOptions" | ||
23 | [titlePage]="titlePage" | ||
24 | [getVideosObservableFunction]="getVideosObservableFunction" | ||
25 | [ownerDisplayType]="ownerDisplayType" | ||
26 | #videosSelection | ||
27 | > | ||
28 | <ng-template ptTemplate="globalButtons"> | ||
29 | <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> | ||
30 | <my-global-icon iconName="delete" aria-hidden="true"></my-global-icon> | ||
31 | <ng-container i18n>Delete</ng-container> | ||
32 | </span> | ||
33 | </ng-template> | ||
34 | |||
35 | <ng-template ptTemplate="rowButtons" let-video> | ||
36 | <div class="action-button"> | ||
37 | <my-edit-button label [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button> | ||
38 | |||
39 | <my-action-dropdown [actions]="videoActions" [entry]="{ video: video }"></my-action-dropdown> | ||
40 | </div> | ||
41 | </ng-template> | ||
42 | </my-videos-selection> | ||
43 | |||
44 | |||
45 | <my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership> | ||
46 | <my-live-stream-information #liveStreamInformationModal></my-live-stream-information> | ||
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 deleted file mode 100644 index 246f46320..000000000 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss +++ /dev/null | |||
@@ -1,127 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | input[type=text] { | ||
5 | @include peertube-input-text(300px); | ||
6 | } | ||
7 | |||
8 | .action-button-delete-selection { | ||
9 | display: inline-block; | ||
10 | |||
11 | @include peertube-button; | ||
12 | @include orange-button; | ||
13 | @include button-with-icon(21px); | ||
14 | |||
15 | my-global-icon { | ||
16 | @include apply-svg-color(#fff); | ||
17 | } | ||
18 | } | ||
19 | |||
20 | ::ng-deep { | ||
21 | .video { | ||
22 | flex-wrap: wrap; | ||
23 | } | ||
24 | |||
25 | .action-button span { | ||
26 | white-space: nowrap; | ||
27 | } | ||
28 | |||
29 | .video-miniature { | ||
30 | &.display-as-row { | ||
31 | // width: min-content !important; | ||
32 | width: 100% !important; | ||
33 | |||
34 | .video-bottom .video-miniature-information { | ||
35 | width: max-content !important; | ||
36 | min-width: unset !important; | ||
37 | } | ||
38 | } | ||
39 | |||
40 | .video-bottom { | ||
41 | max-width: 350px; | ||
42 | } | ||
43 | } | ||
44 | } | ||
45 | |||
46 | .action-button { | ||
47 | display: flex; | ||
48 | margin-left: 55px; | ||
49 | margin-top: 10px; | ||
50 | align-self: flex-end; | ||
51 | } | ||
52 | |||
53 | my-delete-button, | ||
54 | my-edit-button { | ||
55 | margin-right: 10px; | ||
56 | } | ||
57 | |||
58 | @media screen and (max-width: $small-view) { | ||
59 | .action-button { | ||
60 | flex-direction: column; | ||
61 | align-self: center; | ||
62 | margin-left: 0px; | ||
63 | } | ||
64 | |||
65 | ::ng-deep { | ||
66 | .video-miniature { | ||
67 | align-items: center; | ||
68 | |||
69 | .video-bottom, | ||
70 | .video-bottom .video-miniature-information { | ||
71 | /* same width than a.video-thumbnail */ | ||
72 | max-width: $video-thumbnail-width !important; | ||
73 | } | ||
74 | } | ||
75 | } | ||
76 | |||
77 | my-delete-button, | ||
78 | my-edit-button { | ||
79 | margin-right: 0px; | ||
80 | |||
81 | ::ng-deep { | ||
82 | span, a { | ||
83 | margin-right: 0px; | ||
84 | } | ||
85 | } | ||
86 | } | ||
87 | |||
88 | my-delete-button, | ||
89 | my-edit-button, | ||
90 | my-button { | ||
91 | margin-top: 15px; | ||
92 | width: 100%; | ||
93 | text-align: center; | ||
94 | |||
95 | ::ng-deep { | ||
96 | .action-button { | ||
97 | /* same width than a.video-thumbnail */ | ||
98 | width: $video-thumbnail-width; | ||
99 | } | ||
100 | } | ||
101 | } | ||
102 | } | ||
103 | |||
104 | // Adapt my-video-miniature on small screens with menu | ||
105 | @media screen and (min-width: $small-view) and (max-width: #{breakpoint(lg) + ($not-expanded-horizontal-margins / 3) * 2}) { | ||
106 | :host-context(.main-col:not(.expanded)) { | ||
107 | ::ng-deep { | ||
108 | .video-miniature { | ||
109 | flex-direction: column; | ||
110 | |||
111 | .video-miniature-name { | ||
112 | max-width: $video-thumbnail-width; | ||
113 | } | ||
114 | } | ||
115 | } | ||
116 | } | ||
117 | } | ||
118 | |||
119 | @media screen and (max-width: $mobile-view) { | ||
120 | .videos-header { | ||
121 | flex-direction: column; | ||
122 | |||
123 | input[type=text] { | ||
124 | width: 100% !important; | ||
125 | } | ||
126 | } | ||
127 | } | ||
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts deleted file mode 100644 index 84f022ad2..000000000 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts +++ /dev/null | |||
@@ -1,179 +0,0 @@ | |||
1 | import { concat, Observable, Subject } from 'rxjs' | ||
2 | import { debounceTime, tap, toArray } from 'rxjs/operators' | ||
3 | import { Component, OnInit, ViewChild } from '@angular/core' | ||
4 | import { ActivatedRoute, Router } from '@angular/router' | ||
5 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService } from '@app/core' | ||
6 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | ||
7 | import { immutableAssign } from '@app/helpers' | ||
8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | ||
9 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' | ||
10 | import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' | ||
11 | import { VideoSortField } from '@shared/models' | ||
12 | import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' | ||
13 | |||
14 | @Component({ | ||
15 | selector: 'my-account-videos', | ||
16 | templateUrl: './my-account-videos.component.html', | ||
17 | styleUrls: [ './my-account-videos.component.scss' ] | ||
18 | }) | ||
19 | export class MyAccountVideosComponent implements OnInit, DisableForReuseHook { | ||
20 | @ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent | ||
21 | @ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent | ||
22 | @ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent | ||
23 | |||
24 | titlePage: string | ||
25 | selection: SelectionType = {} | ||
26 | pagination: ComponentPagination = { | ||
27 | currentPage: 1, | ||
28 | itemsPerPage: 10, | ||
29 | totalItems: null | ||
30 | } | ||
31 | miniatureDisplayOptions: MiniatureDisplayOptions = { | ||
32 | date: true, | ||
33 | views: true, | ||
34 | by: true, | ||
35 | privacyLabel: false, | ||
36 | privacyText: true, | ||
37 | state: true, | ||
38 | blacklistInfo: true | ||
39 | } | ||
40 | ownerDisplayType: OwnerDisplayType = 'videoChannel' | ||
41 | |||
42 | videoActions: DropdownAction<{ video: Video }>[] = [] | ||
43 | |||
44 | videos: Video[] = [] | ||
45 | videosSearch: string | ||
46 | videosSearchChanged = new Subject<string>() | ||
47 | getVideosObservableFunction = this.getVideosObservable.bind(this) | ||
48 | |||
49 | constructor ( | ||
50 | protected router: Router, | ||
51 | protected serverService: ServerService, | ||
52 | protected route: ActivatedRoute, | ||
53 | protected authService: AuthService, | ||
54 | protected notifier: Notifier, | ||
55 | protected screenService: ScreenService, | ||
56 | private confirmService: ConfirmService, | ||
57 | private videoService: VideoService | ||
58 | ) { | ||
59 | this.titlePage = $localize`My videos` | ||
60 | } | ||
61 | |||
62 | ngOnInit () { | ||
63 | this.buildActions() | ||
64 | |||
65 | this.videosSearchChanged | ||
66 | .pipe(debounceTime(500)) | ||
67 | .subscribe(() => { | ||
68 | this.videosSelection.reloadVideos() | ||
69 | }) | ||
70 | } | ||
71 | |||
72 | resetSearch () { | ||
73 | this.videosSearch = '' | ||
74 | this.onVideosSearchChanged() | ||
75 | } | ||
76 | |||
77 | onVideosSearchChanged () { | ||
78 | this.videosSearchChanged.next() | ||
79 | } | ||
80 | |||
81 | disableForReuse () { | ||
82 | this.videosSelection.disableForReuse() | ||
83 | } | ||
84 | |||
85 | enabledForReuse () { | ||
86 | this.videosSelection.enabledForReuse() | ||
87 | } | ||
88 | |||
89 | getVideosObservable (page: number, sort: VideoSortField) { | ||
90 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) | ||
91 | |||
92 | return this.videoService.getMyVideos(newPagination, sort, this.videosSearch) | ||
93 | .pipe( | ||
94 | tap(res => this.pagination.totalItems = res.total) | ||
95 | ) | ||
96 | } | ||
97 | |||
98 | async deleteSelectedVideos () { | ||
99 | const toDeleteVideosIds = Object.keys(this.selection) | ||
100 | .filter(k => this.selection[ k ] === true) | ||
101 | .map(k => parseInt(k, 10)) | ||
102 | |||
103 | const res = await this.confirmService.confirm( | ||
104 | $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?`, | ||
105 | $localize`Delete` | ||
106 | ) | ||
107 | if (res === false) return | ||
108 | |||
109 | const observables: Observable<any>[] = [] | ||
110 | for (const videoId of toDeleteVideosIds) { | ||
111 | const o = this.videoService.removeVideo(videoId) | ||
112 | .pipe(tap(() => this.removeVideoFromArray(videoId))) | ||
113 | |||
114 | observables.push(o) | ||
115 | } | ||
116 | |||
117 | concat(...observables) | ||
118 | .pipe(toArray()) | ||
119 | .subscribe( | ||
120 | () => { | ||
121 | this.notifier.success($localize`${toDeleteVideosIds.length} videos deleted.`) | ||
122 | this.selection = {} | ||
123 | }, | ||
124 | |||
125 | err => this.notifier.error(err.message) | ||
126 | ) | ||
127 | } | ||
128 | |||
129 | async deleteVideo (video: Video) { | ||
130 | const res = await this.confirmService.confirm( | ||
131 | $localize`Do you really want to delete ${video.name}?`, | ||
132 | $localize`Delete` | ||
133 | ) | ||
134 | if (res === false) return | ||
135 | |||
136 | this.videoService.removeVideo(video.id) | ||
137 | .subscribe( | ||
138 | () => { | ||
139 | this.notifier.success($localize`Video ${video.name} deleted.`) | ||
140 | this.removeVideoFromArray(video.id) | ||
141 | }, | ||
142 | |||
143 | error => this.notifier.error(error.message) | ||
144 | ) | ||
145 | } | ||
146 | |||
147 | changeOwnership (video: Video) { | ||
148 | this.videoChangeOwnershipModal.show(video) | ||
149 | } | ||
150 | |||
151 | displayLiveInformation (video: Video) { | ||
152 | this.liveStreamInformationModal.show(video) | ||
153 | } | ||
154 | |||
155 | private removeVideoFromArray (id: number) { | ||
156 | this.videos = this.videos.filter(v => v.id !== id) | ||
157 | } | ||
158 | |||
159 | private buildActions () { | ||
160 | this.videoActions = [ | ||
161 | { | ||
162 | label: $localize`Display live information`, | ||
163 | handler: ({ video }) => this.displayLiveInformation(video), | ||
164 | isDisplayed: ({ video }) => video.isLive, | ||
165 | iconName: 'live' | ||
166 | }, | ||
167 | { | ||
168 | label: $localize`Change ownership`, | ||
169 | handler: ({ video }) => this.changeOwnership(video), | ||
170 | iconName: 'ownership-change' | ||
171 | }, | ||
172 | { | ||
173 | label: $localize`Delete`, | ||
174 | handler: ({ video }) => this.deleteVideo(video), | ||
175 | iconName: 'delete' | ||
176 | } | ||
177 | ] | ||
178 | } | ||
179 | } | ||
diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts index d3bf8d143..d6e9d1c15 100644 --- a/client/src/app/+my-account/my-account.component.ts +++ b/client/src/app/+my-account/my-account.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { AuthService, AuthUser, ScreenService, ServerService } from '@app/core' | 2 | import { AuthUser, ScreenService } from '@app/core' |
3 | import { ServerConfig } from '@shared/models' | ||
4 | import { TopMenuDropdownParam } from '../shared/shared-main/misc/top-menu-dropdown.component' | 3 | import { TopMenuDropdownParam } from '../shared/shared-main/misc/top-menu-dropdown.component' |
5 | 4 | ||
6 | @Component({ | 5 | @Component({ |
@@ -12,11 +11,7 @@ export class MyAccountComponent implements OnInit { | |||
12 | menuEntries: TopMenuDropdownParam[] = [] | 11 | menuEntries: TopMenuDropdownParam[] = [] |
13 | user: AuthUser | 12 | user: AuthUser |
14 | 13 | ||
15 | private serverConfig: ServerConfig | ||
16 | |||
17 | constructor ( | 14 | constructor ( |
18 | private serverService: ServerService, | ||
19 | private authService: AuthService, | ||
20 | private screenService: ScreenService | 15 | private screenService: ScreenService |
21 | ) { } | 16 | ) { } |
22 | 17 | ||
@@ -25,67 +20,12 @@ export class MyAccountComponent implements OnInit { | |||
25 | } | 20 | } |
26 | 21 | ||
27 | ngOnInit (): void { | 22 | ngOnInit (): void { |
28 | this.serverConfig = this.serverService.getTmpConfig() | 23 | this.buildMenu() |
29 | this.serverService.getConfig() | ||
30 | .subscribe(config => this.serverConfig = config) | ||
31 | |||
32 | this.user = this.authService.getUser() | ||
33 | |||
34 | this.authService.userInformationLoaded.subscribe( | ||
35 | () => this.buildMenu() | ||
36 | ) | ||
37 | } | ||
38 | |||
39 | isVideoImportEnabled () { | ||
40 | const importConfig = this.serverConfig.import.videos | ||
41 | |||
42 | return importConfig.http.enabled || importConfig.torrent.enabled | ||
43 | } | 24 | } |
44 | 25 | ||
45 | private buildMenu () { | 26 | private buildMenu () { |
46 | const libraryEntries: TopMenuDropdownParam = { | 27 | const moderationEntries: TopMenuDropdownParam = { |
47 | label: $localize`My library`, | 28 | label: $localize`Moderation`, |
48 | children: [ | ||
49 | { | ||
50 | label: $localize`My channels`, | ||
51 | routerLink: '/my-account/video-channels', | ||
52 | iconName: 'channel' | ||
53 | }, | ||
54 | { | ||
55 | label: $localize`My videos`, | ||
56 | routerLink: '/my-account/videos', | ||
57 | iconName: 'videos', | ||
58 | isDisplayed: () => this.user.canSeeVideosLink | ||
59 | }, | ||
60 | { | ||
61 | label: $localize`My playlists`, | ||
62 | routerLink: '/my-account/video-playlists', | ||
63 | iconName: 'playlists' | ||
64 | }, | ||
65 | { | ||
66 | label: $localize`My subscriptions`, | ||
67 | routerLink: '/my-account/subscriptions', | ||
68 | iconName: 'subscriptions' | ||
69 | }, | ||
70 | { | ||
71 | label: $localize`My history`, | ||
72 | routerLink: '/my-account/history/videos', | ||
73 | iconName: 'history' | ||
74 | } | ||
75 | ] | ||
76 | } | ||
77 | |||
78 | if (this.isVideoImportEnabled()) { | ||
79 | libraryEntries.children.push({ | ||
80 | label: 'My imports', | ||
81 | routerLink: '/my-account/video-imports', | ||
82 | iconName: 'cloud-download', | ||
83 | isDisplayed: () => this.user.canSeeVideosLink | ||
84 | }) | ||
85 | } | ||
86 | |||
87 | const miscEntries: TopMenuDropdownParam = { | ||
88 | label: $localize`Misc`, | ||
89 | children: [ | 29 | children: [ |
90 | { | 30 | { |
91 | label: $localize`Muted accounts`, | 31 | label: $localize`Muted accounts`, |
@@ -98,29 +38,25 @@ export class MyAccountComponent implements OnInit { | |||
98 | iconName: 'peertube-x' | 38 | iconName: 'peertube-x' |
99 | }, | 39 | }, |
100 | { | 40 | { |
101 | label: $localize`My abuse reports`, | 41 | label: $localize`Abuse reports`, |
102 | routerLink: '/my-account/abuses', | 42 | routerLink: '/my-account/abuses', |
103 | iconName: 'flag' | 43 | iconName: 'flag' |
104 | }, | ||
105 | { | ||
106 | label: $localize`Ownership changes`, | ||
107 | routerLink: '/my-account/ownership', | ||
108 | iconName: 'download' | ||
109 | } | 44 | } |
110 | ] | 45 | ] |
111 | } | 46 | } |
112 | 47 | ||
113 | this.menuEntries = [ | 48 | this.menuEntries = [ |
114 | { | 49 | { |
115 | label: $localize`My settings`, | 50 | label: $localize`Settings`, |
116 | routerLink: '/my-account/settings' | 51 | routerLink: '/my-account/settings' |
117 | }, | 52 | }, |
53 | |||
118 | { | 54 | { |
119 | label: $localize`My notifications`, | 55 | label: $localize`Notifications`, |
120 | routerLink: '/my-account/notifications' | 56 | routerLink: '/my-account/notifications' |
121 | }, | 57 | }, |
122 | libraryEntries, | 58 | |
123 | miscEntries | 59 | moderationEntries |
124 | ] | 60 | ] |
125 | } | 61 | } |
126 | } | 62 | } |
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 6d21fff72..d3b6a9fa3 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts | |||
@@ -6,21 +6,14 @@ import { NgModule } from '@angular/core' | |||
6 | import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' | 6 | import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' |
7 | import { SharedFormModule } from '@app/shared/shared-forms' | 7 | import { SharedFormModule } from '@app/shared/shared-forms' |
8 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | 8 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' |
9 | import { SharedVideoLiveModule } from '@app/shared/shared-video-live' | ||
10 | import { SharedMainModule } from '@app/shared/shared-main' | 9 | import { SharedMainModule } from '@app/shared/shared-main' |
11 | import { SharedModerationModule } from '@app/shared/shared-moderation' | 10 | import { SharedModerationModule } from '@app/shared/shared-moderation' |
12 | import { SharedShareModal } from '@app/shared/shared-share-modal' | 11 | import { SharedShareModal } from '@app/shared/shared-share-modal' |
13 | import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' | 12 | import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' |
14 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription/shared-user-subscription.module' | ||
15 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' | ||
16 | import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module' | ||
17 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' | 13 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' |
18 | import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' | 14 | import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' |
19 | import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' | 15 | import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' |
20 | import { MyAccountHistoryComponent } from './my-account-history/my-account-history.component' | ||
21 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' | 16 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' |
22 | import { MyAccountAcceptOwnershipComponent } from './my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component' | ||
23 | import { MyAccountOwnershipComponent } from './my-account-ownership/my-account-ownership.component' | ||
24 | import { MyAccountRoutingModule } from './my-account-routing.module' | 17 | import { MyAccountRoutingModule } from './my-account-routing.module' |
25 | import { MyAccountChangeEmailComponent } from './my-account-settings/my-account-change-email' | 18 | import { MyAccountChangeEmailComponent } from './my-account-settings/my-account-change-email' |
26 | import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component' | 19 | import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component' |
@@ -28,14 +21,6 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d | |||
28 | import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' | 21 | import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' |
29 | import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' | 22 | import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' |
30 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' | 23 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' |
31 | import { MyAccountSubscriptionsComponent } from './my-account-subscriptions/my-account-subscriptions.component' | ||
32 | import { MyAccountVideoImportsComponent } from './my-account-video-imports/my-account-video-imports.component' | ||
33 | import { MyAccountVideoPlaylistCreateComponent } from './my-account-video-playlists/my-account-video-playlist-create.component' | ||
34 | import { MyAccountVideoPlaylistElementsComponent } from './my-account-video-playlists/my-account-video-playlist-elements.component' | ||
35 | import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playlists/my-account-video-playlist-update.component' | ||
36 | import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component' | ||
37 | import { VideoChangeOwnershipComponent } from './my-account-videos/modals/video-change-ownership.component' | ||
38 | import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component' | ||
39 | import { MyAccountComponent } from './my-account.component' | 24 | import { MyAccountComponent } from './my-account.component' |
40 | 25 | ||
41 | @NgModule({ | 26 | @NgModule({ |
@@ -50,14 +35,10 @@ import { MyAccountComponent } from './my-account.component' | |||
50 | SharedMainModule, | 35 | SharedMainModule, |
51 | SharedFormModule, | 36 | SharedFormModule, |
52 | SharedModerationModule, | 37 | SharedModerationModule, |
53 | SharedVideoMiniatureModule, | ||
54 | SharedUserSubscriptionModule, | ||
55 | SharedVideoPlaylistModule, | ||
56 | SharedUserInterfaceSettingsModule, | 38 | SharedUserInterfaceSettingsModule, |
57 | SharedGlobalIconModule, | 39 | SharedGlobalIconModule, |
58 | SharedAbuseListModule, | 40 | SharedAbuseListModule, |
59 | SharedShareModal, | 41 | SharedShareModal |
60 | SharedVideoLiveModule | ||
61 | ], | 42 | ], |
62 | 43 | ||
63 | declarations: [ | 44 | declarations: [ |
@@ -67,26 +48,12 @@ import { MyAccountComponent } from './my-account.component' | |||
67 | MyAccountProfileComponent, | 48 | MyAccountProfileComponent, |
68 | MyAccountChangeEmailComponent, | 49 | MyAccountChangeEmailComponent, |
69 | 50 | ||
70 | MyAccountVideosComponent, | ||
71 | |||
72 | VideoChangeOwnershipComponent, | ||
73 | |||
74 | MyAccountOwnershipComponent, | ||
75 | MyAccountAcceptOwnershipComponent, | ||
76 | MyAccountVideoImportsComponent, | ||
77 | MyAccountDangerZoneComponent, | 51 | MyAccountDangerZoneComponent, |
78 | MyAccountSubscriptionsComponent, | ||
79 | MyAccountBlocklistComponent, | 52 | MyAccountBlocklistComponent, |
80 | MyAccountAbusesListComponent, | 53 | MyAccountAbusesListComponent, |
81 | MyAccountServerBlocklistComponent, | 54 | MyAccountServerBlocklistComponent, |
82 | MyAccountHistoryComponent, | ||
83 | MyAccountNotificationsComponent, | 55 | MyAccountNotificationsComponent, |
84 | MyAccountNotificationPreferencesComponent, | 56 | MyAccountNotificationPreferencesComponent |
85 | |||
86 | MyAccountVideoPlaylistCreateComponent, | ||
87 | MyAccountVideoPlaylistUpdateComponent, | ||
88 | MyAccountVideoPlaylistsComponent, | ||
89 | MyAccountVideoPlaylistElementsComponent | ||
90 | ], | 57 | ], |
91 | 58 | ||
92 | exports: [ | 59 | exports: [ |