diff options
author | Chocobozzz <me@florianbigard.com> | 2019-03-06 15:36:44 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2019-03-18 11:17:59 +0100 |
commit | 830b4faff15fb9c81d88e8e69fcdf94aad32bef8 (patch) | |
tree | 53de6c9e30ce88734b4bdda62016e0498fe78491 /client/src | |
parent | d4c9f45b31eda0b7a391ddc83eb290ca5cba311f (diff) | |
download | PeerTube-830b4faff15fb9c81d88e8e69fcdf94aad32bef8.tar.gz PeerTube-830b4faff15fb9c81d88e8e69fcdf94aad32bef8.tar.zst PeerTube-830b4faff15fb9c81d88e8e69fcdf94aad32bef8.zip |
Add/update/delete/list my playlists
Diffstat (limited to 'client/src')
41 files changed, 1042 insertions, 145 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 | |||
15 | import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component' | 15 | import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component' |
16 | import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component' | 16 | import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component' |
17 | import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component' | 17 | import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component' |
18 | import { MyAccountVideoPlaylistsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlists.component' | ||
19 | import { | ||
20 | MyAccountVideoPlaylistCreateComponent | ||
21 | } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component' | ||
22 | import { | ||
23 | MyAccountVideoPlaylistUpdateComponent | ||
24 | } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component' | ||
18 | 25 | ||
19 | const myAccountRoutes: Routes = [ | 26 | const 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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 3 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
5 | import { UserSubscriptionService } from '@app/shared/user-subscription' | 4 | import { UserSubscriptionService } from '@app/shared/user-subscription' |
6 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 5 | import { 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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Router } from '@angular/router' | ||
3 | import { AuthService, Notifier, ServerService } from '@app/core' | ||
4 | import { MyAccountVideoPlaylistEdit } from './my-account-video-playlist-edit' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
7 | import { VideoPlaylistValidatorsService } from '@app/shared' | ||
8 | import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' | ||
9 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | ||
10 | import { VideoConstant } from '@shared/models' | ||
11 | import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' | ||
12 | import { 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 | }) | ||
19 | export 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 | |||
8 | input[type=text] { | ||
9 | @include peertube-input-text(340px); | ||
10 | |||
11 | display: block; | ||
12 | } | ||
13 | |||
14 | textarea { | ||
15 | @include peertube-textarea(500px, 150px); | ||
16 | |||
17 | display: block; | ||
18 | } | ||
19 | |||
20 | .peertube-select-container { | ||
21 | @include peertube-select-container(340px); | ||
22 | } | ||
23 | |||
24 | input[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 @@ | |||
1 | import { FormReactive } from '@app/shared' | ||
2 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | ||
3 | import { ServerService } from '@app/core' | ||
4 | import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' | ||
5 | |||
6 | export 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 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { AuthService, Notifier, ServerService } from '@app/core' | ||
4 | import { Subscription } from 'rxjs' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
7 | import { MyAccountVideoPlaylistEdit } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-edit' | ||
8 | import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' | ||
9 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | ||
10 | import { VideoPlaylistValidatorsService } from '@app/shared' | ||
11 | import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model' | ||
12 | import { VideoConstant } from '@shared/models' | ||
13 | import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' | ||
14 | import { 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 | }) | ||
21 | export 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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Notifier } from '@app/core' | ||
3 | import { AuthService } from '../../core/auth' | ||
4 | import { ConfirmService } from '../../core/confirm' | ||
5 | import { User } from '@app/shared' | ||
6 | import { flatMap } from 'rxjs/operators' | ||
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
8 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | ||
9 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | ||
10 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | ||
11 | import { 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 | }) | ||
18 | export 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 | |||
25 | import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component' | 25 | import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component' |
26 | import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component' | 26 | import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component' |
27 | import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences' | 27 | import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences' |
28 | import { | ||
29 | MyAccountVideoPlaylistCreateComponent | ||
30 | } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component' | ||
31 | import { | ||
32 | MyAccountVideoPlaylistUpdateComponent | ||
33 | } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component' | ||
34 | import { 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 | |||
9 | import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' | 9 | import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' |
10 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | 10 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' |
11 | import { sortBy } from '@app/shared/misc/utils' | 11 | import { sortBy } from '@app/shared/misc/utils' |
12 | import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' | ||
12 | 13 | ||
13 | @Injectable() | 14 | @Injectable() |
14 | export class ServerService { | 15 | export 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 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { GlobalIconName } from '@app/shared/icons/global-icon.component' | 2 | import { 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' | |||
10 | export * from './video-channel-validators.service' | 10 | export * from './video-channel-validators.service' |
11 | export * from './video-comment-validators.service' | 11 | export * from './video-comment-validators.service' |
12 | export * from './video-validators.service' | 12 | export * from './video-validators.service' |
13 | export * from './video-playlist-validators.service' | ||
13 | export * from './video-captions-validators.service' | 14 | export * from './video-captions-validators.service' |
14 | export * from './video-change-ownership-validators.service' | 15 | export * from './video-change-ownership-validators.service' |
15 | export * from './video-accept-ownership-validators.service' | 16 | export * 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 @@ | |||
1 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
2 | import { Validators } from '@angular/forms' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { BuildFormValidator } from '@app/shared' | ||
5 | |||
6 | @Injectable() | ||
7 | export 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' | |||
4 | import { ServerService } from '@app/core' | 4 | import { 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 | }) |
18 | export class VideoImageComponent implements ControlValueAccessor { | 18 | export 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 | ||
20 | function populateAsyncUserVideoChannels (authService: AuthService, channel: { id: number, label: string, support: string }[]) { | 20 | function 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' |
50 | import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' | 51 | import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' |
@@ -68,8 +69,11 @@ import { UserNotificationsComponent } from '@app/shared/users/user-notifications | |||
68 | import { InstanceService } from '@app/shared/instance/instance.service' | 69 | import { InstanceService } from '@app/shared/instance/instance.service' |
69 | import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer' | 70 | import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer' |
70 | import { ConfirmComponent } from '@app/shared/confirm/confirm.component' | 71 | import { ConfirmComponent } from '@app/shared/confirm/confirm.component' |
71 | import { GlobalIconComponent } from '@app/shared/icons/global-icon.component' | ||
72 | import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component' | 72 | import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component' |
73 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | ||
74 | import { ImageUploadComponent } from '@app/shared/images/image-upload.component' | ||
75 | import { GlobalIconComponent } from '@app/shared/images/global-icon.component' | ||
76 | import { 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 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { 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 | }) | ||
9 | export 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 @@ | |||
1 | import { | ||
2 | VideoChannelSummary, | ||
3 | VideoConstant, | ||
4 | VideoPlaylist as ServerVideoPlaylist, | ||
5 | VideoPlaylistPrivacy, | ||
6 | VideoPlaylistType | ||
7 | } from '../../../../../shared/models/videos' | ||
8 | import { AccountSummary, peertubeTranslate } from '@shared/models' | ||
9 | import { Actor } from '@app/shared/actor/actor.model' | ||
10 | import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' | ||
11 | |||
12 | export 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 @@ | |||
1 | import { catchError, map, switchMap } from 'rxjs/operators' | ||
2 | import { Injectable } from '@angular/core' | ||
3 | import { Observable } from 'rxjs' | ||
4 | import { RestExtractor } from '../rest/rest-extractor.service' | ||
5 | import { HttpClient } from '@angular/common/http' | ||
6 | import { ResultList } from '../../../../../shared' | ||
7 | import { environment } from '../../../environments/environment' | ||
8 | import { VideoPlaylist as VideoPlaylistServerModel } from '@shared/models/videos/playlist/video-playlist.model' | ||
9 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' | ||
10 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | ||
11 | import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' | ||
12 | import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model' | ||
13 | import { objectToFormData } from '@app/shared/misc/utils' | ||
14 | import { ServerService } from '@app/core' | ||
15 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | ||
16 | import { AccountService } from '@app/shared/account/account.service' | ||
17 | import { Account } from '@app/shared/account/account.model' | ||
18 | |||
19 | @Injectable() | ||
20 | export 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' | |||
2 | import { TagInputModule } from 'ngx-chips' | 2 | import { TagInputModule } from 'ngx-chips' |
3 | import { SharedModule } from '../../../shared/' | 3 | import { SharedModule } from '../../../shared/' |
4 | import { VideoEditComponent } from './video-edit.component' | 4 | import { VideoEditComponent } from './video-edit.component' |
5 | import { VideoImageComponent } from './video-image.component' | ||
6 | import { CalendarModule } from 'primeng/components/calendar/calendar' | 5 | import { CalendarModule } from 'primeng/components/calendar/calendar' |
7 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' | 6 | import { 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 | } | ||