aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared
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 /client/src/app/shared
parentd4c9f45b31eda0b7a391ddc83eb290ca5cba311f (diff)
downloadPeerTube-830b4faff15fb9c81d88e8e69fcdf94aad32bef8.tar.gz
PeerTube-830b4faff15fb9c81d88e8e69fcdf94aad32bef8.tar.zst
PeerTube-830b4faff15fb9c81d88e8e69fcdf94aad32bef8.zip
Add/update/delete/list my playlists
Diffstat (limited to 'client/src/app/shared')
-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.html9
-rw-r--r--client/src/app/shared/images/image-upload.component.scss18
-rw-r--r--client/src/app/shared/images/image-upload.component.ts69
-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
21 files changed, 434 insertions, 94 deletions
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/shared/images/image-upload.component.html b/client/src/app/shared/images/image-upload.component.html
new file mode 100644
index 000000000..c09c862c4
--- /dev/null
+++ b/client/src/app/shared/images/image-upload.component.html
@@ -0,0 +1,9 @@
1<div class="root">
2 <my-reactive-file
3 [inputName]="inputName" [inputLabel]="inputLabel" [extensions]="videoImageExtensions" [maxFileSize]="maxVideoImageSize"
4 (fileChanged)="onFileChanged($event)"
5 ></my-reactive-file>
6
7 <img *ngIf="imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" [src]="imageSrc" class="preview" />
8 <div *ngIf="!imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" class="preview no-image"></div>
9</div>
diff --git a/client/src/app/shared/images/image-upload.component.scss b/client/src/app/shared/images/image-upload.component.scss
new file mode 100644
index 000000000..b63963bca
--- /dev/null
+++ b/client/src/app/shared/images/image-upload.component.scss
@@ -0,0 +1,18 @@
1@import '_variables';
2@import '_mixins';
3
4.root {
5 height: auto;
6 display: flex;
7 align-items: center;
8
9 .preview {
10 border: 2px solid grey;
11 border-radius: 4px;
12 margin-left: 50px;
13
14 &.no-image {
15 background-color: #ececec;
16 }
17 }
18}
diff --git a/client/src/app/shared/images/image-upload.component.ts b/client/src/app/shared/images/image-upload.component.ts
new file mode 100644
index 000000000..2da1592ff
--- /dev/null
+++ b/client/src/app/shared/images/image-upload.component.ts
@@ -0,0 +1,69 @@
1import { Component, forwardRef, Input } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
4import { ServerService } from '@app/core'
5
6@Component({
7 selector: 'my-image-upload',
8 styleUrls: [ './image-upload.component.scss' ],
9 templateUrl: './image-upload.component.html',
10 providers: [
11 {
12 provide: NG_VALUE_ACCESSOR,
13 useExisting: forwardRef(() => ImageUploadComponent),
14 multi: true
15 }
16 ]
17})
18export class ImageUploadComponent implements ControlValueAccessor {
19 @Input() inputLabel: string
20 @Input() inputName: string
21 @Input() previewWidth: string
22 @Input() previewHeight: string
23
24 imageSrc: SafeResourceUrl
25
26 private file: File
27
28 constructor (
29 private sanitizer: DomSanitizer,
30 private serverService: ServerService
31 ) {}
32
33 get videoImageExtensions () {
34 return this.serverService.getConfig().video.image.extensions
35 }
36
37 get maxVideoImageSize () {
38 return this.serverService.getConfig().video.image.size.max
39 }
40
41 onFileChanged (file: File) {
42 this.file = file
43
44 this.propagateChange(this.file)
45 this.updatePreview()
46 }
47
48 propagateChange = (_: any) => { /* empty */ }
49
50 writeValue (file: any) {
51 this.file = file
52 this.updatePreview()
53 }
54
55 registerOnChange (fn: (_: any) => void) {
56 this.propagateChange = fn
57 }
58
59 registerOnTouched () {
60 // Unused
61 }
62
63 private updatePreview () {
64 if (this.file) {
65 const url = URL.createObjectURL(this.file)
66 this.imageSrc = this.sanitizer.bypassSecurityTrustResourceUrl(url)
67 }
68 }
69}
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