"@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",
"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",
<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>
--- /dev/null
+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'
+ }
+}
--- /dev/null
+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}`
+ }
+}
-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: [
BytesPipe,
KeysPipe,
SearchComponent,
- LoaderComponent
+ LoaderComponent,
+ NumberFormatterPipe,
+ FromNowPipe
],
exports: [
KeysPipe,
SearchComponent,
- LoaderComponent
+ LoaderComponent,
+
+ NumberFormatterPipe,
+ FromNowPipe
],
providers: [
this.notificationsService.success('Success', 'Video uploaded.')
// Display all the videos once it's finished
- this.router.navigate([ '/videos/list' ])
+ this.router.navigate([ '/videos/trending' ])
}
},
export * from './my-videos.component'
-export * from './video-list.component'
+export * from './video-recently-added.component'
+export * from './video-trending.component'
export * from './shared'
}
ngOnDestroy () {
- this.subActivatedRoute.unsubscribe()
+ super.ngOnDestroy()
}
getVideosObservable () {
-<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"
}
}
-.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;
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
protected subActivatedRoute: Subscription
+ abstract titlePage: string
abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}>
ngOnInit () {
}
getVideos () {
- this.loading.next(true)
this.videos = []
const observable = this.getVideosObservable()
({ 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
this.navigateToNewParams()
}
- onSort (sort: SortField) {
- this.sort = sort
-
- this.navigateToNewParams()
- }
-
protected buildRouteParams () {
// There is always a sort and a current page
const params = {
export * from './abstract-video-list'
export * from './video-miniature.component'
-export * from './video-sort.component'
<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>
</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>
.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 {
.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;
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;
}
}
}
+++ /dev/null
-<select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()">
- <option *ngFor="let choice of choiceKeys" [value]="choice">
- {{ getStringChoice(choice) }}
- </option>
-</select>
+++ /dev/null
-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)
- }
-}
+++ /dev/null
-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'
- }
- }
- }
-}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)
+ }
+}
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 = [
component: VideosComponent,
canActivateChild: [ MetaGuard ],
children: [
+ {
+ path: 'list',
+ pathMatch: 'full',
+ redirectTo: 'recently-added'
+ },
{
path: 'mine',
component: MyVideosComponent,
}
},
{
- path: 'list',
- component: VideoListComponent,
+ path: 'trending',
+ component: VideoTrendingComponent,
+ data: {
+ meta: {
+ title: 'Trending videos'
+ }
+ }
+ },
+ {
+ path: 'recently-added',
+ component: VideoRecentlyAddedComponent,
data: {
meta: {
- title: 'Videos list'
+ title: 'Recently added videos'
}
}
},
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'
declarations: [
VideosComponent,
- VideoListComponent,
+ VideoTrendingComponent,
+ VideoRecentlyAddedComponent,
MyVideosComponent,
- VideoMiniatureComponent,
- VideoSortComponent
+ VideoMiniatureComponent
],
exports: [
}
.main-col {
- .content-padding {
- padding: 15px 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;
- }
+ padding: 30px;
+
+ .title-page {
+ font-size: 16px;
+ font-weight: $font-bold;
+ display: inline-block;
+ border-bottom: 2px solid $orange-color;
+ margin-bottom: 25px;
}
}
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"
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"