diff options
Diffstat (limited to 'client/src/app/videos/video-list/shared')
11 files changed, 385 insertions, 0 deletions
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.html b/client/src/app/videos/video-list/shared/abstract-video-list.html new file mode 100644 index 000000000..680fba3f5 --- /dev/null +++ b/client/src/app/videos/video-list/shared/abstract-video-list.html | |||
@@ -0,0 +1,28 @@ | |||
1 | <div class="row"> | ||
2 | <div class="content-padding"> | ||
3 | <div class="videos-info"> | ||
4 | <div class="col-md-9 col-xs-5 videos-total-results"> | ||
5 | <span *ngIf="pagination.totalItems !== null">{{ pagination.totalItems }} videos</span> | ||
6 | |||
7 | <my-loader [loading]="loading | async"></my-loader> | ||
8 | </div> | ||
9 | |||
10 | <my-video-sort class="col-md-3 col-xs-7" [currentSort]="sort" (sort)="onSort($event)"></my-video-sort> | ||
11 | </div> | ||
12 | </div> | ||
13 | </div> | ||
14 | |||
15 | <div class="content-padding videos-miniatures"> | ||
16 | <div class="no-video" *ngIf="isThereNoVideo()">There is no video.</div> | ||
17 | |||
18 | <my-video-miniature | ||
19 | class="ng-animate" | ||
20 | *ngFor="let video of videos" [video]="video" [user]="user" [currentSort]="sort" | ||
21 | > | ||
22 | </my-video-miniature> | ||
23 | </div> | ||
24 | |||
25 | <pagination *ngIf="pagination.totalItems !== null && pagination.totalItems !== 0" | ||
26 | [totalItems]="pagination.totalItems" [itemsPerPage]="pagination.itemsPerPage" [maxSize]="6" [boundaryLinks]="true" [rotate]="false" | ||
27 | [(ngModel)]="pagination.currentPage" (pageChanged)="onPageChanged($event)" | ||
28 | ></pagination> | ||
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.scss b/client/src/app/videos/video-list/shared/abstract-video-list.scss new file mode 100644 index 000000000..4b4409602 --- /dev/null +++ b/client/src/app/videos/video-list/shared/abstract-video-list.scss | |||
@@ -0,0 +1,37 @@ | |||
1 | .videos-info { | ||
2 | @media screen and (max-width: 400px) { | ||
3 | margin-left: 0; | ||
4 | } | ||
5 | |||
6 | border-bottom: 1px solid #f1f1f1; | ||
7 | height: 40px; | ||
8 | line-height: 40px; | ||
9 | |||
10 | .videos-total-results { | ||
11 | font-size: 13px; | ||
12 | } | ||
13 | |||
14 | my-loader { | ||
15 | display: inline-block; | ||
16 | margin-left: 5px; | ||
17 | } | ||
18 | } | ||
19 | |||
20 | .videos-miniatures { | ||
21 | text-align: center; | ||
22 | padding-top: 0; | ||
23 | |||
24 | my-video-miniature { | ||
25 | text-align: left; | ||
26 | } | ||
27 | |||
28 | .no-video { | ||
29 | margin-top: 50px; | ||
30 | text-align: center; | ||
31 | } | ||
32 | } | ||
33 | |||
34 | pagination { | ||
35 | display: block; | ||
36 | text-align: center; | ||
37 | } | ||
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.ts b/client/src/app/videos/video-list/shared/abstract-video-list.ts new file mode 100644 index 000000000..87d5bc48a --- /dev/null +++ b/client/src/app/videos/video-list/shared/abstract-video-list.ts | |||
@@ -0,0 +1,104 @@ | |||
1 | import { OnDestroy, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { Subscription } from 'rxjs/Subscription' | ||
4 | import { BehaviorSubject } from 'rxjs/BehaviorSubject' | ||
5 | import { Observable } from 'rxjs/Observable' | ||
6 | |||
7 | import { NotificationsService } from 'angular2-notifications' | ||
8 | |||
9 | import { | ||
10 | SortField, | ||
11 | Video, | ||
12 | VideoPagination | ||
13 | } from '../../shared' | ||
14 | |||
15 | export abstract class AbstractVideoList implements OnInit, OnDestroy { | ||
16 | loading: BehaviorSubject<boolean> = new BehaviorSubject(false) | ||
17 | pagination: VideoPagination = { | ||
18 | currentPage: 1, | ||
19 | itemsPerPage: 25, | ||
20 | totalItems: null | ||
21 | } | ||
22 | sort: SortField | ||
23 | videos: Video[] = [] | ||
24 | |||
25 | protected notificationsService: NotificationsService | ||
26 | protected router: Router | ||
27 | protected route: ActivatedRoute | ||
28 | |||
29 | protected subActivatedRoute: Subscription | ||
30 | |||
31 | abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}> | ||
32 | |||
33 | ngOnInit () { | ||
34 | // Subscribe to route changes | ||
35 | this.subActivatedRoute = this.route.params.subscribe(routeParams => { | ||
36 | this.loadRouteParams(routeParams) | ||
37 | |||
38 | this.getVideos() | ||
39 | }) | ||
40 | } | ||
41 | |||
42 | ngOnDestroy () { | ||
43 | this.subActivatedRoute.unsubscribe() | ||
44 | } | ||
45 | |||
46 | getVideos () { | ||
47 | this.loading.next(true) | ||
48 | this.videos = [] | ||
49 | |||
50 | const observable = this.getVideosObservable() | ||
51 | |||
52 | observable.subscribe( | ||
53 | ({ videos, totalVideos }) => { | ||
54 | this.videos = videos | ||
55 | this.pagination.totalItems = totalVideos | ||
56 | |||
57 | this.loading.next(false) | ||
58 | }, | ||
59 | error => this.notificationsService.error('Error', error.text) | ||
60 | ) | ||
61 | } | ||
62 | |||
63 | isThereNoVideo () { | ||
64 | return !this.loading.getValue() && this.videos.length === 0 | ||
65 | } | ||
66 | |||
67 | onPageChanged (event: { page: number }) { | ||
68 | // Be sure the current page is set | ||
69 | this.pagination.currentPage = event.page | ||
70 | |||
71 | this.navigateToNewParams() | ||
72 | } | ||
73 | |||
74 | onSort (sort: SortField) { | ||
75 | this.sort = sort | ||
76 | |||
77 | this.navigateToNewParams() | ||
78 | } | ||
79 | |||
80 | protected buildRouteParams () { | ||
81 | // There is always a sort and a current page | ||
82 | const params = { | ||
83 | sort: this.sort, | ||
84 | page: this.pagination.currentPage | ||
85 | } | ||
86 | |||
87 | return params | ||
88 | } | ||
89 | |||
90 | protected loadRouteParams (routeParams: { [ key: string ]: any }) { | ||
91 | this.sort = routeParams['sort'] as SortField || '-createdAt' | ||
92 | |||
93 | if (routeParams['page'] !== undefined) { | ||
94 | this.pagination.currentPage = parseInt(routeParams['page'], 10) | ||
95 | } else { | ||
96 | this.pagination.currentPage = 1 | ||
97 | } | ||
98 | } | ||
99 | |||
100 | protected navigateToNewParams () { | ||
101 | const routeParams = this.buildRouteParams() | ||
102 | this.router.navigate([ '/videos/list', routeParams ]) | ||
103 | } | ||
104 | } | ||
diff --git a/client/src/app/videos/video-list/shared/index.ts b/client/src/app/videos/video-list/shared/index.ts new file mode 100644 index 000000000..2c9804e6d --- /dev/null +++ b/client/src/app/videos/video-list/shared/index.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export * from './abstract-video-list' | ||
2 | export * from './loader.component' | ||
3 | export * from './video-miniature.component' | ||
4 | export * from './video-sort.component' | ||
diff --git a/client/src/app/videos/video-list/shared/loader.component.html b/client/src/app/videos/video-list/shared/loader.component.html new file mode 100644 index 000000000..38d06950e --- /dev/null +++ b/client/src/app/videos/video-list/shared/loader.component.html | |||
@@ -0,0 +1,3 @@ | |||
1 | <div id="video-loading" *ngIf="loading"> | ||
2 | <div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div> | ||
3 | </div> | ||
diff --git a/client/src/app/videos/video-list/shared/loader.component.ts b/client/src/app/videos/video-list/shared/loader.component.ts new file mode 100644 index 000000000..f37d70c85 --- /dev/null +++ b/client/src/app/videos/video-list/shared/loader.component.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-loader', | ||
5 | styleUrls: [ ], | ||
6 | templateUrl: './loader.component.html' | ||
7 | }) | ||
8 | |||
9 | export class LoaderComponent { | ||
10 | @Input() loading: boolean | ||
11 | } | ||
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.html b/client/src/app/videos/video-list/shared/video-miniature.component.html new file mode 100644 index 000000000..abe87025f --- /dev/null +++ b/client/src/app/videos/video-list/shared/video-miniature.component.html | |||
@@ -0,0 +1,33 @@ | |||
1 | <div class="video-miniature"> | ||
2 | <a | ||
3 | [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.description" | ||
4 | class="video-miniature-thumbnail" | ||
5 | > | ||
6 | <img [attr.src]="video.thumbnailUrl" alt="video thumbnail" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" /> | ||
7 | |||
8 | <div class="video-miniature-thumbnail-overlay"> | ||
9 | <span class="video-miniature-thumbnail-overlay-views">{{ video.views }} views</span> | ||
10 | <span class="video-miniature-thumbnail-overlay-duration">{{ video.durationLabel }}</span> | ||
11 | </div> | ||
12 | </a> | ||
13 | |||
14 | <div class="video-miniature-information"> | ||
15 | <span class="video-miniature-name"> | ||
16 | <a | ||
17 | class="video-miniature-name" | ||
18 | [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" | ||
19 | > | ||
20 | {{ video.name }} | ||
21 | </a> | ||
22 | </span> | ||
23 | |||
24 | <div class="video-miniature-tags"> | ||
25 | <span *ngFor="let tag of video.tags" class="video-miniature-tag"> | ||
26 | <a [routerLink]="['/videos/list', { field: 'tags', search: tag, sort: currentSort }]" class="label label-primary">{{ tag }}</a> | ||
27 | </span> | ||
28 | </div> | ||
29 | |||
30 | <a [routerLink]="['/videos/list', { field: 'author', search: video.author, sort: currentSort }]" class="video-miniature-author">{{ video.by }}</a> | ||
31 | <span class="video-miniature-created-at">{{ video.createdAt | date:'short' }}</span> | ||
32 | </div> | ||
33 | </div> | ||
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.scss b/client/src/app/videos/video-list/shared/video-miniature.component.scss new file mode 100644 index 000000000..066792d10 --- /dev/null +++ b/client/src/app/videos/video-list/shared/video-miniature.component.scss | |||
@@ -0,0 +1,102 @@ | |||
1 | .video-miniature { | ||
2 | margin-top: 30px; | ||
3 | display: inline-block; | ||
4 | position: relative; | ||
5 | height: 190px; | ||
6 | width: 220px; | ||
7 | vertical-align: top; | ||
8 | |||
9 | .video-miniature-thumbnail { | ||
10 | display: inline-block; | ||
11 | position: relative; | ||
12 | border-radius: 3px; | ||
13 | overflow: hidden; | ||
14 | |||
15 | &:hover { | ||
16 | text-decoration: none !important; | ||
17 | } | ||
18 | |||
19 | img.blur-filter { | ||
20 | filter: blur(5px); | ||
21 | transform : scale(1.03); | ||
22 | } | ||
23 | |||
24 | .video-miniature-thumbnail-overlay { | ||
25 | position: absolute; | ||
26 | right: 0px; | ||
27 | bottom: 0px; | ||
28 | display: inline-block; | ||
29 | background-color: rgba(0, 0, 0, 0.7); | ||
30 | color: #fff; | ||
31 | padding: 3px 5px; | ||
32 | font-size: 11px; | ||
33 | font-weight: bold; | ||
34 | width: 100%; | ||
35 | |||
36 | .video-miniature-thumbnail-overlay-views { | ||
37 | |||
38 | } | ||
39 | |||
40 | .video-miniature-thumbnail-overlay-duration { | ||
41 | float: right; | ||
42 | } | ||
43 | } | ||
44 | } | ||
45 | |||
46 | .video-miniature-information { | ||
47 | width: 200px; | ||
48 | |||
49 | .video-miniature-name { | ||
50 | height: 23px; | ||
51 | display: block; | ||
52 | overflow: hidden; | ||
53 | text-overflow: ellipsis; | ||
54 | white-space: nowrap; | ||
55 | font-weight: bold; | ||
56 | transition: color 0.2s; | ||
57 | font-size: 15px; | ||
58 | |||
59 | &:hover { | ||
60 | text-decoration: none; | ||
61 | } | ||
62 | |||
63 | &.blur-filter { | ||
64 | filter: blur(3px); | ||
65 | padding-left: 4px; | ||
66 | } | ||
67 | |||
68 | .video-miniature-tags { | ||
69 | // Fix for chrome when tags are long | ||
70 | width: 201px; | ||
71 | |||
72 | .video-miniature-tag { | ||
73 | font-size: 13px; | ||
74 | cursor: pointer; | ||
75 | position: relative; | ||
76 | top: -2px; | ||
77 | |||
78 | .label { | ||
79 | transition: background-color 0.2s; | ||
80 | } | ||
81 | } | ||
82 | } | ||
83 | } | ||
84 | |||
85 | .video-miniature-author, .video-miniature-created-at { | ||
86 | display: block; | ||
87 | margin-left: 1px; | ||
88 | font-size: 11px; | ||
89 | color: $video-miniature-other-infos; | ||
90 | opacity: 0.9; | ||
91 | } | ||
92 | |||
93 | .video-miniature-author { | ||
94 | transition: color 0.2s; | ||
95 | |||
96 | &:hover { | ||
97 | color: #23527c; | ||
98 | text-decoration: none; | ||
99 | } | ||
100 | } | ||
101 | } | ||
102 | } | ||
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.ts b/client/src/app/videos/video-list/shared/video-miniature.component.ts new file mode 100644 index 000000000..e5a87907b --- /dev/null +++ b/client/src/app/videos/video-list/shared/video-miniature.component.ts | |||
@@ -0,0 +1,19 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | import { SortField, Video } from '../../shared' | ||
4 | import { User } from '../../../shared' | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'my-video-miniature', | ||
8 | styleUrls: [ './video-miniature.component.scss' ], | ||
9 | templateUrl: './video-miniature.component.html' | ||
10 | }) | ||
11 | export class VideoMiniatureComponent { | ||
12 | @Input() currentSort: SortField | ||
13 | @Input() user: User | ||
14 | @Input() video: Video | ||
15 | |||
16 | isVideoNSFWForThisUser () { | ||
17 | return this.video.isVideoNSFWForUser(this.user) | ||
18 | } | ||
19 | } | ||
diff --git a/client/src/app/videos/video-list/shared/video-sort.component.html b/client/src/app/videos/video-list/shared/video-sort.component.html new file mode 100644 index 000000000..3bece0b22 --- /dev/null +++ b/client/src/app/videos/video-list/shared/video-sort.component.html | |||
@@ -0,0 +1,5 @@ | |||
1 | <select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()"> | ||
2 | <option *ngFor="let choice of choiceKeys" [value]="choice"> | ||
3 | {{ getStringChoice(choice) }} | ||
4 | </option> | ||
5 | </select> | ||
diff --git a/client/src/app/videos/video-list/shared/video-sort.component.ts b/client/src/app/videos/video-list/shared/video-sort.component.ts new file mode 100644 index 000000000..8aa89d32b --- /dev/null +++ b/client/src/app/videos/video-list/shared/video-sort.component.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import { Component, EventEmitter, Input, Output } from '@angular/core' | ||
2 | |||
3 | import { SortField } from '../../shared' | ||
4 | |||
5 | @Component({ | ||
6 | selector: 'my-video-sort', | ||
7 | templateUrl: './video-sort.component.html' | ||
8 | }) | ||
9 | |||
10 | export class VideoSortComponent { | ||
11 | @Output() sort = new EventEmitter<any>() | ||
12 | |||
13 | @Input() currentSort: SortField | ||
14 | |||
15 | sortChoices: { [ P in SortField ]: string } = { | ||
16 | 'name': 'Name - Asc', | ||
17 | '-name': 'Name - Desc', | ||
18 | 'duration': 'Duration - Asc', | ||
19 | '-duration': 'Duration - Desc', | ||
20 | 'createdAt': 'Created Date - Asc', | ||
21 | '-createdAt': 'Created Date - Desc', | ||
22 | 'views': 'Views - Asc', | ||
23 | '-views': 'Views - Desc', | ||
24 | 'likes': 'Likes - Asc', | ||
25 | '-likes': 'Likes - Desc' | ||
26 | } | ||
27 | |||
28 | get choiceKeys () { | ||
29 | return Object.keys(this.sortChoices) | ||
30 | } | ||
31 | |||
32 | getStringChoice (choiceKey: SortField) { | ||
33 | return this.sortChoices[choiceKey] | ||
34 | } | ||
35 | |||
36 | onSortChange () { | ||
37 | this.sort.emit(this.currentSort) | ||
38 | } | ||
39 | } | ||