diff options
Diffstat (limited to 'client/src/app/shared')
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 @@ | |||
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/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 @@ | |||
1 | import { Component, forwardRef, Input } from '@angular/core' | ||
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | ||
3 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' | ||
4 | import { 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 | }) | ||
18 | export 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 | ||
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 |