aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+my-account
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-11-12 15:28:54 +0100
committerChocobozzz <chocobozzz@cpy.re>2020-11-13 12:02:21 +0100
commit17119e4a546522468878cf115558b17949ab50d0 (patch)
tree3f130cfd7fdccf5aeeac9beee941750590239047 /client/src/app/+my-account
parentb4bc269e5517849b5b89052f0c1a2c01b6f65089 (diff)
downloadPeerTube-17119e4a546522468878cf115558b17949ab50d0.tar.gz
PeerTube-17119e4a546522468878cf115558b17949ab50d0.tar.zst
PeerTube-17119e4a546522468878cf115558b17949ab50d0.zip
Reorganize left menu and account menu
Add my-settings and my-library in left menu Move administration below my-library Split account menu: my-setting and my library
Diffstat (limited to 'client/src/app/+my-account')
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts83
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.html105
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.component.scss67
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-edit.ts19
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-update.component.ts135
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts41
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html49
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss125
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts172
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.module.ts31
-rw-r--r--client/src/app/+my-account/my-account-history/my-account-history.component.html28
-rw-r--r--client/src/app/+my-account/my-account-history/my-account-history.component.scss59
-rw-r--r--client/src/app/+my-account/my-account-history/my-account-history.component.ts103
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html36
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.scss14
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts72
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html90
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss71
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts83
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts97
-rw-r--r--client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html42
-rw-r--r--client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss81
-rw-r--r--client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts75
-rw-r--r--client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html70
-rw-r--r--client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss14
-rw-r--r--client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts77
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts92
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html100
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.scss36
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts13
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html51
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss83
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts198
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts130
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html35
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss78
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts102
-rw-r--r--client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.html33
-rw-r--r--client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.scss10
-rw-r--r--client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.ts69
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.html46
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.scss127
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.ts179
-rw-r--r--client/src/app/+my-account/my-account.component.ts84
-rw-r--r--client/src/app/+my-account/my-account.module.ts37
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 @@
1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router'
3import { AuthService, Notifier } from '@app/core'
4import {
5 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
6 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
7 VIDEO_CHANNEL_NAME_VALIDATOR,
8 VIDEO_CHANNEL_SUPPORT_VALIDATOR
9} from '@app/shared/form-validators/video-channel-validators'
10import { FormValidatorService } from '@app/shared/shared-forms'
11import { VideoChannelService } from '@app/shared/shared-main'
12import { VideoChannelCreate } from '@shared/models'
13import { 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})
20export 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
4label {
5 font-weight: $font-regular;
6 font-size: 100%;
7}
8
9.video-channel-title {
10 @include settings-big-title;
11}
12
13my-actor-avatar-info {
14 display: block;
15 margin-bottom: 20px;
16}
17
18.input-group {
19 @include peertube-input-group(fit-content);
20}
21
22.input-group-append {
23 height: 30px;
24}
25
26input {
27 &[type=text] {
28 @include peertube-input-text(340px);
29
30 display: block;
31
32 &#name {
33 width: auto;
34 flex-grow: 1;
35 }
36 }
37
38 &[type=submit] {
39 @include peertube-button;
40 @include orange-button;
41 margin-left: auto;
42 }
43}
44
45textarea {
46 @include peertube-textarea(500px, 150px);
47
48 display: block;
49}
50
51.peertube-select-container {
52 @include peertube-select-container(340px);
53}
54
55.breadcrumb {
56 @include breadcrumb;
57}
58
59@media screen and (max-width: $small-view) {
60 input[type=text]#name {
61 width: auto !important;
62 }
63
64 label[for=name] + div, textarea {
65 width: 100%;
66 }
67}
diff --git a/client/src/app/+my-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 @@
1import { FormReactive } from '@app/shared/shared-forms'
2import { VideoChannel } from '@app/shared/shared-main'
3
4export 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 @@
1import { Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, Notifier, ServerService } from '@app/core'
5import {
6 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
7 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
8 VIDEO_CHANNEL_SUPPORT_VALIDATOR
9} from '@app/shared/form-validators/video-channel-validators'
10import { FormValidatorService } from '@app/shared/shared-forms'
11import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
12import { ServerConfig, VideoChannelUpdate } from '@shared/models'
13import { 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})
20export 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 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3import { MyAccountVideoChannelUpdateComponent } from './my-account-video-channel-update.component'
4import { MyAccountVideoChannelCreateComponent } from './my-account-video-channel-create.component'
5import { MyAccountVideoChannelsComponent } from './my-account-video-channels.component'
6
7const 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})
41export 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
8input[type=text] {
9 @include peertube-input-text(300px);
10}
11
12::ng-deep .action-button {
13 &.action-button-edit {
14 margin-right: 10px;
15 }
16}
17
18.video-channel {
19 @include row-blocks;
20 padding-bottom: 0;
21
22 img {
23 @include avatar(80px);
24
25 margin-right: 10px;
26 }
27
28 .video-channel-info {
29 flex-grow: 1;
30
31 a.video-channel-names {
32 @include disable-default-a-behaviour;
33
34 width: fit-content;
35 display: flex;
36 align-items: baseline;
37 color: pvar(--mainForegroundColor);
38
39 .video-channel-display-name {
40 font-weight: $font-semibold;
41 font-size: 18px;
42 }
43
44 .video-channel-name {
45 font-size: 14px;
46 color: $grey-actor-name;
47 margin-left: 5px;
48 }
49 }
50 }
51
52 .video-channel-buttons {
53 margin-top: 10px;
54 min-width: 190px;
55 }
56}
57
58::ng-deep .chartjs-render-monitor {
59 position: relative;
60 top: 1px;
61}
62
63.video-channels-header {
64 margin-bottom: 30px;
65}
66
67@media screen and (max-width: $small-view) {
68 .video-channels-header {
69 text-align: center;
70 }
71
72 .video-channel {
73 padding-bottom: 10px;
74
75 .video-channel-info {
76 padding-bottom: 10px;
77 text-align: center;
78
79 .video-channel-names {
80 flex-direction: column;
81 align-items: center !important;
82 margin: auto;
83
84 .video-channel-name {
85 margin-left: 0px !important;
86 }
87 }
88 }
89
90 img {
91 margin-right: 0;
92 }
93
94 .video-channel-buttons {
95 align-self: center;
96 }
97 }
98}
99
100@media screen and (max-width: $mobile-view) {
101 .video-channels-header {
102 flex-direction: column;
103
104 input[type=text] {
105 width: 100% !important;
106 margin-bottom: 12px;
107 }
108 }
109}
110
111@media screen and (min-width: breakpoint(lg)) {
112 :host-context(.main-col:not(.expanded)) {
113 .video-channel-buttons {
114 float: right;
115 }
116 }
117}
118
119@media screen and (min-width: $small-view) {
120 :host-context(.expanded) {
121 .video-channel-buttons {
122 float: right;
123 }
124 }
125}
diff --git a/client/src/app/+my-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 @@
1import { ChartData } from 'chart.js'
2import { max, maxBy, min, minBy } from 'lodash-es'
3import { Subject } from 'rxjs'
4import { debounceTime, mergeMap } from 'rxjs/operators'
5import { Component, OnInit } from '@angular/core'
6import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core'
7import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
8
9@Component({
10 selector: 'my-account-video-channels',
11 templateUrl: './my-account-video-channels.component.html',
12 styleUrls: [ './my-account-video-channels.component.scss' ]
13})
14export 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}?
112It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another
113channel 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 @@
1import { ChartModule } from 'primeng/chart'
2import { NgModule } from '@angular/core'
3import { SharedFormModule } from '@app/shared/shared-forms'
4import { SharedGlobalIconModule } from '@app/shared/shared-icons'
5import { SharedMainModule } from '@app/shared/shared-main'
6import { MyAccountVideoChannelCreateComponent } from './my-account-video-channel-create.component'
7import { MyAccountVideoChannelUpdateComponent } from './my-account-video-channel-update.component'
8import { MyAccountVideoChannelsRoutingModule } from './my-account-video-channels-routing.module'
9import { 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})
31export 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 @@
1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import {
4 AuthService,
5 ComponentPagination,
6 ConfirmService,
7 LocalStorageService,
8 Notifier,
9 ScreenService,
10 ServerService,
11 UserService
12} from '@app/core'
13import { immutableAssign } from '@app/helpers'
14import { UserHistoryService } from '@app/shared/shared-main'
15import { 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})
22export 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
4select {
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 @@
1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { AuthService, Notifier } from '@app/core'
3import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { 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})
14export 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 @@
1import { SortMeta } from 'primeng/api'
2import { Component, OnInit, ViewChild } from '@angular/core'
3import { Notifier, RestPagination, RestTable } from '@app/core'
4import { VideoOwnershipService, Actor, Video, Account } from '@app/shared/shared-main'
5import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '@shared/models'
6import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component'
7import { 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})
14export 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'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core' 3import { MetaGuard } from '@ngx-meta/core'
4import { LoginGuard } from '../core' 4import { LoginGuard } from '../core'
5import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
5import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' 6import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
6import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' 7import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
7import { MyAccountHistoryComponent } from './my-account-history/my-account-history.component'
8import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' 8import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
9import { MyAccountOwnershipComponent } from './my-account-ownership/my-account-ownership.component'
10import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' 9import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
11import { MyAccountSubscriptionsComponent } from './my-account-subscriptions/my-account-subscriptions.component'
12import { MyAccountVideoImportsComponent } from './my-account-video-imports/my-account-video-imports.component'
13import { MyAccountVideoPlaylistCreateComponent } from './my-account-video-playlists/my-account-video-playlist-create.component'
14import { MyAccountVideoPlaylistElementsComponent } from './my-account-video-playlists/my-account-video-playlist-elements.component'
15import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playlists/my-account-video-playlist-update.component'
16import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component'
17import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
18import { MyAccountComponent } from './my-account.component' 10import { MyAccountComponent } from './my-account.component'
19import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
20 11
21const myAccountRoutes: Routes = [ 12const 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
4input[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 @@
1import { Subject } from 'rxjs'
2import { Component, OnInit } from '@angular/core'
3import { ComponentPagination, Notifier } from '@app/core'
4import { VideoChannel } from '@app/shared/shared-main'
5import { UserSubscriptionService } from '@app/shared/shared-user-subscription'
6import { 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})
13export 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
4pre {
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 @@
1import { SortMeta } from 'primeng/api'
2import { Component, OnInit } from '@angular/core'
3import { Notifier, RestPagination, RestTable } from '@app/core'
4import { VideoImportService } from '@app/shared/shared-main'
5import { 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})
12export 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 @@
1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router'
3import { AuthService, Notifier, ServerService } from '@app/core'
4import { populateAsyncUserVideoChannels } from '@app/helpers'
5import {
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'
12import { FormValidatorService } from '@app/shared/shared-forms'
13import { VideoPlaylistService } from '@app/shared/shared-video-playlist'
14import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
15import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
16import { 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})
23export 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
4label {
5 font-weight: $font-regular;
6 font-size: 100%;
7}
8
9.video-playlist-title {
10 @include settings-big-title;
11}
12
13input[type=text] {
14 @include peertube-input-text(340px);
15
16 display: block;
17}
18
19textarea {
20 @include peertube-textarea(500px, 150px);
21
22 display: block;
23}
24
25.peertube-select-container {
26 @include peertube-select-container(340px);
27}
28
29input[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 @@
1import { FormReactive, SelectChannelItem } from '@app/shared/shared-forms'
2import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models'
3import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model'
4
5export 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 @@
1import { Subject, Subscription } from 'rxjs'
2import { CdkDragDrop } from '@angular/cdk/drag-drop'
3import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router'
5import { ComponentPagination, ConfirmService, Notifier, ScreenService } from '@app/core'
6import { DropdownAction } from '@app/shared/shared-main'
7import { VideoShareComponent } from '@app/shared/shared-share-modal'
8import { VideoPlaylist, VideoPlaylistElement, VideoPlaylistService } from '@app/shared/shared-video-playlist'
9import { 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})
16export 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 @@
1import { forkJoin, Subscription } from 'rxjs'
2import { map, switchMap } from 'rxjs/operators'
3import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, Notifier, ServerService } from '@app/core'
6import { populateAsyncUserVideoChannels } from '@app/helpers'
7import {
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'
14import { FormValidatorService } from '@app/shared/shared-forms'
15import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
16import { VideoPlaylistUpdate } from '@shared/models'
17import { 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})
24export 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
8input[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 @@
1import { Subject } from 'rxjs'
2import { debounceTime, mergeMap } from 'rxjs/operators'
3import { Component, OnInit } from '@angular/core'
4import { AuthService, ComponentPagination, ConfirmService, Notifier, User } from '@app/core'
5import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
6import { 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})
13export 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
4p-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 @@
1import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
2import { Notifier, UserService } from '@app/core'
3import { OWNERSHIP_CHANGE_USERNAME_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { Video, VideoOwnershipService } from '@app/shared/shared-main'
6import { 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})
13export 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
4input[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
53my-delete-button,
54my-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 @@
1import { concat, Observable, Subject } from 'rxjs'
2import { debounceTime, tap, toArray } from 'rxjs/operators'
3import { Component, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService } from '@app/core'
6import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
7import { immutableAssign } from '@app/helpers'
8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
9import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
10import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature'
11import { VideoSortField } from '@shared/models'
12import { 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})
19export 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 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { AuthService, AuthUser, ScreenService, ServerService } from '@app/core' 2import { AuthUser, ScreenService } from '@app/core'
3import { ServerConfig } from '@shared/models'
4import { TopMenuDropdownParam } from '../shared/shared-main/misc/top-menu-dropdown.component' 3import { 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'
6import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' 6import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
7import { SharedFormModule } from '@app/shared/shared-forms' 7import { SharedFormModule } from '@app/shared/shared-forms'
8import { SharedGlobalIconModule } from '@app/shared/shared-icons' 8import { SharedGlobalIconModule } from '@app/shared/shared-icons'
9import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
10import { SharedMainModule } from '@app/shared/shared-main' 9import { SharedMainModule } from '@app/shared/shared-main'
11import { SharedModerationModule } from '@app/shared/shared-moderation' 10import { SharedModerationModule } from '@app/shared/shared-moderation'
12import { SharedShareModal } from '@app/shared/shared-share-modal' 11import { SharedShareModal } from '@app/shared/shared-share-modal'
13import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' 12import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings'
14import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription/shared-user-subscription.module'
15import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
16import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module'
17import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' 13import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
18import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' 14import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
19import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' 15import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
20import { MyAccountHistoryComponent } from './my-account-history/my-account-history.component'
21import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' 16import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
22import { MyAccountAcceptOwnershipComponent } from './my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component'
23import { MyAccountOwnershipComponent } from './my-account-ownership/my-account-ownership.component'
24import { MyAccountRoutingModule } from './my-account-routing.module' 17import { MyAccountRoutingModule } from './my-account-routing.module'
25import { MyAccountChangeEmailComponent } from './my-account-settings/my-account-change-email' 18import { MyAccountChangeEmailComponent } from './my-account-settings/my-account-change-email'
26import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component' 19import { 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
28import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' 21import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences'
29import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' 22import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component'
30import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' 23import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
31import { MyAccountSubscriptionsComponent } from './my-account-subscriptions/my-account-subscriptions.component'
32import { MyAccountVideoImportsComponent } from './my-account-video-imports/my-account-video-imports.component'
33import { MyAccountVideoPlaylistCreateComponent } from './my-account-video-playlists/my-account-video-playlist-create.component'
34import { MyAccountVideoPlaylistElementsComponent } from './my-account-video-playlists/my-account-video-playlist-elements.component'
35import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playlists/my-account-video-playlist-update.component'
36import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component'
37import { VideoChangeOwnershipComponent } from './my-account-videos/modals/video-change-ownership.component'
38import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
39import { MyAccountComponent } from './my-account.component' 24import { 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: [