Begin videos list new design

pull/159/head
Chocobozzz 2017-12-01 14:46:22 +01:00
parent 26c6ee80d0
commit 9bf9d2a5c2
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
23 changed files with 212 additions and 295 deletions

View File

@ -43,7 +43,6 @@
"@types/webpack": "^3.0.0",
"@types/webtorrent": "^0.98.4",
"add-asset-html-webpack-plugin": "^2.0.1",
"angular-pipes": "^6.0.0",
"angular2-notifications": "^0.7.7",
"angular2-template-loader": "^0.6.0",
"assets-webpack-plugin": "^3.4.0",
@ -72,6 +71,7 @@
"ngc-webpack": "3.2.2",
"ngx-bootstrap": "2.0.0-beta.9",
"ngx-chips": "1.5.3",
"ngx-pipes": "^2.0.5",
"node-sass": "^4.1.1",
"normalize.css": "^7.0.0",
"optimize-js-plugin": "0.0.4",

View File

@ -26,12 +26,12 @@
<div class="panel-block">
<div class="block-title">Videos</div>
<a routerLink="/videos/list" routerLinkActive="active">
<a routerLink="/videos/trending" routerLinkActive="active">
<span class="icon icon-videos-trending"></span>
Trending
</a>
<a routerLink="/videos/list" routerLinkActive="active">
<a routerLink="/videos/recently-added" routerLinkActive="active">
<span class="icon icon-videos-recently-added"></span>
Recently added
</a>

View File

@ -0,0 +1,37 @@
import { Pipe, PipeTransform } from '@angular/core'
// Thanks: https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts
@Pipe({name: 'fromNow'})
export class FromNowPipe implements PipeTransform {
transform (value: number) {
const seconds = Math.floor((Date.now() - value) / 1000)
let interval = Math.floor(seconds / 31536000)
if (interval > 1) {
return interval + ' years ago'
}
interval = Math.floor(seconds / 2592000)
if (interval > 1) return interval + ' months ago'
if (interval === 1) return interval + ' month ago'
interval = Math.floor(seconds / 604800)
if (interval > 1) return interval + ' weeks ago'
if (interval === 1) return interval + ' week ago'
interval = Math.floor(seconds / 86400)
if (interval > 1) return interval + ' days ago'
if (interval === 1) return interval + ' day ago'
interval = Math.floor(seconds / 3600)
if (interval > 1) return interval + ' hours ago'
if (interval === 1) return interval + ' hour ago'
interval = Math.floor(seconds / 60)
if (interval >= 1) return interval + ' min ago'
return Math.floor(seconds) + ' sec ago'
}
}

View File

@ -0,0 +1,19 @@
import { Pipe, PipeTransform } from '@angular/core'
// Thanks: https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts
@Pipe({name: 'numberFormatter'})
export class NumberFormatterPipe implements PipeTransform {
private dictionary: Array<{max: number, type: string}> = [
{ max: 1000, type: '' },
{ max: 1000000, type: 'K' },
{ max: 1000000000, type: 'M' }
]
transform (value: number) {
const format = this.dictionary.find(d => value < d.max) || this.dictionary[this.dictionary.length - 1]
const calc = Math.floor(value / (format.max / 1000))
return `${calc}${format.type}`
}
}

View File

@ -1,25 +1,26 @@
import { NgModule } from '@angular/core'
import { HttpClientModule } from '@angular/common/http'
import { CommonModule } from '@angular/common'
import { HttpClientModule } from '@angular/common/http'
import { NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router'
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'
import { KeysPipe } from 'angular-pipes/src/object/keys.pipe'
import { BsDropdownModule } from 'ngx-bootstrap/dropdown'
import { ProgressbarModule } from 'ngx-bootstrap/progressbar'
import { PaginationModule } from 'ngx-bootstrap/pagination'
import { ModalModule } from 'ngx-bootstrap/modal'
import { DataTableModule } from 'primeng/components/datatable/datatable'
import { PaginationModule } from 'ngx-bootstrap/pagination'
import { ProgressbarModule } from 'ngx-bootstrap/progressbar'
import { BytesPipe, KeysPipe } from 'ngx-pipes'
import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
import { DataTableModule } from 'primeng/components/datatable/datatable'
import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
import { LoaderComponent } from './misc/loader.component'
import { RestExtractor, RestService } from './rest'
import { SearchComponent, SearchService } from './search'
import { UserService } from './users'
import { VideoAbuseService } from './video-abuse'
import { VideoBlacklistService } from './video-blacklist'
import { LoaderComponent } from './misc/loader.component'
import { NumberFormatterPipe } from './misc/number-formatter.pipe'
import { FromNowPipe } from './misc/from-now.pipe'
@NgModule({
imports: [
@ -42,7 +43,9 @@ import { LoaderComponent } from './misc/loader.component'
BytesPipe,
KeysPipe,
SearchComponent,
LoaderComponent
LoaderComponent,
NumberFormatterPipe,
FromNowPipe
],
exports: [
@ -62,7 +65,10 @@ import { LoaderComponent } from './misc/loader.component'
KeysPipe,
SearchComponent,
LoaderComponent
LoaderComponent,
NumberFormatterPipe,
FromNowPipe
],
providers: [

View File

@ -184,7 +184,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
this.notificationsService.success('Success', 'Video uploaded.')
// Display all the videos once it's finished
this.router.navigate([ '/videos/list' ])
this.router.navigate([ '/videos/trending' ])
}
},

View File

@ -1,3 +1,4 @@
export * from './my-videos.component'
export * from './video-list.component'
export * from './video-recently-added.component'
export * from './video-trending.component'
export * from './shared'

View File

@ -27,7 +27,7 @@ export class MyVideosComponent extends AbstractVideoList implements OnInit, OnDe
}
ngOnDestroy () {
this.subActivatedRoute.unsubscribe()
super.ngOnDestroy()
}
getVideosObservable () {

View File

@ -1,20 +1,8 @@
<div class="row">
<div class="content-padding">
<div class="videos-info">
<div class="col-md-9 col-xs-5 videos-total-results">
<span *ngIf="pagination.totalItems !== null">{{ pagination.totalItems }} videos</span>
<my-loader [loading]="loading | async"></my-loader>
</div>
<my-video-sort class="col-md-3 col-xs-7" [currentSort]="sort" (sort)="onSort($event)"></my-video-sort>
</div>
</div>
<div class="title-page">
{{ titlePage }}
</div>
<div class="content-padding videos-miniatures">
<div class="no-video" *ngIf="isThereNoVideo()">There is no video.</div>
<div class="videos-miniatures">
<my-video-miniature
class="ng-animate"
*ngFor="let video of videos" [video]="video" [user]="user" [currentSort]="sort"

View File

@ -17,20 +17,6 @@
}
}
.videos-miniatures {
text-align: center;
padding-top: 0;
my-video-miniature {
text-align: left;
}
.no-video {
margin-top: 50px;
text-align: center;
}
}
pagination {
display: block;
text-align: center;

View File

@ -1,25 +1,19 @@
import { OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { Subscription } from 'rxjs/Subscription'
import { BehaviorSubject } from 'rxjs/BehaviorSubject'
import { Observable } from 'rxjs/Observable'
import { NotificationsService } from 'angular2-notifications'
import { Observable } from 'rxjs/Observable'
import { Subscription } from 'rxjs/Subscription'
import {
SortField,
Video,
VideoPagination
} from '../../shared'
import { SortField, Video, VideoPagination } from '../../shared'
export abstract class AbstractVideoList implements OnInit, OnDestroy {
loading: BehaviorSubject<boolean> = new BehaviorSubject(false)
pagination: VideoPagination = {
currentPage: 1,
itemsPerPage: 25,
totalItems: null
}
sort: SortField
sort: SortField = '-createdAt'
videos: Video[] = []
protected notificationsService: NotificationsService
@ -28,6 +22,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
protected subActivatedRoute: Subscription
abstract titlePage: string
abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}>
ngOnInit () {
@ -44,7 +39,6 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
}
getVideos () {
this.loading.next(true)
this.videos = []
const observable = this.getVideosObservable()
@ -53,17 +47,11 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
({ videos, totalVideos }) => {
this.videos = videos
this.pagination.totalItems = totalVideos
this.loading.next(false)
},
error => this.notificationsService.error('Error', error.text)
)
}
isThereNoVideo () {
return !this.loading.getValue() && this.videos.length === 0
}
onPageChanged (event: { page: number }) {
// Be sure the current page is set
this.pagination.currentPage = event.page
@ -71,12 +59,6 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
this.navigateToNewParams()
}
onSort (sort: SortField) {
this.sort = sort
this.navigateToNewParams()
}
protected buildRouteParams () {
// There is always a sort and a current page
const params = {

View File

@ -1,3 +1,2 @@
export * from './abstract-video-list'
export * from './video-miniature.component'
export * from './video-sort.component'

View File

@ -6,8 +6,7 @@
<img [attr.src]="video.thumbnailUrl" alt="video thumbnail" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" />
<div class="video-miniature-thumbnail-overlay">
<span class="video-miniature-thumbnail-overlay-views">{{ video.views }} views</span>
<span class="video-miniature-thumbnail-overlay-duration">{{ video.durationLabel }}</span>
{{ video.durationLabel }}
</div>
</a>
@ -21,13 +20,7 @@
</a>
</span>
<div class="video-miniature-tags">
<span *ngFor="let tag of video.tags" class="video-miniature-tag">
<a [routerLink]="['/videos/list', { field: 'tags', search: tag, sort: currentSort }]" class="label label-primary">{{ tag }}</a>
</span>
</div>
<a [routerLink]="['/videos/list', { field: 'account', search: video.account, sort: currentSort }]" class="video-miniature-account">{{ video.by }}</a>
<span class="video-miniature-created-at">{{ video.createdAt | date:'short' }}</span>
<span class="video-miniature-created-at-views">{{ video.createdAt | fromNow }} - {{ video.views | numberFormatter }} views</span>
<span class="video-miniature-account">{{ video.by }}</span>
</div>
</div>

View File

@ -1,14 +1,14 @@
.video-miniature {
margin: 15px 10px;
display: inline-block;
position: relative;
height: 190px;
padding-right: 15px;
margin-bottom: 30px;
height: 175px;
vertical-align: top;
.video-miniature-thumbnail {
display: inline-block;
position: relative;
border-radius: 3px;
border-radius: 4px;
overflow: hidden;
&:hover {
@ -22,38 +22,33 @@
.video-miniature-thumbnail-overlay {
position: absolute;
right: 0px;
bottom: 0px;
right: 5px;
bottom: 5px;
display: inline-block;
background-color: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 3px 5px;
font-size: 11px;
font-weight: bold;
width: 100%;
.video-miniature-thumbnail-overlay-views {
}
.video-miniature-thumbnail-overlay-duration {
float: right;
}
font-size: 12px;
font-weight: $font-bold;
border-radius: 3px;
padding: 0 5px;
}
}
.video-miniature-information {
width: 200px;
margin-top: 2px;
line-height: normal;
.video-miniature-name {
height: 23px;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
transition: color 0.2s;
font-size: 15px;
font-size: 16px;
font-weight: $font-semibold;
color: #000;
&:hover {
text-decoration: none;
@ -63,39 +58,16 @@
filter: blur(3px);
padding-left: 4px;
}
.video-miniature-tags {
// Fix for chrome when tags are long
width: 201px;
.video-miniature-tag {
font-size: 13px;
cursor: pointer;
position: relative;
top: -2px;
.label {
transition: background-color 0.2s;
}
}
}
}
.video-miniature-account, .video-miniature-created-at {
.video-miniature-created-at-views {
display: block;
margin-left: 1px;
font-size: 11px;
color: $video-miniature-other-infos;
opacity: 0.9;
font-size: 13px;
}
.video-miniature-account {
transition: color 0.2s;
&:hover {
color: #23527c;
text-decoration: none;
}
font-size: 12px;
color: #585858;
}
}
}

View File

@ -1,5 +0,0 @@
<select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()">
<option *ngFor="let choice of choiceKeys" [value]="choice">
{{ getStringChoice(choice) }}
</option>
</select>

View File

@ -1,39 +0,0 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { SortField } from '../../shared'
@Component({
selector: 'my-video-sort',
templateUrl: './video-sort.component.html'
})
export class VideoSortComponent {
@Output() sort = new EventEmitter<any>()
@Input() currentSort: SortField
sortChoices: { [ P in SortField ]: string } = {
'name': 'Name - Asc',
'-name': 'Name - Desc',
'duration': 'Duration - Asc',
'-duration': 'Duration - Desc',
'createdAt': 'Created Date - Asc',
'-createdAt': 'Created Date - Desc',
'views': 'Views - Asc',
'-views': 'Views - Desc',
'likes': 'Likes - Asc',
'-likes': 'Likes - Desc'
}
get choiceKeys () {
return Object.keys(this.sortChoices)
}
getStringChoice (choiceKey: SortField) {
return this.sortChoices[choiceKey]
}
onSortChange () {
this.sort.emit(this.currentSort)
}
}

View File

@ -1,94 +0,0 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { Subscription } from 'rxjs/Subscription'
import { NotificationsService } from 'angular2-notifications'
import { VideoService } from '../shared'
import { Search, SearchField, SearchService } from '../../shared'
import { AbstractVideoList } from './shared'
@Component({
selector: 'my-videos-list',
styleUrls: [ './shared/abstract-video-list.scss' ],
templateUrl: './shared/abstract-video-list.html'
})
export class VideoListComponent extends AbstractVideoList implements OnInit, OnDestroy {
private search: Search
private subSearch: Subscription
constructor (
protected router: Router,
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
private videoService: VideoService,
private searchService: SearchService
) {
super()
}
ngOnInit () {
// Subscribe to route changes
this.subActivatedRoute = this.route.params.subscribe(routeParams => {
this.loadRouteParams(routeParams)
// Update the search service component
this.searchService.updateSearch.next(this.search)
this.getVideos()
})
// Subscribe to search changes
this.subSearch = this.searchService.searchUpdated.subscribe(search => {
this.search = search
// Reset pagination
this.pagination.currentPage = 1
this.navigateToNewParams()
})
}
ngOnDestroy () {
super.ngOnDestroy()
this.subSearch.unsubscribe()
}
getVideosObservable () {
let observable = null
if (this.search.value) {
observable = this.videoService.searchVideos(this.search, this.pagination, this.sort)
} else {
observable = this.videoService.getVideos(this.pagination, this.sort)
}
return observable
}
protected buildRouteParams () {
const params = super.buildRouteParams()
// Maybe there is a search
if (this.search.value) {
params['field'] = this.search.field
params['search'] = this.search.value
}
return params
}
protected loadRouteParams (routeParams: { [ key: string ]: any }) {
super.loadRouteParams(routeParams)
if (routeParams['search'] !== undefined) {
this.search = {
value: routeParams['search'],
field: routeParams['field'] as SearchField
}
} else {
this.search = {
value: '',
field: 'name'
}
}
}
}

View File

@ -0,0 +1,33 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { NotificationsService } from 'angular2-notifications'
import { VideoService } from '../shared'
import { AbstractVideoList } from './shared'
@Component({
selector: 'my-videos-recently-added',
styleUrls: [ './shared/abstract-video-list.scss' ],
templateUrl: './shared/abstract-video-list.html'
})
export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage = 'Recently added'
constructor (protected router: Router,
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
private videoService: VideoService) {
super()
}
ngOnInit () {
super.ngOnInit()
}
ngOnDestroy () {
super.ngOnDestroy()
}
getVideosObservable () {
return this.videoService.getVideos(this.pagination, this.sort)
}
}

View File

@ -0,0 +1,33 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { NotificationsService } from 'angular2-notifications'
import { VideoService } from '../shared'
import { AbstractVideoList } from './shared'
@Component({
selector: 'my-videos-trending',
styleUrls: [ './shared/abstract-video-list.scss' ],
templateUrl: './shared/abstract-video-list.html'
})
export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage = 'Trending'
constructor (protected router: Router,
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
private videoService: VideoService) {
super()
}
ngOnInit () {
super.ngOnInit()
}
ngOnDestroy () {
super.ngOnDestroy()
}
getVideosObservable () {
return this.videoService.getVideos(this.pagination, this.sort)
}
}

View File

@ -1,9 +1,9 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { MetaGuard } from '@ngx-meta/core'
import { VideoListComponent, MyVideosComponent } from './video-list'
import { MyVideosComponent } from './video-list'
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
import { VideoTrendingComponent } from './video-list/video-trending.component'
import { VideosComponent } from './videos.component'
const videosRoutes: Routes = [
@ -12,6 +12,11 @@ const videosRoutes: Routes = [
component: VideosComponent,
canActivateChild: [ MetaGuard ],
children: [
{
path: 'list',
pathMatch: 'full',
redirectTo: 'recently-added'
},
{
path: 'mine',
component: MyVideosComponent,
@ -22,11 +27,20 @@ const videosRoutes: Routes = [
}
},
{
path: 'list',
component: VideoListComponent,
path: 'trending',
component: VideoTrendingComponent,
data: {
meta: {
title: 'Videos list'
title: 'Trending videos'
}
}
},
{
path: 'recently-added',
component: VideoRecentlyAddedComponent,
data: {
meta: {
title: 'Recently added videos'
}
}
},

View File

@ -1,7 +1,9 @@
import { NgModule } from '@angular/core'
import { SharedModule } from '../shared'
import { VideoService } from './shared'
import { MyVideosComponent, VideoListComponent, VideoMiniatureComponent, VideoSortComponent } from './video-list'
import { MyVideosComponent, VideoMiniatureComponent } from './video-list'
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
import { VideoTrendingComponent } from './video-list/video-trending.component'
import { VideosRoutingModule } from './videos-routing.module'
import { VideosComponent } from './videos.component'
@ -14,10 +16,10 @@ import { VideosComponent } from './videos.component'
declarations: [
VideosComponent,
VideoListComponent,
VideoTrendingComponent,
VideoRecentlyAddedComponent,
MyVideosComponent,
VideoMiniatureComponent,
VideoSortComponent
VideoMiniatureComponent
],
exports: [

View File

@ -33,24 +33,14 @@ input.readonly {
}
.main-col {
.content-padding {
padding: 15px 30px;
padding: 30px;
@media screen and (max-width: 800px) {
padding: 15px 10px;
}
@media screen and (min-width: 1400px) {
padding: 15px 40px;
}
@media screen and (min-width: 1600px) {
padding: 15px 50px;
}
@media screen and (min-width: 1800px) {
padding: 15px 60px;
}
.title-page {
font-size: 16px;
font-weight: $font-bold;
display: inline-block;
border-bottom: 2px solid $orange-color;
margin-bottom: 25px;
}
}

View File

@ -264,10 +264,6 @@ amdefine@>=0.0.4:
version "1.0.1"
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
angular-pipes@^6.0.0:
version "6.5.3"
resolved "https://registry.yarnpkg.com/angular-pipes/-/angular-pipes-6.5.3.tgz#6bed37c51ebc2adaf3412663bfe25179d0489b02"
angular2-notifications@^0.7.7:
version "0.7.8"
resolved "https://registry.yarnpkg.com/angular2-notifications/-/angular2-notifications-0.7.8.tgz#ecbcb95a8d2d402af94a9a080d6664c70d33a029"
@ -4718,6 +4714,10 @@ ngx-chips@1.5.3:
dependencies:
ng2-material-dropdown "0.7.10"
ngx-pipes@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.0.5.tgz#743b827e350b1e66f5bdae49e90a02fa631d4c54"
no-case@^2.2.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"