import { LoginGuard } from '../core'
import { AccountComponent } from './account.component'
import { AccountSettingsComponent } from './account-settings/account-settings.component'
+import { AccountVideosComponent } from './account-videos/account-videos.component'
const accountRoutes: Routes = [
{
}
}
},
- // {
- // path: 'videos',
- // component: AccountVideosComponent,
- // data: {
- // meta: {
- // title: 'Account videos'
- // }
- // }
- // }
+ {
+ path: 'videos',
+ component: AccountVideosComponent,
+ data: {
+ meta: {
+ title: 'Account videos'
+ }
+ }
+ }
]
}
]
--- /dev/null
+<div
+ infiniteScroll
+ [infiniteScrollDistance]="0.5"
+ (scrolled)="onNearOfBottom()"
+>
+ <div *ngFor="let video of videos">
+ <my-video-thumbnail [video]="video"></my-video-thumbnail>
+ </div>
+</div>
--- /dev/null
+import { Component, OnDestroy, OnInit } from '@angular/core'
+import { AbstractVideoList } from '../../shared/video/abstract-video-list'
+import { ActivatedRoute } from '@angular/router'
+import { Router } from '@angular/router'
+import { NotificationsService } from 'angular2-notifications'
+import { VideoService } from '../../shared/video/video.service'
+
+@Component({
+ selector: 'my-account-videos',
+ templateUrl: './account-videos.component.html',
+ styleUrls: [ './account-videos.component.scss' ]
+})
+export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
+ titlePage = 'My videos'
+ currentRoute = '/account/videos'
+
+ constructor (protected router: Router,
+ protected route: ActivatedRoute,
+ protected notificationsService: NotificationsService,
+ private videoService: VideoService) {
+ super()
+ }
+
+ ngOnInit () {
+ super.ngOnInit()
+ }
+
+ ngOnDestroy () {
+ super.ngOnDestroy()
+ }
+
+ getVideosObservable () {
+ return this.videoService.getMyVideos(this.pagination, this.sort)
+ }
+}
import { AccountSettingsComponent } from './account-settings/account-settings.component'
import { AccountComponent } from './account.component'
import { AccountService } from './account.service'
+import { AccountVideosComponent } from './account-videos/account-videos.component'
@NgModule({
imports: [
AccountComponent,
AccountSettingsComponent,
AccountChangePasswordComponent,
- AccountDetailsComponent
+ AccountDetailsComponent,
+ AccountVideosComponent
],
exports: [
import { UserService } from './users'
import { VideoAbuseService } from './video-abuse'
import { VideoBlacklistService } from './video-blacklist'
+import { VideoThumbnailComponent } from './video/video-thumbnail.component'
+import { VideoService } from './video/video.service'
+import { InfiniteScrollModule } from 'ngx-infinite-scroll'
@NgModule({
imports: [
ProgressbarModule.forRoot(),
DataTableModule,
- PrimeSharedModule
+ PrimeSharedModule,
+ InfiniteScrollModule
],
declarations: [
KeysPipe,
SearchComponent,
LoaderComponent,
+ VideoThumbnailComponent,
NumberFormatterPipe,
FromNowPipe
],
ProgressbarModule,
DataTableModule,
PrimeSharedModule,
+ InfiniteScrollModule,
BytesPipe,
KeysPipe,
SearchComponent,
LoaderComponent,
+ VideoThumbnailComponent,
NumberFormatterPipe,
FromNowPipe
SearchService,
VideoAbuseService,
VideoBlacklistService,
- UserService
+ UserService,
+ VideoService
]
})
export class SharedModule { }
--- /dev/null
+<div class="margin-content">
+ <div class="title-page title-page-single">
+ {{ titlePage }}
+ </div>
+
+ <div
+ infiniteScroll
+ [infiniteScrollUpDistance]="1.5"
+ [infiniteScrollDistance]="0.5"
+ (scrolled)="onNearOfBottom()"
+ (scrolledUp)="onNearOfTop()"
+ >
+ <my-video-miniature
+ class="ng-animate"
+ *ngFor="let video of videos" [video]="video" [user]="user" [currentSort]="sort"
+ >
+ </my-video-miniature>
+ </div>
+</div>
--- /dev/null
+import { OnDestroy, OnInit } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { NotificationsService } from 'angular2-notifications'
+import { Observable } from 'rxjs/Observable'
+import { Subscription } from 'rxjs/Subscription'
+import { SortField } from './sort-field.type'
+import { VideoPagination } from './video-pagination.model'
+import { Video } from './video.model'
+
+export abstract class AbstractVideoList implements OnInit, OnDestroy {
+ pagination: VideoPagination = {
+ currentPage: 1,
+ itemsPerPage: 25,
+ totalItems: null
+ }
+ sort: SortField = '-createdAt'
+ videos: Video[] = []
+
+ protected notificationsService: NotificationsService
+ protected router: Router
+ protected route: ActivatedRoute
+ protected subActivatedRoute: Subscription
+
+ protected abstract currentRoute: string
+
+ abstract titlePage: string
+ private loadedPages: { [ id: number ]: boolean } = {}
+
+ abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}>
+
+ ngOnInit () {
+ // Subscribe to route changes
+ const routeParams = this.route.snapshot.params
+ this.loadRouteParams(routeParams)
+ this.loadMoreVideos('after')
+ }
+
+ ngOnDestroy () {
+ if (this.subActivatedRoute) {
+ this.subActivatedRoute.unsubscribe()
+ }
+ }
+
+ onNearOfTop () {
+ if (this.pagination.currentPage > 1) {
+ this.previousPage()
+ }
+ }
+
+ onNearOfBottom () {
+ if (this.hasMoreVideos()) {
+ this.nextPage()
+ }
+ }
+
+ loadMoreVideos (where: 'before' | 'after') {
+ if (this.loadedPages[this.pagination.currentPage] === true) return
+
+ const observable = this.getVideosObservable()
+
+ observable.subscribe(
+ ({ videos, totalVideos }) => {
+ this.loadedPages[this.pagination.currentPage] = true
+ this.pagination.totalItems = totalVideos
+
+ if (where === 'before') {
+ this.videos = videos.concat(this.videos)
+ } else {
+ this.videos = this.videos.concat(videos)
+ }
+ },
+ error => this.notificationsService.error('Error', error.text)
+ )
+ }
+
+ protected hasMoreVideos () {
+ if (!this.pagination.totalItems) return true
+
+ const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage
+ return maxPage > this.pagination.currentPage
+ }
+
+ protected previousPage () {
+ this.pagination.currentPage--
+
+ this.setNewRouteParams()
+ this.loadMoreVideos('before')
+ }
+
+ protected nextPage () {
+ this.pagination.currentPage++
+
+ this.setNewRouteParams()
+ this.loadMoreVideos('after')
+ }
+
+ protected buildRouteParams () {
+ // There is always a sort and a current page
+ const params = {
+ sort: this.sort,
+ page: this.pagination.currentPage
+ }
+
+ return params
+ }
+
+ protected loadRouteParams (routeParams: { [ key: string ]: any }) {
+ this.sort = routeParams['sort'] as SortField || '-createdAt'
+
+ if (routeParams['page'] !== undefined) {
+ this.pagination.currentPage = parseInt(routeParams['page'], 10)
+ } else {
+ this.pagination.currentPage = 1
+ }
+ }
+
+ protected setNewRouteParams () {
+ const routeParams = this.buildRouteParams()
+ this.router.navigate([ this.currentRoute, routeParams ])
+ }
+}
--- /dev/null
+export type SortField = 'name' | '-name'
+ | 'duration' | '-duration'
+ | 'createdAt' | '-createdAt'
+ | 'views' | '-views'
+ | 'likes' | '-likes'
--- /dev/null
+import { Video } from '../../shared/video/video.model'
+import { AuthUser } from '../../core'
+import {
+ VideoDetails as VideoDetailsServerModel,
+ VideoFile,
+ VideoChannel,
+ VideoResolution,
+ UserRight,
+ VideoPrivacy
+} from '../../../../../shared'
+
+export class VideoDetails extends Video implements VideoDetailsServerModel {
+ account: string
+ by: string
+ createdAt: Date
+ updatedAt: Date
+ categoryLabel: string
+ category: number
+ licenceLabel: string
+ licence: number
+ languageLabel: string
+ language: number
+ description: string
+ duration: number
+ durationLabel: string
+ id: number
+ uuid: string
+ isLocal: boolean
+ name: string
+ serverHost: string
+ tags: string[]
+ thumbnailPath: string
+ thumbnailUrl: string
+ previewPath: string
+ previewUrl: string
+ embedPath: string
+ embedUrl: string
+ views: number
+ likes: number
+ dislikes: number
+ nsfw: boolean
+ descriptionPath: string
+ files: VideoFile[]
+ channel: VideoChannel
+ privacy: VideoPrivacy
+ privacyLabel: string
+
+ constructor (hash: VideoDetailsServerModel) {
+ super(hash)
+
+ this.privacy = hash.privacy
+ this.privacyLabel = hash.privacyLabel
+ this.descriptionPath = hash.descriptionPath
+ this.files = hash.files
+ this.channel = hash.channel
+ }
+
+ getAppropriateMagnetUri (actualDownloadSpeed = 0) {
+ if (this.files === undefined || this.files.length === 0) return ''
+ if (this.files.length === 1) return this.files[0].magnetUri
+
+ // Find first video that is good for our download speed (remember they are sorted)
+ let betterResolutionFile = this.files.find(f => actualDownloadSpeed > (f.size / this.duration))
+
+ // If the download speed is too bad, return the lowest resolution we have
+ if (betterResolutionFile === undefined) {
+ betterResolutionFile = this.files.find(f => f.resolution === VideoResolution.H_240P)
+ }
+
+ return betterResolutionFile.magnetUri
+ }
+
+ isRemovableBy (user: AuthUser) {
+ return user && this.isLocal === true && (this.account === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO))
+ }
+
+ isBlackistableBy (user: AuthUser) {
+ return user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true && this.isLocal === false
+ }
+
+ isUpdatableBy (user: AuthUser) {
+ return user && this.isLocal === true && user.username === this.account
+ }
+}
--- /dev/null
+import { VideoDetails } from './video-details.model'
+import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
+
+export class VideoEdit {
+ category: number
+ licence: number
+ language: number
+ description: string
+ name: string
+ tags: string[]
+ nsfw: boolean
+ channel: number
+ privacy: VideoPrivacy
+ uuid?: string
+ id?: number
+
+ constructor (videoDetails: VideoDetails) {
+ this.id = videoDetails.id
+ this.uuid = videoDetails.uuid
+ this.category = videoDetails.category
+ this.licence = videoDetails.licence
+ this.language = videoDetails.language
+ this.description = videoDetails.description
+ this.name = videoDetails.name
+ this.tags = videoDetails.tags
+ this.nsfw = videoDetails.nsfw
+ this.channel = videoDetails.channel.id
+ this.privacy = videoDetails.privacy
+ }
+
+ patch (values: Object) {
+ Object.keys(values).forEach((key) => {
+ this[key] = values[key]
+ })
+ }
+
+ toJSON () {
+ return {
+ category: this.category,
+ licence: this.licence,
+ language: this.language,
+ description: this.description,
+ name: this.name,
+ tags: this.tags,
+ nsfw: this.nsfw,
+ channel: this.channel,
+ privacy: this.privacy
+ }
+ }
+}
--- /dev/null
+export interface VideoPagination {
+ currentPage: number
+ itemsPerPage: number
+ totalItems: number
+}
--- /dev/null
+<a
+ [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name"
+class="video-thumbnail"
+>
+<img [attr.src]="video.thumbnailUrl" alt="video thumbnail" [ngClass]="{ 'blur-filter': nsfw }" />
+
+<div class="video-thumbnail-overlay">
+ {{ video.durationLabel }}
+</div>
+</a>
--- /dev/null
+.video-thumbnail {
+ display: inline-block;
+ position: relative;
+ border-radius: 4px;
+ overflow: hidden;
+
+ &:hover {
+ text-decoration: none !important;
+ }
+
+ img.blur-filter {
+ filter: blur(5px);
+ transform : scale(1.03);
+ }
+
+ .video-thumbnail-overlay {
+ position: absolute;
+ right: 5px;
+ bottom: 5px;
+ display: inline-block;
+ background-color: rgba(0, 0, 0, 0.7);
+ color: #fff;
+ font-size: 12px;
+ font-weight: $font-bold;
+ border-radius: 3px;
+ padding: 0 5px;
+ }
+}
--- /dev/null
+import { Component, Input } from '@angular/core'
+import { Video } from './video.model'
+
+@Component({
+ selector: 'my-video-thumbnail',
+ styleUrls: [ './video-thumbnail.component.scss' ],
+ templateUrl: './video-thumbnail.component.html'
+})
+export class VideoThumbnailComponent {
+ @Input() video: Video
+ @Input() nsfw = false
+}
--- /dev/null
+import { Video as VideoServerModel } from '../../../../../shared'
+import { User } from '../'
+
+export class Video implements VideoServerModel {
+ account: string
+ by: string
+ createdAt: Date
+ updatedAt: Date
+ categoryLabel: string
+ category: number
+ licenceLabel: string
+ licence: number
+ languageLabel: string
+ language: number
+ description: string
+ duration: number
+ durationLabel: string
+ id: number
+ uuid: string
+ isLocal: boolean
+ name: string
+ serverHost: string
+ tags: string[]
+ thumbnailPath: string
+ thumbnailUrl: string
+ previewPath: string
+ previewUrl: string
+ embedPath: string
+ embedUrl: string
+ views: number
+ likes: number
+ dislikes: number
+ nsfw: boolean
+
+ private static createByString (account: string, serverHost: string) {
+ return account + '@' + serverHost
+ }
+
+ private static createDurationString (duration: number) {
+ const minutes = Math.floor(duration / 60)
+ const seconds = duration % 60
+ const minutesPadding = minutes >= 10 ? '' : '0'
+ const secondsPadding = seconds >= 10 ? '' : '0'
+
+ return minutesPadding + minutes.toString() + ':' + secondsPadding + seconds.toString()
+ }
+
+ constructor (hash: VideoServerModel) {
+ let absoluteAPIUrl = API_URL
+ if (!absoluteAPIUrl) {
+ // The API is on the same domain
+ absoluteAPIUrl = window.location.origin
+ }
+
+ this.account = hash.account
+ this.createdAt = new Date(hash.createdAt.toString())
+ this.categoryLabel = hash.categoryLabel
+ this.category = hash.category
+ this.licenceLabel = hash.licenceLabel
+ this.licence = hash.licence
+ this.languageLabel = hash.languageLabel
+ this.language = hash.language
+ this.description = hash.description
+ this.duration = hash.duration
+ this.durationLabel = Video.createDurationString(hash.duration)
+ this.id = hash.id
+ this.uuid = hash.uuid
+ this.isLocal = hash.isLocal
+ this.name = hash.name
+ this.serverHost = hash.serverHost
+ this.tags = hash.tags
+ this.thumbnailPath = hash.thumbnailPath
+ this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath
+ this.previewPath = hash.previewPath
+ this.previewUrl = absoluteAPIUrl + hash.previewPath
+ this.embedPath = hash.embedPath
+ this.embedUrl = absoluteAPIUrl + hash.embedPath
+ this.views = hash.views
+ this.likes = hash.likes
+ this.dislikes = hash.dislikes
+ this.nsfw = hash.nsfw
+
+ this.by = Video.createByString(hash.account, hash.serverHost)
+ }
+
+ isVideoNSFWForUser (user: User) {
+ // If the video is NSFW and the user is not logged in, or the user does not want to display NSFW videos...
+ return (this.nsfw && (!user || user.displayNSFW === false))
+ }
+}
--- /dev/null
+import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
+import { Injectable } from '@angular/core'
+import 'rxjs/add/operator/catch'
+import 'rxjs/add/operator/map'
+import { Observable } from 'rxjs/Observable'
+import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } from '../../../../../shared'
+import { ResultList } from '../../../../../shared/models/result-list.model'
+import { UserVideoRateUpdate } from '../../../../../shared/models/videos/user-video-rate-update.model'
+import { UserVideoRate } from '../../../../../shared/models/videos/user-video-rate.model'
+import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type'
+import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model'
+import { RestExtractor } from '../rest/rest-extractor.service'
+import { RestService } from '../rest/rest.service'
+import { Search } from '../search/search.model'
+import { UserService } from '../users/user.service'
+import { SortField } from './sort-field.type'
+import { VideoDetails } from './video-details.model'
+import { VideoEdit } from './video-edit.model'
+import { VideoPagination } from './video-pagination.model'
+import { Video } from './video.model'
+
+@Injectable()
+export class VideoService {
+ private static BASE_VIDEO_URL = API_URL + '/api/v1/videos/'
+
+ constructor (
+ private authHttp: HttpClient,
+ private restExtractor: RestExtractor,
+ private restService: RestService
+ ) {}
+
+ getVideo (uuid: string): Observable<VideoDetails> {
+ return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + uuid)
+ .map(videoHash => new VideoDetails(videoHash))
+ .catch((res) => this.restExtractor.handleError(res))
+ }
+
+ viewVideo (uuid: string): Observable<VideoDetails> {
+ return this.authHttp.post(VideoService.BASE_VIDEO_URL + uuid + '/views', {})
+ .map(this.restExtractor.extractDataBool)
+ .catch(this.restExtractor.handleError)
+ }
+
+ updateVideo (video: VideoEdit) {
+ const language = video.language ? video.language : null
+
+ const body: VideoUpdate = {
+ name: video.name,
+ category: video.category,
+ licence: video.licence,
+ language,
+ description: video.description,
+ privacy: video.privacy,
+ tags: video.tags,
+ nsfw: video.nsfw
+ }
+
+ return this.authHttp.put(VideoService.BASE_VIDEO_URL + video.id, body)
+ .map(this.restExtractor.extractDataBool)
+ .catch(this.restExtractor.handleError)
+ }
+
+ uploadVideo (video: FormData) {
+ const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
+
+ return this.authHttp
+ .request(req)
+ .catch(this.restExtractor.handleError)
+ }
+
+ getMyVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
+ const pagination = this.videoPaginationToRestPagination(videoPagination)
+
+ let params = new HttpParams()
+ params = this.restService.addRestGetParams(params, pagination, sort)
+
+ return this.authHttp.get(UserService.BASE_USERS_URL + '/me/videos', { params })
+ .map(this.extractVideos)
+ .catch((res) => this.restExtractor.handleError(res))
+ }
+
+ getVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
+ const pagination = this.videoPaginationToRestPagination(videoPagination)
+
+ let params = new HttpParams()
+ params = this.restService.addRestGetParams(params, pagination, sort)
+
+ return this.authHttp
+ .get(VideoService.BASE_VIDEO_URL, { params })
+ .map(this.extractVideos)
+ .catch((res) => this.restExtractor.handleError(res))
+ }
+
+ searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
+ const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value)
+
+ const pagination = this.videoPaginationToRestPagination(videoPagination)
+
+ let params = new HttpParams()
+ params = this.restService.addRestGetParams(params, pagination, sort)
+
+ if (search.field) params.set('field', search.field)
+
+ return this.authHttp
+ .get<ResultList<VideoServerModel>>(url, { params })
+ .map(this.extractVideos)
+ .catch((res) => this.restExtractor.handleError(res))
+ }
+
+ removeVideo (id: number) {
+ return this.authHttp
+ .delete(VideoService.BASE_VIDEO_URL + id)
+ .map(this.restExtractor.extractDataBool)
+ .catch((res) => this.restExtractor.handleError(res))
+ }
+
+ loadCompleteDescription (descriptionPath: string) {
+ return this.authHttp
+ .get(API_URL + descriptionPath)
+ .map(res => res['description'])
+ .catch((res) => this.restExtractor.handleError(res))
+ }
+
+ setVideoLike (id: number) {
+ return this.setVideoRate(id, 'like')
+ }
+
+ setVideoDislike (id: number) {
+ return this.setVideoRate(id, 'dislike')
+ }
+
+ getUserVideoRating (id: number): Observable<UserVideoRate> {
+ const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating'
+
+ return this.authHttp
+ .get(url)
+ .catch(res => this.restExtractor.handleError(res))
+ }
+
+ private videoPaginationToRestPagination (videoPagination: VideoPagination) {
+ const start: number = (videoPagination.currentPage - 1) * videoPagination.itemsPerPage
+ const count: number = videoPagination.itemsPerPage
+
+ return { start, count }
+ }
+
+ private setVideoRate (id: number, rateType: VideoRateType) {
+ const url = VideoService.BASE_VIDEO_URL + id + '/rate'
+ const body: UserVideoRateUpdate = {
+ rating: rateType
+ }
+
+ return this.authHttp
+ .put(url, body)
+ .map(this.restExtractor.extractDataBool)
+ .catch(res => this.restExtractor.handleError(res))
+ }
+
+ private extractVideos (result: ResultList<VideoServerModel>) {
+ const videosJson = result.data
+ const totalVideos = result.total
+ const videos = []
+
+ for (const videoJson of videosJson) {
+ videos.push(new Video(videoJson))
+ }
+
+ return { videos, totalVideos }
+ }
+}
import { TagInputModule } from 'ngx-chips'
import { TabsModule } from 'ngx-bootstrap/tabs'
-import { VideoService, MarkdownService, VideoDescriptionComponent } from '../../shared'
+import { MarkdownService, VideoDescriptionComponent } from '../../shared'
import { SharedModule } from '../../../shared'
@NgModule({
],
providers: [
- VideoService,
MarkdownService
]
})
+import { HttpEventType, HttpResponse } from '@angular/common/http'
import { Component, OnInit, ViewChild } from '@angular/core'
import { FormBuilder, FormGroup } from '@angular/forms'
import { Router } from '@angular/router'
-
import { NotificationsService } from 'angular2-notifications'
-
+import { VideoService } from 'app/shared/video/video.service'
+import { VideoCreate } from '../../../../../shared'
+import { AuthService, ServerService } from '../../core'
import {
FormReactive,
- VIDEO_NAME,
VIDEO_CATEGORY,
- VIDEO_LICENCE,
- VIDEO_LANGUAGE,
- VIDEO_DESCRIPTION,
- VIDEO_TAGS,
VIDEO_CHANNEL,
+ VIDEO_DESCRIPTION,
VIDEO_FILE,
- VIDEO_PRIVACY
+ VIDEO_LANGUAGE,
+ VIDEO_LICENCE,
+ VIDEO_NAME,
+ VIDEO_PRIVACY,
+ VIDEO_TAGS
} from '../../shared'
-import { AuthService, ServerService } from '../../core'
-import { VideoService } from '../shared'
-import { VideoCreate } from '../../../../../shared'
-import { HttpEventType, HttpResponse } from '@angular/common/http'
@Component({
selector: 'my-videos-add',
import { Component, OnInit } from '@angular/core'
import { FormBuilder, FormGroup } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router'
-import 'rxjs/add/observable/forkJoin'
-
import { NotificationsService } from 'angular2-notifications'
-
+import 'rxjs/add/observable/forkJoin'
+import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
import { ServerService } from '../../core'
import {
FormReactive,
- VIDEO_NAME,
VIDEO_CATEGORY,
- VIDEO_LICENCE,
- VIDEO_LANGUAGE,
VIDEO_DESCRIPTION,
- VIDEO_TAGS,
- VIDEO_PRIVACY
+ VIDEO_LANGUAGE,
+ VIDEO_LICENCE,
+ VIDEO_NAME,
+ VIDEO_PRIVACY,
+ VIDEO_TAGS
} from '../../shared'
-import { VideoEdit, VideoService } from '../shared'
-import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
+import { VideoService } from '../../shared/video/video.service'
+import { VideoEdit } from '../../shared/video/video-edit.model'
@Component({
selector: 'my-videos-update',
import { Component, Input, ViewChild } from '@angular/core'
-
import { ModalDirective } from 'ngx-bootstrap/modal'
-
-import { VideoDetails } from '../shared'
+import { VideoDetails } from '../../shared/video/video-details.model'
@Component({
selector: 'my-video-download',
import { Component, Input, OnInit, ViewChild } from '@angular/core'
import { FormBuilder, FormGroup } from '@angular/forms'
-
-import { ModalDirective } from 'ngx-bootstrap/modal'
import { NotificationsService } from 'angular2-notifications'
-
-import { FormReactive, VideoAbuseService, VIDEO_ABUSE_REASON } from '../../shared'
-import { VideoDetails, VideoService } from '../shared'
+import { ModalDirective } from 'ngx-bootstrap/modal'
+import { FormReactive, VIDEO_ABUSE_REASON, VideoAbuseService } from '../../shared'
+import { VideoDetails } from '../../shared/video/video-details.model'
@Component({
selector: 'my-video-report',
import { Component, Input, ViewChild } from '@angular/core'
-
import { ModalDirective } from 'ngx-bootstrap/modal'
-
-import { VideoDetails } from '../shared'
+import { VideoDetails } from '../../shared/video/video-details.model'
@Component({
selector: 'my-video-share',
import { ActivatedRoute, Router } from '@angular/router'
import { MetaService } from '@ngx-meta/core'
import { NotificationsService } from 'angular2-notifications'
+import { VideoService } from 'app/shared/video/video.service'
import { Observable } from 'rxjs/Observable'
import { Subscription } from 'rxjs/Subscription'
import videojs from 'video.js'
import '../../../assets/player/peertube-videojs-plugin'
import { AuthService, ConfirmService } from '../../core'
import { VideoBlacklistService } from '../../shared'
-import { MarkdownService, VideoDetails, VideoService } from '../shared'
+import { MarkdownService } from '../shared'
import { VideoDownloadComponent } from './video-download.component'
import { VideoReportComponent } from './video-report.component'
import { VideoShareComponent } from './video-share.component'
+import { VideoDetails } from '../../shared/video/video-details.model'
@Component({
selector: 'my-video-watch',
import { NgModule } from '@angular/core'
import { VideoWatchRoutingModule } from './video-watch-routing.module'
-import { VideoService, MarkdownService } from '../shared'
+import { MarkdownService } from '../shared'
import { SharedModule } from '../../shared'
import { VideoWatchComponent } from './video-watch.component'
],
providers: [
- MarkdownService,
- VideoService
+ MarkdownService
]
})
export class VideoWatchModule { }
-export * from './sort-field.type'
export * from './markdown.service'
-export * from './video.model'
-export * from './video-details.model'
-export * from './video-edit.model'
-export * from './video.service'
export * from './video-description.component'
-export * from './video-pagination.model'
+++ /dev/null
-export type SortField = 'name' | '-name'
- | 'duration' | '-duration'
- | 'createdAt' | '-createdAt'
- | 'views' | '-views'
- | 'likes' | '-likes'
+++ /dev/null
-import { Video } from './video.model'
-import { AuthUser } from '../../core'
-import {
- VideoDetails as VideoDetailsServerModel,
- VideoFile,
- VideoChannel,
- VideoResolution,
- UserRight,
- VideoPrivacy
-} from '../../../../../shared'
-
-export class VideoDetails extends Video implements VideoDetailsServerModel {
- account: string
- by: string
- createdAt: Date
- updatedAt: Date
- categoryLabel: string
- category: number
- licenceLabel: string
- licence: number
- languageLabel: string
- language: number
- description: string
- duration: number
- durationLabel: string
- id: number
- uuid: string
- isLocal: boolean
- name: string
- serverHost: string
- tags: string[]
- thumbnailPath: string
- thumbnailUrl: string
- previewPath: string
- previewUrl: string
- embedPath: string
- embedUrl: string
- views: number
- likes: number
- dislikes: number
- nsfw: boolean
- descriptionPath: string
- files: VideoFile[]
- channel: VideoChannel
- privacy: VideoPrivacy
- privacyLabel: string
-
- constructor (hash: VideoDetailsServerModel) {
- super(hash)
-
- this.privacy = hash.privacy
- this.privacyLabel = hash.privacyLabel
- this.descriptionPath = hash.descriptionPath
- this.files = hash.files
- this.channel = hash.channel
- }
-
- getAppropriateMagnetUri (actualDownloadSpeed = 0) {
- if (this.files === undefined || this.files.length === 0) return ''
- if (this.files.length === 1) return this.files[0].magnetUri
-
- // Find first video that is good for our download speed (remember they are sorted)
- let betterResolutionFile = this.files.find(f => actualDownloadSpeed > (f.size / this.duration))
-
- // If the download speed is too bad, return the lowest resolution we have
- if (betterResolutionFile === undefined) {
- betterResolutionFile = this.files.find(f => f.resolution === VideoResolution.H_240P)
- }
-
- return betterResolutionFile.magnetUri
- }
-
- isRemovableBy (user: AuthUser) {
- return user && this.isLocal === true && (this.account === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO))
- }
-
- isBlackistableBy (user: AuthUser) {
- return user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true && this.isLocal === false
- }
-
- isUpdatableBy (user: AuthUser) {
- return user && this.isLocal === true && user.username === this.account
- }
-}
+++ /dev/null
-import { VideoDetails } from './video-details.model'
-import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
-
-export class VideoEdit {
- category: number
- licence: number
- language: number
- description: string
- name: string
- tags: string[]
- nsfw: boolean
- channel: number
- privacy: VideoPrivacy
- uuid?: string
- id?: number
-
- constructor (videoDetails: VideoDetails) {
- this.id = videoDetails.id
- this.uuid = videoDetails.uuid
- this.category = videoDetails.category
- this.licence = videoDetails.licence
- this.language = videoDetails.language
- this.description = videoDetails.description
- this.name = videoDetails.name
- this.tags = videoDetails.tags
- this.nsfw = videoDetails.nsfw
- this.channel = videoDetails.channel.id
- this.privacy = videoDetails.privacy
- }
-
- patch (values: Object) {
- Object.keys(values).forEach((key) => {
- this[key] = values[key]
- })
- }
-
- toJSON () {
- return {
- category: this.category,
- licence: this.licence,
- language: this.language,
- description: this.description,
- name: this.name,
- tags: this.tags,
- nsfw: this.nsfw,
- channel: this.channel,
- privacy: this.privacy
- }
- }
-}
+++ /dev/null
-export interface VideoPagination {
- currentPage: number
- itemsPerPage: number
- totalItems: number
-}
+++ /dev/null
-import { Video as VideoServerModel } from '../../../../../shared'
-import { User } from '../../shared'
-
-export class Video implements VideoServerModel {
- account: string
- by: string
- createdAt: Date
- updatedAt: Date
- categoryLabel: string
- category: number
- licenceLabel: string
- licence: number
- languageLabel: string
- language: number
- description: string
- duration: number
- durationLabel: string
- id: number
- uuid: string
- isLocal: boolean
- name: string
- serverHost: string
- tags: string[]
- thumbnailPath: string
- thumbnailUrl: string
- previewPath: string
- previewUrl: string
- embedPath: string
- embedUrl: string
- views: number
- likes: number
- dislikes: number
- nsfw: boolean
-
- private static createByString (account: string, serverHost: string) {
- return account + '@' + serverHost
- }
-
- private static createDurationString (duration: number) {
- const minutes = Math.floor(duration / 60)
- const seconds = duration % 60
- const minutesPadding = minutes >= 10 ? '' : '0'
- const secondsPadding = seconds >= 10 ? '' : '0'
-
- return minutesPadding + minutes.toString() + ':' + secondsPadding + seconds.toString()
- }
-
- constructor (hash: VideoServerModel) {
- let absoluteAPIUrl = API_URL
- if (!absoluteAPIUrl) {
- // The API is on the same domain
- absoluteAPIUrl = window.location.origin
- }
-
- this.account = hash.account
- this.createdAt = new Date(hash.createdAt.toString())
- this.categoryLabel = hash.categoryLabel
- this.category = hash.category
- this.licenceLabel = hash.licenceLabel
- this.licence = hash.licence
- this.languageLabel = hash.languageLabel
- this.language = hash.language
- this.description = hash.description
- this.duration = hash.duration
- this.durationLabel = Video.createDurationString(hash.duration)
- this.id = hash.id
- this.uuid = hash.uuid
- this.isLocal = hash.isLocal
- this.name = hash.name
- this.serverHost = hash.serverHost
- this.tags = hash.tags
- this.thumbnailPath = hash.thumbnailPath
- this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath
- this.previewPath = hash.previewPath
- this.previewUrl = absoluteAPIUrl + hash.previewPath
- this.embedPath = hash.embedPath
- this.embedUrl = absoluteAPIUrl + hash.embedPath
- this.views = hash.views
- this.likes = hash.likes
- this.dislikes = hash.dislikes
- this.nsfw = hash.nsfw
-
- this.by = Video.createByString(hash.account, hash.serverHost)
- }
-
- isVideoNSFWForUser (user: User) {
- // If the video is NSFW and the user is not logged in, or the user does not want to display NSFW videos...
- return (this.nsfw && (!user || user.displayNSFW === false))
- }
-}
+++ /dev/null
-import { Injectable } from '@angular/core'
-import { Observable } from 'rxjs/Observable'
-import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
-import 'rxjs/add/operator/catch'
-import 'rxjs/add/operator/map'
-
-import { SortField } from './sort-field.type'
-import {
- RestExtractor,
- RestService,
- UserService,
- Search
-} from '../../shared'
-import { Video } from './video.model'
-import { VideoDetails } from './video-details.model'
-import { VideoEdit } from './video-edit.model'
-import { VideoPagination } from './video-pagination.model'
-import {
- UserVideoRate,
- VideoRateType,
- VideoUpdate,
- UserVideoRateUpdate,
- Video as VideoServerModel,
- VideoDetails as VideoDetailsServerModel,
- ResultList
-} from '../../../../../shared'
-
-@Injectable()
-export class VideoService {
- private static BASE_VIDEO_URL = API_URL + '/api/v1/videos/'
-
- constructor (
- private authHttp: HttpClient,
- private restExtractor: RestExtractor,
- private restService: RestService
- ) {}
-
- getVideo (uuid: string): Observable<VideoDetails> {
- return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + uuid)
- .map(videoHash => new VideoDetails(videoHash))
- .catch((res) => this.restExtractor.handleError(res))
- }
-
- viewVideo (uuid: string): Observable<VideoDetails> {
- return this.authHttp.post(VideoService.BASE_VIDEO_URL + uuid + '/views', {})
- .map(this.restExtractor.extractDataBool)
- .catch(this.restExtractor.handleError)
- }
-
- updateVideo (video: VideoEdit) {
- const language = video.language ? video.language : null
-
- const body: VideoUpdate = {
- name: video.name,
- category: video.category,
- licence: video.licence,
- language,
- description: video.description,
- privacy: video.privacy,
- tags: video.tags,
- nsfw: video.nsfw
- }
-
- return this.authHttp.put(VideoService.BASE_VIDEO_URL + video.id, body)
- .map(this.restExtractor.extractDataBool)
- .catch(this.restExtractor.handleError)
- }
-
- uploadVideo (video: FormData) {
- const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
-
- return this.authHttp
- .request(req)
- .catch(this.restExtractor.handleError)
- }
-
- getMyVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
- const pagination = this.videoPaginationToRestPagination(videoPagination)
-
- let params = new HttpParams()
- params = this.restService.addRestGetParams(params, pagination, sort)
-
- return this.authHttp.get(UserService.BASE_USERS_URL + '/me/videos', { params })
- .map(this.extractVideos)
- .catch((res) => this.restExtractor.handleError(res))
- }
-
- getVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
- const pagination = this.videoPaginationToRestPagination(videoPagination)
-
- let params = new HttpParams()
- params = this.restService.addRestGetParams(params, pagination, sort)
-
- return this.authHttp
- .get(VideoService.BASE_VIDEO_URL, { params })
- .map(this.extractVideos)
- .catch((res) => this.restExtractor.handleError(res))
- }
-
- searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
- const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value)
-
- const pagination = this.videoPaginationToRestPagination(videoPagination)
-
- let params = new HttpParams()
- params = this.restService.addRestGetParams(params, pagination, sort)
-
- if (search.field) params.set('field', search.field)
-
- return this.authHttp
- .get<ResultList<VideoServerModel>>(url, { params })
- .map(this.extractVideos)
- .catch((res) => this.restExtractor.handleError(res))
- }
-
- removeVideo (id: number) {
- return this.authHttp
- .delete(VideoService.BASE_VIDEO_URL + id)
- .map(this.restExtractor.extractDataBool)
- .catch((res) => this.restExtractor.handleError(res))
- }
-
- loadCompleteDescription (descriptionPath: string) {
- return this.authHttp
- .get(API_URL + descriptionPath)
- .map(res => res['description'])
- .catch((res) => this.restExtractor.handleError(res))
- }
-
- setVideoLike (id: number) {
- return this.setVideoRate(id, 'like')
- }
-
- setVideoDislike (id: number) {
- return this.setVideoRate(id, 'dislike')
- }
-
- getUserVideoRating (id: number): Observable<UserVideoRate> {
- const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating'
-
- return this.authHttp
- .get(url)
- .catch(res => this.restExtractor.handleError(res))
- }
-
- private videoPaginationToRestPagination (videoPagination: VideoPagination) {
- const start: number = (videoPagination.currentPage - 1) * videoPagination.itemsPerPage
- const count: number = videoPagination.itemsPerPage
-
- return { start, count }
- }
-
- private setVideoRate (id: number, rateType: VideoRateType) {
- const url = VideoService.BASE_VIDEO_URL + id + '/rate'
- const body: UserVideoRateUpdate = {
- rating: rateType
- }
-
- return this.authHttp
- .put(url, body)
- .map(this.restExtractor.extractDataBool)
- .catch(res => this.restExtractor.handleError(res))
- }
-
- private extractVideos (result: ResultList<VideoServerModel>) {
- const videosJson = result.data
- const totalVideos = result.total
- const videos = []
-
- for (const videoJson of videosJson) {
- videos.push(new Video(videoJson))
- }
-
- return { videos, totalVideos }
- }
-}
+++ /dev/null
-<div class="margin-content">
- <div class="title-page title-page-single">
- {{ titlePage }}
- </div>
-
- <div
- infiniteScroll
- [infiniteScrollUpDistance]="1.5"
- [infiniteScrollDistance]="0.5"
- (scrolled)="onNearOfBottom()"
- (scrolledUp)="onNearOfTop()"
- >
- <my-video-miniature
- class="ng-animate"
- *ngFor="let video of videos" [video]="video" [user]="user" [currentSort]="sort"
- >
- </my-video-miniature>
- </div>
-</div>
+++ /dev/null
-import { OnDestroy, OnInit } from '@angular/core'
-import { ActivatedRoute, Router } from '@angular/router'
-
-import { NotificationsService } from 'angular2-notifications'
-import { Observable } from 'rxjs/Observable'
-import { Subscription } from 'rxjs/Subscription'
-
-import { SortField, Video, VideoPagination } from '../../shared'
-
-export abstract class AbstractVideoList implements OnInit, OnDestroy {
- pagination: VideoPagination = {
- currentPage: 1,
- itemsPerPage: 25,
- totalItems: null
- }
- sort: SortField = '-createdAt'
- videos: Video[] = []
-
- protected notificationsService: NotificationsService
- protected router: Router
- protected route: ActivatedRoute
- protected subActivatedRoute: Subscription
-
- protected abstract currentRoute: string
-
- abstract titlePage: string
- private loadedPages: { [ id: number ]: boolean } = {}
-
- abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}>
-
- ngOnInit () {
- // Subscribe to route changes
- const routeParams = this.route.snapshot.params
- this.loadRouteParams(routeParams)
- this.loadMoreVideos('after')
- }
-
- ngOnDestroy () {
- if (this.subActivatedRoute) {
- this.subActivatedRoute.unsubscribe()
- }
- }
-
- onNearOfTop () {
- if (this.pagination.currentPage > 1) {
- this.previousPage()
- }
- }
-
- onNearOfBottom () {
- if (this.hasMoreVideos()) {
- this.nextPage()
- }
- }
-
- loadMoreVideos (where: 'before' | 'after') {
- if (this.loadedPages[this.pagination.currentPage] === true) return
-
- const observable = this.getVideosObservable()
-
- observable.subscribe(
- ({ videos, totalVideos }) => {
- this.loadedPages[this.pagination.currentPage] = true
- this.pagination.totalItems = totalVideos
-
- if (where === 'before') {
- this.videos = videos.concat(this.videos)
- } else {
- this.videos = this.videos.concat(videos)
- }
- },
- error => this.notificationsService.error('Error', error.text)
- )
- }
-
- protected hasMoreVideos () {
- if (!this.pagination.totalItems) return true
-
- const maxPage = this.pagination.totalItems/this.pagination.itemsPerPage
- return maxPage > this.pagination.currentPage
- }
-
- protected previousPage () {
- this.pagination.currentPage--
-
- this.setNewRouteParams()
- this.loadMoreVideos('before')
- }
-
- protected nextPage () {
- this.pagination.currentPage++
-
- this.setNewRouteParams()
- this.loadMoreVideos('after')
- }
-
- protected buildRouteParams () {
- // There is always a sort and a current page
- const params = {
- sort: this.sort,
- page: this.pagination.currentPage
- }
-
- return params
- }
-
- protected loadRouteParams (routeParams: { [ key: string ]: any }) {
- this.sort = routeParams['sort'] as SortField || '-createdAt'
-
- if (routeParams['page'] !== undefined) {
- this.pagination.currentPage = parseInt(routeParams['page'], 10)
- } else {
- this.pagination.currentPage = 1
- }
- }
-
- protected setNewRouteParams () {
- const routeParams = this.buildRouteParams()
- this.router.navigate([ this.currentRoute, routeParams ])
- }
-}
-export * from './abstract-video-list'
export * from './video-miniature.component'
<div class="video-miniature">
- <a
- [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.description"
- class="video-miniature-thumbnail"
- >
- <img [attr.src]="video.thumbnailUrl" alt="video thumbnail" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" />
-
- <div class="video-miniature-thumbnail-overlay">
- {{ video.durationLabel }}
- </div>
- </a>
+ <my-video-thumbnail [video]="video" [nsfw]="isVideoNSFWForThisUser()"></my-video-thumbnail>
<div class="video-miniature-information">
<span class="video-miniature-name">
height: 175px;
vertical-align: top;
- .video-miniature-thumbnail {
- display: inline-block;
- position: relative;
- border-radius: 4px;
- overflow: hidden;
-
- &:hover {
- text-decoration: none !important;
- }
-
- img.blur-filter {
- filter: blur(5px);
- transform : scale(1.03);
- }
-
- .video-miniature-thumbnail-overlay {
- position: absolute;
- right: 5px;
- bottom: 5px;
- display: inline-block;
- background-color: rgba(0, 0, 0, 0.7);
- color: #fff;
- font-size: 12px;
- font-weight: $font-bold;
- border-radius: 3px;
- padding: 0 5px;
- }
- }
-
.video-miniature-information {
width: 200px;
margin-top: 2px;
import { Component, Input } from '@angular/core'
-
-import { SortField, Video } from '../../shared'
import { User } from '../../../shared'
+import { SortField } from '../../../shared/video/sort-field.type'
+import { Video } from '../../../shared/video/video.model'
@Component({
selector: 'my-video-miniature',
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'
+import { VideoService } from '../../shared/video/video.service'
+import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@Component({
selector: 'my-videos-recently-added',
- styleUrls: [ './shared/abstract-video-list.scss' ],
- templateUrl: './shared/abstract-video-list.html'
+ styleUrls: [ '../../shared/video/abstract-video-list.scss' ],
+ templateUrl: '../../shared/video/abstract-video-list.html'
})
export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage = 'Recently added'
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'
+import { VideoService } from '../../shared/video/video.service'
+import { AbstractVideoList } from 'app/shared/video/abstract-video-list'
@Component({
selector: 'my-videos-trending',
- styleUrls: [ './shared/abstract-video-list.scss' ],
- templateUrl: './shared/abstract-video-list.html'
+ styleUrls: [ '../../shared/video/abstract-video-list.scss' ],
+ templateUrl: '../../shared/video/abstract-video-list.html'
})
export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage = 'Trending'
import { NgModule } from '@angular/core'
-import { InfiniteScrollModule } from 'ngx-infinite-scroll'
import { SharedModule } from '../shared'
-import { VideoService } from './shared'
import { VideoMiniatureComponent } from './video-list'
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
import { VideoTrendingComponent } from './video-list/video-trending.component'
@NgModule({
imports: [
VideosRoutingModule,
- SharedModule,
- InfiniteScrollModule
+ SharedModule
],
declarations: [
VideosComponent
],
- providers: [
- VideoService
- ]
+ providers: []
})
export class VideosModule { }