aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-03-06 15:36:44 +0100
committerChocobozzz <chocobozzz@cpy.re>2019-03-18 11:17:59 +0100
commit830b4faff15fb9c81d88e8e69fcdf94aad32bef8 (patch)
tree53de6c9e30ce88734b4bdda62016e0498fe78491
parentd4c9f45b31eda0b7a391ddc83eb290ca5cba311f (diff)
downloadPeerTube-830b4faff15fb9c81d88e8e69fcdf94aad32bef8.tar.gz
PeerTube-830b4faff15fb9c81d88e8e69fcdf94aad32bef8.tar.zst
PeerTube-830b4faff15fb9c81d88e8e69fcdf94aad32bef8.zip
Add/update/delete/list my playlists
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts37
-rw-r--r--client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts4
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html2
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts89
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html64
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.scss27
-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-update.component.ts132
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html20
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss50
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts85
-rw-r--r--client/src/app/+my-account/my-account.component.ts4
-rw-r--r--client/src/app/+my-account/my-account.module.ts13
-rw-r--r--client/src/app/app.component.ts1
-rw-r--r--client/src/app/core/server/server.service.ts30
-rw-r--r--client/src/app/shared/buttons/button.component.ts2
-rw-r--r--client/src/app/shared/forms/form-validators/index.ts1
-rw-r--r--client/src/app/shared/forms/form-validators/video-playlist-validators.service.ts52
-rw-r--r--client/src/app/shared/images/global-icon.component.html (renamed from client/src/app/shared/icons/global-icon.component.html)0
-rw-r--r--client/src/app/shared/images/global-icon.component.scss (renamed from client/src/app/shared/icons/global-icon.component.scss)0
-rw-r--r--client/src/app/shared/images/global-icon.component.ts (renamed from client/src/app/shared/icons/global-icon.component.ts)0
-rw-r--r--client/src/app/shared/images/image-upload.component.html (renamed from client/src/app/videos/+video-edit/shared/video-image.component.html)0
-rw-r--r--client/src/app/shared/images/image-upload.component.scss (renamed from client/src/app/videos/+video-edit/shared/video-image.component.scss)0
-rw-r--r--client/src/app/shared/images/image-upload.component.ts (renamed from client/src/app/videos/+video-edit/shared/video-image.component.ts)10
-rw-r--r--client/src/app/shared/misc/utils.ts2
-rw-r--r--client/src/app/shared/shared.module.ts20
-rw-r--r--client/src/app/shared/video-playlist/video-playlist-miniature.component.html22
-rw-r--r--client/src/app/shared/video-playlist/video-playlist-miniature.component.scss34
-rw-r--r--client/src/app/shared/video-playlist/video-playlist-miniature.component.ts11
-rw-r--r--client/src/app/shared/video-playlist/video-playlist.model.ts74
-rw-r--r--client/src/app/shared/video-playlist/video-playlist.service.ts108
-rw-r--r--client/src/app/shared/video/abstract-video-list.scss1
-rw-r--r--client/src/app/shared/video/video-miniature.component.scss22
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.html6
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.scss72
-rw-r--r--client/src/app/shared/video/video.model.ts5
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html8
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.module.ts2
-rw-r--r--client/src/app/videos/video-list/video-overview.component.scss3
-rw-r--r--client/src/sass/include/_miniature.scss133
-rw-r--r--client/src/sass/include/_mixins.scss28
-rw-r--r--client/tsconfig.json12
-rwxr-xr-xscripts/i18n/create-custom-files.ts4
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts1
-rw-r--r--shared/models/videos/index.ts9
-rw-r--r--shared/models/videos/playlist/video-playlist.model.ts2
-rw-r--r--shared/utils/videos/video-playlists.ts12
48 files changed, 1076 insertions, 153 deletions
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 9996218ca..0193afff7 100644
--- a/client/src/app/+my-account/my-account-routing.module.ts
+++ b/client/src/app/+my-account/my-account-routing.module.ts
@@ -15,6 +15,13 @@ import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blockli
15import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component' 15import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
16import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component' 16import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
17import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component' 17import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component'
18import { MyAccountVideoPlaylistsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlists.component'
19import {
20 MyAccountVideoPlaylistCreateComponent
21} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component'
22import {
23 MyAccountVideoPlaylistUpdateComponent
24} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component'
18 25
19const myAccountRoutes: Routes = [ 26const myAccountRoutes: Routes = [
20 { 27 {
@@ -36,6 +43,7 @@ const myAccountRoutes: Routes = [
36 } 43 }
37 } 44 }
38 }, 45 },
46
39 { 47 {
40 path: 'video-channels', 48 path: 'video-channels',
41 component: MyAccountVideoChannelsComponent, 49 component: MyAccountVideoChannelsComponent,
@@ -63,6 +71,35 @@ const myAccountRoutes: Routes = [
63 } 71 }
64 } 72 }
65 }, 73 },
74
75 {
76 path: 'video-playlists',
77 component: MyAccountVideoPlaylistsComponent,
78 data: {
79 meta: {
80 title: 'Account playlists'
81 }
82 }
83 },
84 {
85 path: 'video-playlists/create',
86 component: MyAccountVideoPlaylistCreateComponent,
87 data: {
88 meta: {
89 title: 'Create new playlist'
90 }
91 }
92 },
93 {
94 path: 'video-playlists/update/:videoPlaylistId',
95 component: MyAccountVideoPlaylistUpdateComponent,
96 data: {
97 meta: {
98 title: 'Update playlist'
99 }
100 }
101 },
102
66 { 103 {
67 path: 'videos', 104 path: 'videos',
68 component: MyAccountVideosComponent, 105 component: MyAccountVideosComponent,
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
index 9d2dccdf0..6ce22989b 100644
--- 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
@@ -1,7 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 3import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { UserSubscriptionService } from '@app/shared/user-subscription' 4import { UserSubscriptionService } from '@app/shared/user-subscription'
6import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 5import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
7 6
@@ -21,8 +20,7 @@ export class MyAccountSubscriptionsComponent implements OnInit {
21 20
22 constructor ( 21 constructor (
23 private userSubscriptionService: UserSubscriptionService, 22 private userSubscriptionService: UserSubscriptionService,
24 private notifier: Notifier, 23 private notifier: Notifier
25 private i18n: I18n
26 ) {} 24 ) {}
27 25
28 ngOnInit () { 26 ngOnInit () {
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
index 51db2e75d..11e87ba79 100644
--- 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
@@ -1,7 +1,7 @@
1<div class="video-channels-header"> 1<div class="video-channels-header">
2 <a class="create-button" routerLink="create"> 2 <a class="create-button" routerLink="create">
3 <my-global-icon iconName="add"></my-global-icon> 3 <my-global-icon iconName="add"></my-global-icon>
4 <ng-container i18n>Create another video channel</ng-container> 4 <ng-container i18n>Create a new video channel</ng-container>
5 </a> 5 </a>
6</div> 6</div>
7 7
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
new file mode 100644
index 000000000..61b61e221
--- /dev/null
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts
@@ -0,0 +1,89 @@
1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router'
3import { AuthService, Notifier, ServerService } from '@app/core'
4import { MyAccountVideoPlaylistEdit } from './my-account-video-playlist-edit'
5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { VideoPlaylistValidatorsService } from '@app/shared'
8import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
10import { VideoConstant } from '@shared/models'
11import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
12import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
13
14@Component({
15 selector: 'my-account-video-playlist-create',
16 templateUrl: './my-account-video-playlist-edit.component.html',
17 styleUrls: [ './my-account-video-playlist-edit.component.scss' ]
18})
19export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylistEdit implements OnInit {
20 error: string
21 videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = []
22
23 constructor (
24 protected formValidatorService: FormValidatorService,
25 private authService: AuthService,
26 private videoPlaylistValidatorsService: VideoPlaylistValidatorsService,
27 private notifier: Notifier,
28 private router: Router,
29 private videoPlaylistService: VideoPlaylistService,
30 private serverService: ServerService,
31 private i18n: I18n
32 ) {
33 super()
34 }
35
36 ngOnInit () {
37 this.buildForm({
38 'display-name': this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME,
39 privacy: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_PRIVACY,
40 description: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DESCRIPTION,
41 videoChannelId: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_CHANNEL_ID,
42 thumbnailfile: null
43 })
44
45 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
46
47 this.serverService.videoPlaylistPrivaciesLoaded.subscribe(
48 () => {
49 this.videoPlaylistPrivacies = this.serverService.getVideoPlaylistPrivacies()
50
51 this.form.patchValue({
52 privacy: VideoPlaylistPrivacy.PRIVATE
53 })
54 }
55 )
56 }
57
58 formValidated () {
59 this.error = undefined
60
61 const body = this.form.value
62 const videoPlaylistCreate: VideoPlaylistCreate = {
63 displayName: body['display-name'],
64 privacy: body.privacy,
65 description: body.description || null,
66 videoChannelId: body.videoChannelId || null,
67 thumbnailfile: body.thumbnailfile || null
68 }
69
70 this.videoPlaylistService.createVideoPlaylist(videoPlaylistCreate).subscribe(
71 () => {
72 this.notifier.success(
73 this.i18n('Playlist {{playlistName}} created.', { playlistName: videoPlaylistCreate.displayName })
74 )
75 this.router.navigate([ '/my-account', 'video-playlists' ])
76 },
77
78 err => this.error = err.message
79 )
80 }
81
82 isCreation () {
83 return true
84 }
85
86 getFormButtonTitle () {
87 return this.i18n('Create')
88 }
89}
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
new file mode 100644
index 000000000..b76488c78
--- /dev/null
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.html
@@ -0,0 +1,64 @@
1<div i18n class="form-sub-title" *ngIf="isCreation() === true">Create a new playlist</div>
2
3<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
4
5<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
6 <div class="row">
7 <div class="col-md-12 col-xl-6">
8 <div class="form-group">
9 <label i18n for="display-name">Display name</label>
10 <input
11 type="text" id="display-name"
12 formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
13 >
14 <div *ngIf="formErrors['display-name']" class="form-error">
15 {{ formErrors['display-name'] }}
16 </div>
17 </div>
18
19 <div class="form-group">
20 <label i18n for="description">Description</label>
21 <textarea
22 id="description" formControlName="description"
23 [ngClass]="{ 'input-error': formErrors['description'] }"
24 ></textarea>
25 <div *ngIf="formErrors.description" class="form-error">
26 {{ formErrors.description }}
27 </div>
28 </div>
29 </div>
30
31 <div class="col-md-12 col-xl-6">
32 <div class="form-group">
33 <label i18n for="privacy">Privacy</label>
34 <div class="peertube-select-container">
35 <select id="privacy" formControlName="privacy">
36 <option *ngFor="let privacy of videoPlaylistPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
37 </select>
38 </div>
39
40 <div *ngIf="formErrors.privacy" class="form-error">
41 {{ formErrors.privacy }}
42 </div>
43 </div>
44
45 <div class="form-group">
46 <label i18n>Channel</label>
47 <div class="peertube-select-container">
48 <select formControlName="videoChannelId">
49 <option></option>
50 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
51 </select>
52 </div>
53 </div>
54
55 <div class="form-group">
56 <my-image-upload
57 i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
58 previewWidth="200px" previewHeight="110px"
59 ></my-image-upload>
60 </div>
61 </div>
62 </div>
63 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
64</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
new file mode 100644
index 000000000..5af846d8e
--- /dev/null
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.component.scss
@@ -0,0 +1,27 @@
1@import '_variables';
2@import '_mixins';
3
4.form-sub-title {
5 margin-bottom: 20px;
6}
7
8input[type=text] {
9 @include peertube-input-text(340px);
10
11 display: block;
12}
13
14textarea {
15 @include peertube-textarea(500px, 150px);
16
17 display: block;
18}
19
20.peertube-select-container {
21 @include peertube-select-container(340px);
22}
23
24input[type=submit] {
25 @include peertube-button;
26 @include orange-button;
27}
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
new file mode 100644
index 000000000..fbfb4c8f7
--- /dev/null
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts
@@ -0,0 +1,13 @@
1import { FormReactive } from '@app/shared'
2import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
3import { ServerService } from '@app/core'
4import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model'
5
6export abstract class MyAccountVideoPlaylistEdit extends FormReactive {
7 // Declare it here to avoid errors in create template
8 videoPlaylistToUpdate: VideoPlaylist
9 userVideoChannels: { id: number, label: string }[] = []
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-update.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts
new file mode 100644
index 000000000..167d7dd09
--- /dev/null
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts
@@ -0,0 +1,132 @@
1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { AuthService, Notifier, ServerService } from '@app/core'
4import { Subscription } from 'rxjs'
5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { MyAccountVideoPlaylistEdit } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-edit'
8import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
9import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
10import { VideoPlaylistValidatorsService } from '@app/shared'
11import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model'
12import { VideoConstant } from '@shared/models'
13import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
14import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
15
16@Component({
17 selector: 'my-account-video-playlist-update',
18 templateUrl: './my-account-video-playlist-edit.component.html',
19 styleUrls: [ './my-account-video-playlist-edit.component.scss' ]
20})
21export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylistEdit implements OnInit, OnDestroy {
22 error: string
23 videoPlaylistToUpdate: VideoPlaylist
24 videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = []
25
26 private paramsSub: Subscription
27
28 constructor (
29 protected formValidatorService: FormValidatorService,
30 private authService: AuthService,
31 private videoPlaylistValidatorsService: VideoPlaylistValidatorsService,
32 private notifier: Notifier,
33 private router: Router,
34 private route: ActivatedRoute,
35 private videoPlaylistService: VideoPlaylistService,
36 private i18n: I18n,
37 private serverService: ServerService
38 ) {
39 super()
40 }
41
42 ngOnInit () {
43 this.buildForm({
44 'display-name': this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME,
45 privacy: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_PRIVACY,
46 description: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DESCRIPTION,
47 videoChannelId: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_CHANNEL_ID,
48 thumbnailfile: null
49 })
50
51 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
52
53 this.paramsSub = this.route.params.subscribe(routeParams => {
54 const videoPlaylistId = routeParams['videoPlaylistId']
55
56 this.videoPlaylistService.getVideoPlaylist(videoPlaylistId).subscribe(
57 videoPlaylistToUpdate => {
58 this.videoPlaylistToUpdate = videoPlaylistToUpdate
59
60 this.hydrateFormFromPlaylist()
61
62 this.serverService.videoPlaylistPrivaciesLoaded.subscribe(
63 () => {
64 this.videoPlaylistPrivacies = this.serverService.getVideoPlaylistPrivacies()
65 .filter(p => {
66 // If the playlist is not private, we cannot put it in private anymore
67 return this.videoPlaylistToUpdate.privacy.id === VideoPlaylistPrivacy.PRIVATE ||
68 p.id !== VideoPlaylistPrivacy.PRIVATE
69 })
70 }
71 )
72 },
73
74 err => this.error = err.message
75 )
76 })
77 }
78
79 ngOnDestroy () {
80 if (this.paramsSub) this.paramsSub.unsubscribe()
81 }
82
83 formValidated () {
84 this.error = undefined
85
86 const body = this.form.value
87 const videoPlaylistUpdate: VideoPlaylistUpdate = {
88 displayName: body['display-name'],
89 privacy: body['privacy'],
90 description: body.description || null,
91 videoChannelId: body.videoChannelId || null,
92 thumbnailfile: body.thumbnailfile || undefined
93 }
94
95 this.videoPlaylistService.updateVideoPlaylist(this.videoPlaylistToUpdate, videoPlaylistUpdate).subscribe(
96 () => {
97 this.notifier.success(
98 this.i18n('Playlist {{videoPlaylistName}} updated.', { videoPlaylistName: videoPlaylistUpdate.displayName })
99 )
100
101 this.router.navigate([ '/my-account', 'video-playlists' ])
102 },
103
104 err => this.error = err.message
105 )
106 }
107
108 isCreation () {
109 return false
110 }
111
112 getFormButtonTitle () {
113 return this.i18n('Update')
114 }
115
116 private hydrateFormFromPlaylist () {
117 this.form.patchValue({
118 'display-name': this.videoPlaylistToUpdate.displayName,
119 privacy: this.videoPlaylistToUpdate.privacy.id,
120 description: this.videoPlaylistToUpdate.description,
121 videoChannelId: this.videoPlaylistToUpdate.videoChannel ? this.videoPlaylistToUpdate.videoChannel.id : null
122 })
123
124 fetch(this.videoPlaylistToUpdate.thumbnailUrl)
125 .then(response => response.blob())
126 .then(data => {
127 this.form.patchValue({
128 thumbnailfile: data
129 })
130 })
131 }
132}
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
new file mode 100644
index 000000000..ab5d9cc5a
--- /dev/null
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html
@@ -0,0 +1,20 @@
1<div class="video-playlists-header">
2 <a class="create-button" routerLink="create">
3 <my-global-icon iconName="add"></my-global-icon>
4 <ng-container i18n>Create a new playlist</ng-container>
5 </a>
6</div>
7
8<div class="video-playlists">
9 <div *ngFor="let playlist of videoPlaylists" class="video-playlist">
10 <div class="miniature-wrapper">
11 <my-video-playlist-miniature [playlist]="playlist"></my-video-playlist-miniature>
12 </div>
13
14 <div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons">
15 <my-delete-button (click)="deleteVideoPlaylist(playlist)"></my-delete-button>
16
17 <my-edit-button [routerLink]="[ 'update', playlist.uuid ]"></my-edit-button>
18 </div>
19 </div>
20</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
new file mode 100644
index 000000000..88fba5b05
--- /dev/null
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss
@@ -0,0 +1,50 @@
1@import '_variables';
2@import '_mixins';
3
4.create-button {
5 @include create-button;
6}
7
8/deep/ .action-button {
9 &.action-button-delete {
10 margin-right: 10px;
11 }
12}
13
14.video-playlist {
15 @include row-blocks;
16
17 .miniature-wrapper {
18 flex-grow: 1;
19
20 /deep/ .miniature {
21 display: flex;
22
23 .miniature-bottom {
24 margin-left: 10px;
25 }
26 }
27 }
28
29 .video-playlist-buttons {
30 min-width: 190px;
31 }
32}
33
34.video-playlists-header {
35 text-align: right;
36 margin: 20px 0 50px;
37}
38
39@media screen and (max-width: 800px) {
40 .video-playlists-header {
41 text-align: center;
42 }
43
44 .video-playlist {
45
46 .video-playlist-buttons {
47 margin-top: 10px;
48 }
49 }
50}
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
new file mode 100644
index 000000000..761ce90e8
--- /dev/null
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts
@@ -0,0 +1,85 @@
1import { Component, OnInit } from '@angular/core'
2import { Notifier } from '@app/core'
3import { AuthService } from '../../core/auth'
4import { ConfirmService } from '../../core/confirm'
5import { User } from '@app/shared'
6import { flatMap } from 'rxjs/operators'
7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
9import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
10import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
11import { VideoPlaylistType } from '@shared/models'
12
13@Component({
14 selector: 'my-account-video-playlists',
15 templateUrl: './my-account-video-playlists.component.html',
16 styleUrls: [ './my-account-video-playlists.component.scss' ]
17})
18export class MyAccountVideoPlaylistsComponent implements OnInit {
19 videoPlaylists: VideoPlaylist[] = []
20
21 pagination: ComponentPagination = {
22 currentPage: 1,
23 itemsPerPage: 10,
24 totalItems: null
25 }
26
27 private user: User
28
29 constructor (
30 private authService: AuthService,
31 private notifier: Notifier,
32 private confirmService: ConfirmService,
33 private videoPlaylistService: VideoPlaylistService,
34 private i18n: I18n
35 ) {}
36
37 ngOnInit () {
38 this.user = this.authService.getUser()
39
40 this.loadVideoPlaylists()
41 }
42
43 async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) {
44 const res = await this.confirmService.confirm(
45 this.i18n(
46 'Do you really want to delete {{playlistDisplayName}}?',
47 { playlistDisplayName: videoPlaylist.displayName }
48 ),
49 this.i18n('Delete')
50 )
51 if (res === false) return
52
53 this.videoPlaylistService.removeVideoPlaylist(videoPlaylist)
54 .subscribe(
55 () => {
56 this.videoPlaylists = this.videoPlaylists
57 .filter(p => p.id !== videoPlaylist.id)
58
59 this.notifier.success(
60 this.i18n('Playlist {{playlistDisplayName}} deleted.', { playlistDisplayName: videoPlaylist.displayName })
61 )
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 private loadVideoPlaylists () {
73 this.authService.userInformationLoaded
74 .pipe(flatMap(() => this.videoPlaylistService.listAccountPlaylists(this.user.account)))
75 .subscribe(res => this.videoPlaylists = res.data)
76 }
77
78 private ofNearOfBottom () {
79 // Last page
80 if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
81
82 this.pagination.currentPage += 1
83 this.loadVideoPlaylists()
84 }
85}
diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts
index 8a4102d80..f624ff505 100644
--- a/client/src/app/+my-account/my-account.component.ts
+++ b/client/src/app/+my-account/my-account.component.ts
@@ -28,6 +28,10 @@ export class MyAccountComponent {
28 routerLink: '/my-account/videos' 28 routerLink: '/my-account/videos'
29 }, 29 },
30 { 30 {
31 label: this.i18n('My playlists'),
32 routerLink: '/my-account/video-playlists'
33 },
34 {
31 label: this.i18n('My subscriptions'), 35 label: this.i18n('My subscriptions'),
32 routerLink: '/my-account/subscriptions' 36 routerLink: '/my-account/subscriptions'
33 }, 37 },
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 18f51f171..3dbce2b92 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -25,6 +25,13 @@ import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-b
25import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component' 25import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
26import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component' 26import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component'
27import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences' 27import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences'
28import {
29 MyAccountVideoPlaylistCreateComponent
30} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component'
31import {
32 MyAccountVideoPlaylistUpdateComponent
33} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component'
34import { MyAccountVideoPlaylistsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlists.component'
28 35
29@NgModule({ 36@NgModule({
30 imports: [ 37 imports: [
@@ -57,7 +64,11 @@ import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-a
57 MyAccountServerBlocklistComponent, 64 MyAccountServerBlocklistComponent,
58 MyAccountHistoryComponent, 65 MyAccountHistoryComponent,
59 MyAccountNotificationsComponent, 66 MyAccountNotificationsComponent,
60 MyAccountNotificationPreferencesComponent 67 MyAccountNotificationPreferencesComponent,
68
69 MyAccountVideoPlaylistCreateComponent,
70 MyAccountVideoPlaylistUpdateComponent,
71 MyAccountVideoPlaylistsComponent
61 ], 72 ],
62 73
63 exports: [ 74 exports: [
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 7583fdee8..c5c5a8f66 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -74,6 +74,7 @@ export class AppComponent implements OnInit {
74 this.serverService.loadVideoLanguages() 74 this.serverService.loadVideoLanguages()
75 this.serverService.loadVideoLicences() 75 this.serverService.loadVideoLicences()
76 this.serverService.loadVideoPrivacies() 76 this.serverService.loadVideoPrivacies()
77 this.serverService.loadVideoPlaylistPrivacies()
77 78
78 // Do not display menu on small screens 79 // Do not display menu on small screens
79 if (this.screenService.isInSmallView()) { 80 if (this.screenService.isInSmallView()) {
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 10acf6e72..acaca8a01 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -9,17 +9,20 @@ import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos
9import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' 9import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n'
10import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 10import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
11import { sortBy } from '@app/shared/misc/utils' 11import { sortBy } from '@app/shared/misc/utils'
12import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
12 13
13@Injectable() 14@Injectable()
14export class ServerService { 15export class ServerService {
15 private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server/' 16 private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server/'
16 private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/' 17 private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/'
17 private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' 18 private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
19 private static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/'
18 private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' 20 private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
19 private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' 21 private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
20 22
21 configLoaded = new ReplaySubject<boolean>(1) 23 configLoaded = new ReplaySubject<boolean>(1)
22 videoPrivaciesLoaded = new ReplaySubject<boolean>(1) 24 videoPrivaciesLoaded = new ReplaySubject<boolean>(1)
25 videoPlaylistPrivaciesLoaded = new ReplaySubject<boolean>(1)
23 videoCategoriesLoaded = new ReplaySubject<boolean>(1) 26 videoCategoriesLoaded = new ReplaySubject<boolean>(1)
24 videoLicencesLoaded = new ReplaySubject<boolean>(1) 27 videoLicencesLoaded = new ReplaySubject<boolean>(1)
25 videoLanguagesLoaded = new ReplaySubject<boolean>(1) 28 videoLanguagesLoaded = new ReplaySubject<boolean>(1)
@@ -101,6 +104,7 @@ export class ServerService {
101 private videoLicences: Array<VideoConstant<number>> = [] 104 private videoLicences: Array<VideoConstant<number>> = []
102 private videoLanguages: Array<VideoConstant<string>> = [] 105 private videoLanguages: Array<VideoConstant<string>> = []
103 private videoPrivacies: Array<VideoConstant<VideoPrivacy>> = [] 106 private videoPrivacies: Array<VideoConstant<VideoPrivacy>> = []
107 private videoPlaylistPrivacies: Array<VideoConstant<VideoPlaylistPrivacy>> = []
104 108
105 constructor ( 109 constructor (
106 private http: HttpClient, 110 private http: HttpClient,
@@ -121,19 +125,28 @@ export class ServerService {
121 } 125 }
122 126
123 loadVideoCategories () { 127 loadVideoCategories () {
124 return this.loadVideoAttributeEnum('categories', this.videoCategories, this.videoCategoriesLoaded, true) 128 return this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'categories', this.videoCategories, this.videoCategoriesLoaded, true)
125 } 129 }
126 130
127 loadVideoLicences () { 131 loadVideoLicences () {
128 return this.loadVideoAttributeEnum('licences', this.videoLicences, this.videoLicencesLoaded) 132 return this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'licences', this.videoLicences, this.videoLicencesLoaded)
129 } 133 }
130 134
131 loadVideoLanguages () { 135 loadVideoLanguages () {
132 return this.loadVideoAttributeEnum('languages', this.videoLanguages, this.videoLanguagesLoaded, true) 136 return this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'languages', this.videoLanguages, this.videoLanguagesLoaded, true)
133 } 137 }
134 138
135 loadVideoPrivacies () { 139 loadVideoPrivacies () {
136 return this.loadVideoAttributeEnum('privacies', this.videoPrivacies, this.videoPrivaciesLoaded) 140 return this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'privacies', this.videoPrivacies, this.videoPrivaciesLoaded)
141 }
142
143 loadVideoPlaylistPrivacies () {
144 return this.loadAttributeEnum(
145 ServerService.BASE_VIDEO_PLAYLIST_URL,
146 'privacies',
147 this.videoPlaylistPrivacies,
148 this.videoPlaylistPrivaciesLoaded
149 )
137 } 150 }
138 151
139 getConfig () { 152 getConfig () {
@@ -156,7 +169,12 @@ export class ServerService {
156 return this.videoPrivacies 169 return this.videoPrivacies
157 } 170 }
158 171
159 private loadVideoAttributeEnum ( 172 getVideoPlaylistPrivacies () {
173 return this.videoPlaylistPrivacies
174 }
175
176 private loadAttributeEnum (
177 baseUrl: string,
160 attributeName: 'categories' | 'licences' | 'languages' | 'privacies', 178 attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
161 hashToPopulate: VideoConstant<string | number>[], 179 hashToPopulate: VideoConstant<string | number>[],
162 notifier: ReplaySubject<boolean>, 180 notifier: ReplaySubject<boolean>,
@@ -165,7 +183,7 @@ export class ServerService {
165 this.localeObservable 183 this.localeObservable
166 .pipe( 184 .pipe(
167 switchMap(translations => { 185 switchMap(translations => {
168 return this.http.get<{ [id: string]: string }>(ServerService.BASE_VIDEO_URL + attributeName) 186 return this.http.get<{ [id: string]: string }>(baseUrl + attributeName)
169 .pipe(map(data => ({ data, translations }))) 187 .pipe(map(data => ({ data, translations })))
170 }) 188 })
171 ) 189 )
diff --git a/client/src/app/shared/buttons/button.component.ts b/client/src/app/shared/buttons/button.component.ts
index a91e9c7eb..c2b69d31a 100644
--- a/client/src/app/shared/buttons/button.component.ts
+++ b/client/src/app/shared/buttons/button.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { GlobalIconName } from '@app/shared/icons/global-icon.component' 2import { GlobalIconName } from '@app/shared/images/global-icon.component'
3 3
4@Component({ 4@Component({
5 selector: 'my-button', 5 selector: 'my-button',
diff --git a/client/src/app/shared/forms/form-validators/index.ts b/client/src/app/shared/forms/form-validators/index.ts
index fdcbedb71..e3de3ae13 100644
--- a/client/src/app/shared/forms/form-validators/index.ts
+++ b/client/src/app/shared/forms/form-validators/index.ts
@@ -10,6 +10,7 @@ export * from './video-blacklist-validators.service'
10export * from './video-channel-validators.service' 10export * from './video-channel-validators.service'
11export * from './video-comment-validators.service' 11export * from './video-comment-validators.service'
12export * from './video-validators.service' 12export * from './video-validators.service'
13export * from './video-playlist-validators.service'
13export * from './video-captions-validators.service' 14export * from './video-captions-validators.service'
14export * from './video-change-ownership-validators.service' 15export * from './video-change-ownership-validators.service'
15export * from './video-accept-ownership-validators.service' 16export * from './video-accept-ownership-validators.service'
diff --git a/client/src/app/shared/forms/form-validators/video-playlist-validators.service.ts b/client/src/app/shared/forms/form-validators/video-playlist-validators.service.ts
new file mode 100644
index 000000000..726084b47
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/video-playlist-validators.service.ts
@@ -0,0 +1,52 @@
1import { I18n } from '@ngx-translate/i18n-polyfill'
2import { Validators } from '@angular/forms'
3import { Injectable } from '@angular/core'
4import { BuildFormValidator } from '@app/shared'
5
6@Injectable()
7export class VideoPlaylistValidatorsService {
8 readonly VIDEO_PLAYLIST_DISPLAY_NAME: BuildFormValidator
9 readonly VIDEO_PLAYLIST_PRIVACY: BuildFormValidator
10 readonly VIDEO_PLAYLIST_DESCRIPTION: BuildFormValidator
11 readonly VIDEO_PLAYLIST_CHANNEL_ID: BuildFormValidator
12
13 constructor (private i18n: I18n) {
14 this.VIDEO_PLAYLIST_DISPLAY_NAME = {
15 VALIDATORS: [
16 Validators.required,
17 Validators.minLength(1),
18 Validators.maxLength(120)
19 ],
20 MESSAGES: {
21 'required': this.i18n('Display name is required.'),
22 'minlength': this.i18n('Display name must be at least 1 character long.'),
23 'maxlength': this.i18n('Display name cannot be more than 120 characters long.')
24 }
25 }
26
27 this.VIDEO_PLAYLIST_PRIVACY = {
28 VALIDATORS: [
29 Validators.required
30 ],
31 MESSAGES: {
32 'required': this.i18n('Privacy is required.')
33 }
34 }
35
36 this.VIDEO_PLAYLIST_DESCRIPTION = {
37 VALIDATORS: [
38 Validators.minLength(3),
39 Validators.maxLength(1000)
40 ],
41 MESSAGES: {
42 'minlength': i18n('Description must be at least 3 characters long.'),
43 'maxlength': i18n('Description cannot be more than 1000 characters long.')
44 }
45 }
46
47 this.VIDEO_PLAYLIST_CHANNEL_ID = {
48 VALIDATORS: [ ],
49 MESSAGES: { }
50 }
51 }
52}
diff --git a/client/src/app/shared/icons/global-icon.component.html b/client/src/app/shared/images/global-icon.component.html
index e69de29bb..e69de29bb 100644
--- a/client/src/app/shared/icons/global-icon.component.html
+++ b/client/src/app/shared/images/global-icon.component.html
diff --git a/client/src/app/shared/icons/global-icon.component.scss b/client/src/app/shared/images/global-icon.component.scss
index 6805fb6f7..6805fb6f7 100644
--- a/client/src/app/shared/icons/global-icon.component.scss
+++ b/client/src/app/shared/images/global-icon.component.scss
diff --git a/client/src/app/shared/icons/global-icon.component.ts b/client/src/app/shared/images/global-icon.component.ts
index e8ada0324..e8ada0324 100644
--- a/client/src/app/shared/icons/global-icon.component.ts
+++ b/client/src/app/shared/images/global-icon.component.ts
diff --git a/client/src/app/videos/+video-edit/shared/video-image.component.html b/client/src/app/shared/images/image-upload.component.html
index c09c862c4..c09c862c4 100644
--- a/client/src/app/videos/+video-edit/shared/video-image.component.html
+++ b/client/src/app/shared/images/image-upload.component.html
diff --git a/client/src/app/videos/+video-edit/shared/video-image.component.scss b/client/src/app/shared/images/image-upload.component.scss
index b63963bca..b63963bca 100644
--- a/client/src/app/videos/+video-edit/shared/video-image.component.scss
+++ b/client/src/app/shared/images/image-upload.component.scss
diff --git a/client/src/app/videos/+video-edit/shared/video-image.component.ts b/client/src/app/shared/images/image-upload.component.ts
index a604cde90..2da1592ff 100644
--- a/client/src/app/videos/+video-edit/shared/video-image.component.ts
+++ b/client/src/app/shared/images/image-upload.component.ts
@@ -4,18 +4,18 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
4import { ServerService } from '@app/core' 4import { ServerService } from '@app/core'
5 5
6@Component({ 6@Component({
7 selector: 'my-video-image', 7 selector: 'my-image-upload',
8 styleUrls: [ './video-image.component.scss' ], 8 styleUrls: [ './image-upload.component.scss' ],
9 templateUrl: './video-image.component.html', 9 templateUrl: './image-upload.component.html',
10 providers: [ 10 providers: [
11 { 11 {
12 provide: NG_VALUE_ACCESSOR, 12 provide: NG_VALUE_ACCESSOR,
13 useExisting: forwardRef(() => VideoImageComponent), 13 useExisting: forwardRef(() => ImageUploadComponent),
14 multi: true 14 multi: true
15 } 15 }
16 ] 16 ]
17}) 17})
18export class VideoImageComponent implements ControlValueAccessor { 18export class ImageUploadComponent implements ControlValueAccessor {
19 @Input() inputLabel: string 19 @Input() inputLabel: string
20 @Input() inputName: string 20 @Input() inputName: string
21 @Input() previewWidth: string 21 @Input() previewWidth: string
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts
index 7cc6055c2..8a1d342c9 100644
--- a/client/src/app/shared/misc/utils.ts
+++ b/client/src/app/shared/misc/utils.ts
@@ -17,7 +17,7 @@ function getParameterByName (name: string, url: string) {
17 return decodeURIComponent(results[2].replace(/\+/g, ' ')) 17 return decodeURIComponent(results[2].replace(/\+/g, ' '))
18} 18}
19 19
20function populateAsyncUserVideoChannels (authService: AuthService, channel: { id: number, label: string, support: string }[]) { 20function populateAsyncUserVideoChannels (authService: AuthService, channel: { id: number, label: string, support?: string }[]) {
21 return new Promise(res => { 21 return new Promise(res => {
22 authService.userInformationLoaded 22 authService.userInformationLoaded
23 .subscribe( 23 .subscribe(
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 1c4e3df1a..60a7bd6e2 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -45,6 +45,7 @@ import {
45 VideoChangeOwnershipValidatorsService, 45 VideoChangeOwnershipValidatorsService,
46 VideoChannelValidatorsService, 46 VideoChannelValidatorsService,
47 VideoCommentValidatorsService, 47 VideoCommentValidatorsService,
48 VideoPlaylistValidatorsService,
48 VideoValidatorsService 49 VideoValidatorsService
49} from '@app/shared/forms' 50} from '@app/shared/forms'
50import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' 51import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
@@ -68,8 +69,11 @@ import { UserNotificationsComponent } from '@app/shared/users/user-notifications
68import { InstanceService } from '@app/shared/instance/instance.service' 69import { InstanceService } from '@app/shared/instance/instance.service'
69import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer' 70import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer'
70import { ConfirmComponent } from '@app/shared/confirm/confirm.component' 71import { ConfirmComponent } from '@app/shared/confirm/confirm.component'
71import { GlobalIconComponent } from '@app/shared/icons/global-icon.component'
72import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component' 72import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
73import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
74import { ImageUploadComponent } from '@app/shared/images/image-upload.component'
75import { GlobalIconComponent } from '@app/shared/images/global-icon.component'
76import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component'
73 77
74@NgModule({ 78@NgModule({
75 imports: [ 79 imports: [
@@ -92,8 +96,11 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
92 declarations: [ 96 declarations: [
93 LoaderComponent, 97 LoaderComponent,
94 SmallLoaderComponent, 98 SmallLoaderComponent,
99
95 VideoThumbnailComponent, 100 VideoThumbnailComponent,
96 VideoMiniatureComponent, 101 VideoMiniatureComponent,
102 VideoPlaylistMiniatureComponent,
103
97 FeedComponent, 104 FeedComponent,
98 ButtonComponent, 105 ButtonComponent,
99 DeleteButtonComponent, 106 DeleteButtonComponent,
@@ -116,7 +123,9 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
116 TopMenuDropdownComponent, 123 TopMenuDropdownComponent,
117 UserNotificationsComponent, 124 UserNotificationsComponent,
118 ConfirmComponent, 125 ConfirmComponent,
119 GlobalIconComponent 126
127 GlobalIconComponent,
128 ImageUploadComponent
120 ], 129 ],
121 130
122 exports: [ 131 exports: [
@@ -138,8 +147,11 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
138 147
139 LoaderComponent, 148 LoaderComponent,
140 SmallLoaderComponent, 149 SmallLoaderComponent,
150
141 VideoThumbnailComponent, 151 VideoThumbnailComponent,
142 VideoMiniatureComponent, 152 VideoMiniatureComponent,
153 VideoPlaylistMiniatureComponent,
154
143 FeedComponent, 155 FeedComponent,
144 ButtonComponent, 156 ButtonComponent,
145 DeleteButtonComponent, 157 DeleteButtonComponent,
@@ -159,7 +171,9 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
159 TopMenuDropdownComponent, 171 TopMenuDropdownComponent,
160 UserNotificationsComponent, 172 UserNotificationsComponent,
161 ConfirmComponent, 173 ConfirmComponent,
174
162 GlobalIconComponent, 175 GlobalIconComponent,
176 ImageUploadComponent,
163 177
164 NumberFormatterPipe, 178 NumberFormatterPipe,
165 ObjectLengthPipe, 179 ObjectLengthPipe,
@@ -177,6 +191,7 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
177 VideoService, 191 VideoService,
178 AccountService, 192 AccountService,
179 VideoChannelService, 193 VideoChannelService,
194 VideoPlaylistService,
180 VideoCaptionService, 195 VideoCaptionService,
181 VideoImportService, 196 VideoImportService,
182 UserSubscriptionService, 197 UserSubscriptionService,
@@ -186,6 +201,7 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
186 LoginValidatorsService, 201 LoginValidatorsService,
187 ResetPasswordValidatorsService, 202 ResetPasswordValidatorsService,
188 UserValidatorsService, 203 UserValidatorsService,
204 VideoPlaylistValidatorsService,
189 VideoAbuseValidatorsService, 205 VideoAbuseValidatorsService,
190 VideoChannelValidatorsService, 206 VideoChannelValidatorsService,
191 VideoCommentValidatorsService, 207 VideoCommentValidatorsService,
diff --git a/client/src/app/shared/video-playlist/video-playlist-miniature.component.html b/client/src/app/shared/video-playlist/video-playlist-miniature.component.html
new file mode 100644
index 000000000..1a39f5fe5
--- /dev/null
+++ b/client/src/app/shared/video-playlist/video-playlist-miniature.component.html
@@ -0,0 +1,22 @@
1<div class="miniature">
2 <a
3 [routerLink]="[ '/videos/watch' ]" [attr.title]="playlist.displayName"
4 class="miniature-thumbnail"
5 >
6 <img alt="" [attr.aria-labelledby]="playlist.displayName" [attr.src]="playlist.thumbnailUrl" />
7
8 <div class="miniature-playlist-info-overlay">
9 <ng-container i18n>{playlist.videosLength, plural, =0 {No videos} =1 {1 video} other {{{playlist.videosLength}} videos}}</ng-container>
10 </div>
11
12 <div class="play-overlay">
13 <div class="icon"></div>
14 </div>
15 </a>
16
17 <div class="miniature-bottom">
18 <a tabindex="-1" class="miniature-name" [routerLink]="[ '/videos/watch' ]" [attr.title]="playlist.displayName">
19 {{ playlist.displayName }}
20 </a>
21 </div>
22</div>
diff --git a/client/src/app/shared/video-playlist/video-playlist-miniature.component.scss b/client/src/app/shared/video-playlist/video-playlist-miniature.component.scss
new file mode 100644
index 000000000..a47206577
--- /dev/null
+++ b/client/src/app/shared/video-playlist/video-playlist-miniature.component.scss
@@ -0,0 +1,34 @@
1@import '_variables';
2@import '_mixins';
3@import '_miniature';
4
5.miniature {
6 display: inline-block;
7
8 .miniature-thumbnail {
9 @include miniature-thumbnail;
10
11 .miniature-playlist-info-overlay {
12 @include static-thumbnail-overlay;
13
14 position: absolute;
15 right: 0;
16 bottom: 0;
17 height: $video-thumbnail-height;
18 padding: 0 10px;
19 display: flex;
20 align-items: center;
21 font-size: 15px;
22 }
23 }
24
25 .miniature-bottom {
26 width: 200px;
27 margin-top: 2px;
28 line-height: normal;
29
30 .miniature-name {
31 @include miniature-name;
32 }
33 }
34}
diff --git a/client/src/app/shared/video-playlist/video-playlist-miniature.component.ts b/client/src/app/shared/video-playlist/video-playlist-miniature.component.ts
new file mode 100644
index 000000000..b3bba7c87
--- /dev/null
+++ b/client/src/app/shared/video-playlist/video-playlist-miniature.component.ts
@@ -0,0 +1,11 @@
1import { Component, Input } from '@angular/core'
2import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
3
4@Component({
5 selector: 'my-video-playlist-miniature',
6 styleUrls: [ './video-playlist-miniature.component.scss' ],
7 templateUrl: './video-playlist-miniature.component.html'
8})
9export class VideoPlaylistMiniatureComponent {
10 @Input() playlist: VideoPlaylist
11}
diff --git a/client/src/app/shared/video-playlist/video-playlist.model.ts b/client/src/app/shared/video-playlist/video-playlist.model.ts
new file mode 100644
index 000000000..9d0b02789
--- /dev/null
+++ b/client/src/app/shared/video-playlist/video-playlist.model.ts
@@ -0,0 +1,74 @@
1import {
2 VideoChannelSummary,
3 VideoConstant,
4 VideoPlaylist as ServerVideoPlaylist,
5 VideoPlaylistPrivacy,
6 VideoPlaylistType
7} from '../../../../../shared/models/videos'
8import { AccountSummary, peertubeTranslate } from '@shared/models'
9import { Actor } from '@app/shared/actor/actor.model'
10import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
11
12export class VideoPlaylist implements ServerVideoPlaylist {
13 id: number
14 uuid: string
15 isLocal: boolean
16
17 displayName: string
18 description: string
19 privacy: VideoConstant<VideoPlaylistPrivacy>
20
21 thumbnailPath: string
22
23 videosLength: number
24
25 type: VideoConstant<VideoPlaylistType>
26
27 createdAt: Date | string
28 updatedAt: Date | string
29
30 ownerAccount: AccountSummary
31 videoChannel?: VideoChannelSummary
32
33 thumbnailUrl: string
34
35 ownerBy: string
36 ownerAvatarUrl: string
37
38 videoChannelBy?: string
39 videoChannelAvatarUrl?: string
40
41 constructor (hash: ServerVideoPlaylist, translations: {}) {
42 const absoluteAPIUrl = getAbsoluteAPIUrl()
43
44 this.id = hash.id
45 this.uuid = hash.uuid
46 this.isLocal = hash.isLocal
47
48 this.displayName = hash.displayName
49 this.description = hash.description
50 this.privacy = hash.privacy
51
52 this.thumbnailPath = hash.thumbnailPath
53 this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath
54
55 this.videosLength = hash.videosLength
56
57 this.type = hash.type
58
59 this.createdAt = new Date(hash.createdAt)
60 this.updatedAt = new Date(hash.updatedAt)
61
62 this.ownerAccount = hash.ownerAccount
63 this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
64 this.ownerAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.ownerAccount)
65
66 if (hash.videoChannel) {
67 this.videoChannel = hash.videoChannel
68 this.videoChannelBy = Actor.CREATE_BY_STRING(hash.videoChannel.name, hash.videoChannel.host)
69 this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.videoChannel)
70 }
71
72 this.privacy.label = peertubeTranslate(this.privacy.label, translations)
73 }
74}
diff --git a/client/src/app/shared/video-playlist/video-playlist.service.ts b/client/src/app/shared/video-playlist/video-playlist.service.ts
new file mode 100644
index 000000000..8b66e122c
--- /dev/null
+++ b/client/src/app/shared/video-playlist/video-playlist.service.ts
@@ -0,0 +1,108 @@
1import { catchError, map, switchMap } from 'rxjs/operators'
2import { Injectable } from '@angular/core'
3import { Observable } from 'rxjs'
4import { RestExtractor } from '../rest/rest-extractor.service'
5import { HttpClient } from '@angular/common/http'
6import { ResultList } from '../../../../../shared'
7import { environment } from '../../../environments/environment'
8import { VideoPlaylist as VideoPlaylistServerModel } from '@shared/models/videos/playlist/video-playlist.model'
9import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
10import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
11import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
12import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model'
13import { objectToFormData } from '@app/shared/misc/utils'
14import { ServerService } from '@app/core'
15import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
16import { AccountService } from '@app/shared/account/account.service'
17import { Account } from '@app/shared/account/account.model'
18
19@Injectable()
20export class VideoPlaylistService {
21 static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/'
22
23 constructor (
24 private authHttp: HttpClient,
25 private serverService: ServerService,
26 private restExtractor: RestExtractor
27 ) { }
28
29 listChannelPlaylists (videoChannel: VideoChannel): Observable<ResultList<VideoPlaylist>> {
30 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/video-playlists'
31
32 return this.authHttp.get<ResultList<VideoPlaylist>>(url)
33 .pipe(
34 switchMap(res => this.extractPlaylists(res)),
35 catchError(err => this.restExtractor.handleError(err))
36 )
37 }
38
39 listAccountPlaylists (account: Account): Observable<ResultList<VideoPlaylist>> {
40 const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists'
41
42 return this.authHttp.get<ResultList<VideoPlaylist>>(url)
43 .pipe(
44 switchMap(res => this.extractPlaylists(res)),
45 catchError(err => this.restExtractor.handleError(err))
46 )
47 }
48
49 getVideoPlaylist (id: string | number) {
50 const url = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + id
51
52 return this.authHttp.get<VideoPlaylist>(url)
53 .pipe(
54 switchMap(res => this.extractPlaylist(res)),
55 catchError(err => this.restExtractor.handleError(err))
56 )
57 }
58
59 createVideoPlaylist (body: VideoPlaylistCreate) {
60 const data = objectToFormData(body)
61
62 return this.authHttp.post(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL, data)
63 .pipe(
64 map(this.restExtractor.extractDataBool),
65 catchError(err => this.restExtractor.handleError(err))
66 )
67 }
68
69 updateVideoPlaylist (videoPlaylist: VideoPlaylist, body: VideoPlaylistUpdate) {
70 const data = objectToFormData(body)
71
72 return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id, data)
73 .pipe(
74 map(this.restExtractor.extractDataBool),
75 catchError(err => this.restExtractor.handleError(err))
76 )
77 }
78
79 removeVideoPlaylist (videoPlaylist: VideoPlaylist) {
80 return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id)
81 .pipe(
82 map(this.restExtractor.extractDataBool),
83 catchError(err => this.restExtractor.handleError(err))
84 )
85 }
86
87 extractPlaylists (result: ResultList<VideoPlaylistServerModel>) {
88 return this.serverService.localeObservable
89 .pipe(
90 map(translations => {
91 const playlistsJSON = result.data
92 const total = result.total
93 const playlists: VideoPlaylist[] = []
94
95 for (const playlistJSON of playlistsJSON) {
96 playlists.push(new VideoPlaylist(playlistJSON, translations))
97 }
98
99 return { data: playlists, total }
100 })
101 )
102 }
103
104 extractPlaylist (playlist: VideoPlaylistServerModel) {
105 return this.serverService.localeObservable
106 .pipe(map(translations => new VideoPlaylist(playlist, translations)))
107 }
108}
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss
index 292ede698..65842af35 100644
--- a/client/src/app/shared/video/abstract-video-list.scss
+++ b/client/src/app/shared/video/abstract-video-list.scss
@@ -1,4 +1,5 @@
1@import '_mixins'; 1@import '_mixins';
2@import '_miniature';
2 3
3.videos { 4.videos {
4 text-align: center; 5 text-align: center;
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss
index c118fc3a1..7d857a74e 100644
--- a/client/src/app/shared/video/video-miniature.component.scss
+++ b/client/src/app/shared/video/video-miniature.component.scss
@@ -1,5 +1,6 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3@import '_miniature';
3 4
4.video-miniature { 5.video-miniature {
5 display: inline-block; 6 display: inline-block;
@@ -14,26 +15,7 @@
14 line-height: normal; 15 line-height: normal;
15 16
16 .video-miniature-name { 17 .video-miniature-name {
17 @include ellipsis-multiline( 18 @include miniature-name;
18 $font-size: 1rem,
19 $line-height: 1,
20 $lines-to-show: 2
21 );
22 transition: color 0.2s;
23 font-size: 16px;
24 font-weight: $font-semibold;
25 color: var(--mainForegroundColor);
26 margin-top: 5px;
27 margin-bottom: 5px;
28
29 &:hover {
30 text-decoration: none;
31 }
32
33 &.blur-filter {
34 filter: blur(3px);
35 padding-left: 4px;
36 }
37 } 19 }
38 20
39 .video-miniature-created-at-views { 21 .video-miniature-created-at-views {
diff --git a/client/src/app/shared/video/video-thumbnail.component.html b/client/src/app/shared/video/video-thumbnail.component.html
index a15df725e..a6757fc4a 100644
--- a/client/src/app/shared/video/video-thumbnail.component.html
+++ b/client/src/app/shared/video/video-thumbnail.component.html
@@ -4,9 +4,11 @@
4> 4>
5 <img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" /> 5 <img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
6 6
7 <div class="video-thumbnail-overlay">{{ video.durationLabel }}</div> 7 <div class="video-thumbnail-duration-overlay">{{ video.durationLabel }}</div>
8 8
9 <div class="play-overlay"></div> 9 <div class="play-overlay">
10 <div class="icon"></div>
11 </div>
10 12
11 <div class="progress-bar" *ngIf="video.userHistory?.currentTime"> 13 <div class="progress-bar" *ngIf="video.userHistory?.currentTime">
12 <div [ngStyle]="{ 'width.%': getProgressPercent() }"></div> 14 <div [ngStyle]="{ 'width.%': getProgressPercent() }"></div>
diff --git a/client/src/app/shared/video/video-thumbnail.component.scss b/client/src/app/shared/video/video-thumbnail.component.scss
index b9fd9182f..0113427a3 100644
--- a/client/src/app/shared/video/video-thumbnail.component.scss
+++ b/client/src/app/shared/video/video-thumbnail.component.scss
@@ -1,66 +1,9 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3@import '_miniature';
4$play-overlay-transition: 0.2s ease;
5$play-overlay-height: 26px;
6$play-overlay-width: 18px;
7 4
8.video-thumbnail { 5.video-thumbnail {
9 @include disable-outline; 6 @include miniature-thumbnail;
10
11 display: inline-block;
12 position: relative;
13 border-radius: 3px;
14 overflow: hidden;
15 width: $video-thumbnail-width;
16 height: $video-thumbnail-height;
17 background-color: #ececec;
18 transition: filter $play-overlay-transition;
19
20 &:hover {
21 text-decoration: none !important;
22
23 filter: brightness(85%);
24
25 .play-overlay {
26 opacity: 1;
27
28 transform: translate(-50%, -50%) scale(1);
29 }
30 }
31
32 &.focus-visible {
33 box-shadow: 0 0 0 2px var(--mainColor);
34 }
35
36 img {
37 width: $video-thumbnail-width;
38 height: $video-thumbnail-height;
39
40 &.blur-filter {
41 filter: blur(5px);
42 transform : scale(1.03);
43 }
44 }
45
46 .play-overlay {
47 width: 0;
48 height: 0;
49
50 position: absolute;
51 left: 50%;
52 top: 50%;
53 transform: translate(-50%, -50%) scale(0.5);
54
55 transition: all $play-overlay-transition;
56
57 border-top: ($play-overlay-height / 2) solid transparent;
58 border-bottom: ($play-overlay-height / 2) solid transparent;
59
60 border-left: $play-overlay-width solid rgba(255, 255, 255, 0.95);
61
62 opacity: 0;
63 }
64 7
65 .progress-bar { 8 .progress-bar {
66 height: 3px; 9 height: 3px;
@@ -75,16 +18,15 @@ $play-overlay-width: 18px;
75 } 18 }
76 } 19 }
77 20
78 .video-thumbnail-overlay { 21 .video-thumbnail-duration-overlay {
22 @include static-thumbnail-overlay;
23
79 position: absolute; 24 position: absolute;
80 right: 5px; 25 right: 5px;
81 bottom: 5px; 26 bottom: 5px;
82 display: inline-block; 27 padding: 0 5px;
83 background-color: rgba(0, 0, 0, 0.7); 28 border-radius: 3px;
84 color: #fff;
85 font-size: 12px; 29 font-size: 12px;
86 font-weight: $font-bold; 30 font-weight: $font-bold;
87 border-radius: 3px;
88 padding: 0 5px;
89 } 31 }
90} 32}
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index 460c09258..c936a8207 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -117,9 +117,8 @@ export class Video implements VideoServerModel {
117 this.privacy.label = peertubeTranslate(this.privacy.label, translations) 117 this.privacy.label = peertubeTranslate(this.privacy.label, translations)
118 118
119 this.scheduledUpdate = hash.scheduledUpdate 119 this.scheduledUpdate = hash.scheduledUpdate
120 this.originallyPublishedAt = hash.originallyPublishedAt ? 120 this.originallyPublishedAt = hash.originallyPublishedAt ? new Date(hash.originallyPublishedAt.toString()) : null
121 new Date(hash.originallyPublishedAt.toString()) 121
122 : null
123 if (this.state) this.state.label = peertubeTranslate(this.state.label, translations) 122 if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
124 123
125 this.blacklisted = hash.blacklisted 124 this.blacklisted = hash.blacklisted
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html
index 1be1084ad..99695204d 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html
@@ -188,17 +188,17 @@
188 <div class="row advanced-settings"> 188 <div class="row advanced-settings">
189 <div class="col-md-12 col-xl-8"> 189 <div class="col-md-12 col-xl-8">
190 <div class="form-group"> 190 <div class="form-group">
191 <my-video-image 191 <my-image-upload
192 i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile" 192 i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
193 previewWidth="200px" previewHeight="110px" 193 previewWidth="200px" previewHeight="110px"
194 ></my-video-image> 194 ></my-image-upload>
195 </div> 195 </div>
196 196
197 <div class="form-group"> 197 <div class="form-group">
198 <my-video-image 198 <my-image-upload
199 i18n-inputLabel inputLabel="Upload preview" inputName="previewfile" formControlName="previewfile" 199 i18n-inputLabel inputLabel="Upload preview" inputName="previewfile" formControlName="previewfile"
200 previewWidth="360px" previewHeight="200px" 200 previewWidth="360px" previewHeight="200px"
201 ></my-video-image> 201 ></my-image-upload>
202 </div> 202 </div>
203 203
204 <div class="form-group"> 204 <div class="form-group">
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.module.ts b/client/src/app/videos/+video-edit/shared/video-edit.module.ts
index f441d3fde..39b6daa93 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.module.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.module.ts
@@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'
2import { TagInputModule } from 'ngx-chips' 2import { TagInputModule } from 'ngx-chips'
3import { SharedModule } from '../../../shared/' 3import { SharedModule } from '../../../shared/'
4import { VideoEditComponent } from './video-edit.component' 4import { VideoEditComponent } from './video-edit.component'
5import { VideoImageComponent } from './video-image.component'
6import { CalendarModule } from 'primeng/components/calendar/calendar' 5import { CalendarModule } from 'primeng/components/calendar/calendar'
7import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 6import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
8 7
@@ -16,7 +15,6 @@ import { VideoCaptionAddModalComponent } from './video-caption-add-modal.compone
16 15
17 declarations: [ 16 declarations: [
18 VideoEditComponent, 17 VideoEditComponent,
19 VideoImageComponent,
20 VideoCaptionAddModalComponent 18 VideoCaptionAddModalComponent
21 ], 19 ],
22 20
diff --git a/client/src/app/videos/video-list/video-overview.component.scss b/client/src/app/videos/video-list/video-overview.component.scss
index aff45c072..42b542233 100644
--- a/client/src/app/videos/video-list/video-overview.component.scss
+++ b/client/src/app/videos/video-list/video-overview.component.scss
@@ -1,5 +1,6 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3@import '_miniature';
3 4
4.section { 5.section {
5 padding-top: 10px; 6 padding-top: 10px;
@@ -50,4 +51,4 @@
50 .section { 51 .section {
51 @include video-miniature-small-screen; 52 @include video-miniature-small-screen;
52 } 53 }
53} \ No newline at end of file 54}
diff --git a/client/src/sass/include/_miniature.scss b/client/src/sass/include/_miniature.scss
new file mode 100644
index 000000000..36d4e84d3
--- /dev/null
+++ b/client/src/sass/include/_miniature.scss
@@ -0,0 +1,133 @@
1@import '_variables';
2@import '_mixins';
3
4@mixin miniature-name {
5 @include ellipsis-multiline(
6 $font-size: 1rem,
7 $line-height: 1,
8 $lines-to-show: 2
9 );
10 transition: color 0.2s;
11 font-size: 16px;
12 font-weight: $font-semibold;
13 color: var(--mainForegroundColor);
14 margin-top: 5px;
15 margin-bottom: 5px;
16
17 &:hover {
18 text-decoration: none;
19 }
20
21 &.blur-filter {
22 filter: blur(3px);
23 padding-left: 4px;
24 }
25}
26
27$play-overlay-transition: 0.2s ease;
28$play-overlay-height: 26px;
29$play-overlay-width: 18px;
30
31@mixin miniature-thumbnail {
32 @include disable-outline;
33
34 display: inline-block;
35 position: relative;
36 border-radius: 3px;
37 overflow: hidden;
38 width: $video-thumbnail-width;
39 height: $video-thumbnail-height;
40 background-color: #ececec;
41 transition: filter $play-overlay-transition;
42
43 .play-overlay {
44 position: absolute;
45 right: 0;
46 bottom: 0;
47
48 width: $video-thumbnail-width;
49 height: $video-thumbnail-height;
50 opacity: 0;
51 background-color: rgba(0, 0, 0, 0.7);
52
53 &, .icon {
54 transition: all $play-overlay-transition;
55 }
56
57 .icon {
58 width: 0;
59 height: 0;
60
61 position: absolute;
62 left: 50%;
63 top: 50%;
64 transform: translate(-50%, -50%) scale(0.5);
65
66 border-top: ($play-overlay-height / 2) solid transparent;
67 border-bottom: ($play-overlay-height / 2) solid transparent;
68
69 border-left: $play-overlay-width solid rgba(255, 255, 255, 0.95);
70 }
71 }
72
73 &:hover {
74 text-decoration: none !important;
75
76 .play-overlay {
77 opacity: 1;
78
79 .icon {
80 transform: translate(-50%, -50%) scale(1);
81 }
82 }
83 }
84
85 &.focus-visible {
86 box-shadow: 0 0 0 2px var(--mainColor);
87 }
88
89 img {
90 width: $video-thumbnail-width;
91 height: $video-thumbnail-height;
92
93 &.blur-filter {
94 filter: blur(5px);
95 transform : scale(1.03);
96 }
97 }
98}
99
100@mixin static-thumbnail-overlay {
101 display: inline-block;
102 background-color: rgba(0, 0, 0, 0.7);
103 color: #fff;
104}
105
106@mixin video-miniature-small-screen {
107 text-align: center;
108
109 /deep/ .video-miniature {
110 padding-right: 0;
111 height: auto;
112 width: 100%;
113 margin-bottom: 20px;
114
115 .video-miniature-information {
116 width: 100% !important;
117
118 span {
119 width: 100%;
120 }
121 }
122
123 .video-thumbnail {
124 width: 100%;
125 height: auto;
126
127 img {
128 width: 100%;
129 height: auto;
130 }
131 }
132 }
133}
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index c2e200a14..59b2f42a5 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -516,31 +516,3 @@
516 } 516 }
517} 517}
518 518
519@mixin video-miniature-small-screen {
520 text-align: center;
521
522 /deep/ .video-miniature {
523 padding-right: 0;
524 height: auto;
525 width: 100%;
526 margin-bottom: 20px;
527
528 .video-miniature-information {
529 width: 100% !important;
530
531 span {
532 width: 100%;
533 }
534 }
535
536 .video-thumbnail {
537 width: 100%;
538 height: auto;
539
540 img {
541 width: 100%;
542 height: auto;
543 }
544 }
545 }
546}
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 3f9986f8a..a0fbc27c6 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -28,6 +28,7 @@
28 "baseUrl": "src", 28 "baseUrl": "src",
29 "paths": { 29 "paths": {
30 "@app/*": [ "app/*" ], 30 "@app/*": [ "app/*" ],
31 "@shared/*": [ "../../shared/*" ],
31 "video.js": [ "../node_modules/video.js/dist/alt/video.core.js" ], 32 "video.js": [ "../node_modules/video.js/dist/alt/video.core.js" ],
32 "fs": [ "./shims/noop" ], 33 "fs": [ "./shims/noop" ],
33 "http": [ "./shims/http" ], 34 "http": [ "./shims/http" ],
@@ -41,11 +42,14 @@
41 "strictInjectionParameters": true, 42 "strictInjectionParameters": true,
42 "fullTemplateTypeCheck": true 43 "fullTemplateTypeCheck": true
43 }, 44 },
45 "include": [
46 "../../shared"
47 ],
44 "exclude": [ 48 "exclude": [
49 "../../node_modules",
45 "../node_modules", 50 "../node_modules",
46 "node_modules", 51 "../dist",
47 "dist", 52 "../../server",
48 "../server", 53 "../src/**/*.spec.ts"
49 "src/**/*.spec.ts"
50 ] 54 ]
51} 55}
diff --git a/scripts/i18n/create-custom-files.ts b/scripts/i18n/create-custom-files.ts
index 40c420825..95897afa3 100755
--- a/scripts/i18n/create-custom-files.ts
+++ b/scripts/i18n/create-custom-files.ts
@@ -5,7 +5,7 @@ import {
5 buildLanguages, 5 buildLanguages,
6 VIDEO_CATEGORIES, 6 VIDEO_CATEGORIES,
7 VIDEO_IMPORT_STATES, 7 VIDEO_IMPORT_STATES,
8 VIDEO_LICENCES, 8 VIDEO_LICENCES, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PLAYLIST_TYPES,
9 VIDEO_PRIVACIES, 9 VIDEO_PRIVACIES,
10 VIDEO_STATES 10 VIDEO_STATES
11} from '../../server/initializers/constants' 11} from '../../server/initializers/constants'
@@ -46,6 +46,8 @@ values(VIDEO_CATEGORIES)
46 .concat(values(VIDEO_PRIVACIES)) 46 .concat(values(VIDEO_PRIVACIES))
47 .concat(values(VIDEO_STATES)) 47 .concat(values(VIDEO_STATES))
48 .concat(values(VIDEO_IMPORT_STATES)) 48 .concat(values(VIDEO_IMPORT_STATES))
49 .concat(values(VIDEO_PLAYLIST_PRIVACIES))
50 .concat(values(VIDEO_PLAYLIST_TYPES))
49 .concat([ 51 .concat([
50 'This video does not exist.', 52 'This video does not exist.',
51 'We cannot fetch the video. Please try again later.', 53 'We cannot fetch the video. Please try again later.',
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 4cbb87ab5..54c390540 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -642,7 +642,7 @@ let STATIC_MAX_AGE = '2h'
642// Videos thumbnail size 642// Videos thumbnail size
643const THUMBNAILS_SIZE = { 643const THUMBNAILS_SIZE = {
644 width: 223, 644 width: 223,
645 height: 112 645 height: 122
646} 646}
647const PREVIEWS_SIZE = { 647const PREVIEWS_SIZE = {
648 width: 560, 648 width: 560,
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
index fa26e2336..22b8b8ff1 100644
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -344,6 +344,7 @@ function getCommonPlaylistEditAttributes () {
344 .custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'), 344 .custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'),
345 body('videoChannelId') 345 body('videoChannelId')
346 .optional() 346 .optional()
347 .customSanitizer(toValueOrNull)
347 .toInt() 348 .toInt()
348 ] as (ValidationChain | express.Handler)[] 349 ] as (ValidationChain | express.Handler)[]
349} 350}
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts
index 056ae06da..9cf861048 100644
--- a/shared/models/videos/index.ts
+++ b/shared/models/videos/index.ts
@@ -11,6 +11,13 @@ export * from './blacklist/video-blacklist-update.model'
11export * from './channel/video-channel-create.model' 11export * from './channel/video-channel-create.model'
12export * from './channel/video-channel-update.model' 12export * from './channel/video-channel-update.model'
13export * from './channel/video-channel.model' 13export * from './channel/video-channel.model'
14export * from './playlist/video-playlist-create.model'
15export * from './playlist/video-playlist-element-create.model'
16export * from './playlist/video-playlist-element-update.model'
17export * from './playlist/video-playlist-privacy.model'
18export * from './playlist/video-playlist-type.model'
19export * from './playlist/video-playlist-update.model'
20export * from './playlist/video-playlist.model'
14export * from './video-change-ownership.model' 21export * from './video-change-ownership.model'
15export * from './video-change-ownership-create.model' 22export * from './video-change-ownership-create.model'
16export * from './video-create.model' 23export * from './video-create.model'
@@ -27,4 +34,4 @@ export * from './caption/video-caption-update.model'
27export * from './import/video-import-create.model' 34export * from './import/video-import-create.model'
28export * from './import/video-import-state.enum' 35export * from './import/video-import-state.enum'
29export * from './import/video-import.model' 36export * from './import/video-import.model'
30export { VideoConstant } from './video-constant.model' 37export * from './video-constant.model'
diff --git a/shared/models/videos/playlist/video-playlist.model.ts b/shared/models/videos/playlist/video-playlist.model.ts
index 7fec0e42b..c0941727a 100644
--- a/shared/models/videos/playlist/video-playlist.model.ts
+++ b/shared/models/videos/playlist/video-playlist.model.ts
@@ -21,6 +21,6 @@ export interface VideoPlaylist {
21 createdAt: Date | string 21 createdAt: Date | string
22 updatedAt: Date | string 22 updatedAt: Date | string
23 23
24 ownerAccount?: AccountSummary 24 ownerAccount: AccountSummary
25 videoChannel?: VideoChannelSummary 25 videoChannel?: VideoChannelSummary
26} 26}
diff --git a/shared/utils/videos/video-playlists.ts b/shared/utils/videos/video-playlists.ts
index 4af52ec0f..b84b21623 100644
--- a/shared/utils/videos/video-playlists.ts
+++ b/shared/utils/videos/video-playlists.ts
@@ -265,9 +265,21 @@ async function checkPlaylistFilesWereRemoved (
265 } 265 }
266} 266}
267 267
268function getVideoPlaylistPrivacies (url: string) {
269 const path = '/api/v1/video-playlists/privacies'
270
271 return makeGetRequest({
272 url,
273 path,
274 statusCodeExpected: 200
275 })
276}
277
268// --------------------------------------------------------------------------- 278// ---------------------------------------------------------------------------
269 279
270export { 280export {
281 getVideoPlaylistPrivacies,
282
271 getVideoPlaylistsList, 283 getVideoPlaylistsList,
272 getVideoChannelPlaylistsList, 284 getVideoChannelPlaylistsList,
273 getAccountPlaylistsList, 285 getAccountPlaylistsList,