From 202f6b6c9dcc9b0aec4b0c1b15e455c22a7952a7 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 1 Dec 2017 18:56:26 +0100 Subject: Begin videos of an account --- .../src/app/shared/video/abstract-video-list.html | 19 +++ .../src/app/shared/video/abstract-video-list.scss | 0 client/src/app/shared/video/abstract-video-list.ts | 121 +++++++++++++++ client/src/app/shared/video/sort-field.type.ts | 5 + client/src/app/shared/video/video-details.model.ts | 84 ++++++++++ client/src/app/shared/video/video-edit.model.ts | 50 ++++++ .../src/app/shared/video/video-pagination.model.ts | 5 + .../shared/video/video-thumbnail.component.html | 10 ++ .../shared/video/video-thumbnail.component.scss | 28 ++++ .../app/shared/video/video-thumbnail.component.ts | 12 ++ client/src/app/shared/video/video.model.ts | 90 +++++++++++ client/src/app/shared/video/video.service.ts | 170 +++++++++++++++++++++ 12 files changed, 594 insertions(+) create mode 100644 client/src/app/shared/video/abstract-video-list.html create mode 100644 client/src/app/shared/video/abstract-video-list.scss create mode 100644 client/src/app/shared/video/abstract-video-list.ts create mode 100644 client/src/app/shared/video/sort-field.type.ts create mode 100644 client/src/app/shared/video/video-details.model.ts create mode 100644 client/src/app/shared/video/video-edit.model.ts create mode 100644 client/src/app/shared/video/video-pagination.model.ts create mode 100644 client/src/app/shared/video/video-thumbnail.component.html create mode 100644 client/src/app/shared/video/video-thumbnail.component.scss create mode 100644 client/src/app/shared/video/video-thumbnail.component.ts create mode 100644 client/src/app/shared/video/video.model.ts create mode 100644 client/src/app/shared/video/video.service.ts (limited to 'client/src/app/shared/video') diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html new file mode 100644 index 000000000..bd4f6b1f8 --- /dev/null +++ b/client/src/app/shared/video/abstract-video-list.html @@ -0,0 +1,19 @@ +
+
+ {{ titlePage }} +
+ +
+ + +
+
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts new file mode 100644 index 000000000..cf717cf4c --- /dev/null +++ b/client/src/app/shared/video/abstract-video-list.ts @@ -0,0 +1,121 @@ +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 ]) + } +} diff --git a/client/src/app/shared/video/sort-field.type.ts b/client/src/app/shared/video/sort-field.type.ts new file mode 100644 index 000000000..776f360f8 --- /dev/null +++ b/client/src/app/shared/video/sort-field.type.ts @@ -0,0 +1,5 @@ +export type SortField = 'name' | '-name' + | 'duration' | '-duration' + | 'createdAt' | '-createdAt' + | 'views' | '-views' + | 'likes' | '-likes' diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts new file mode 100644 index 000000000..93c380b73 --- /dev/null +++ b/client/src/app/shared/video/video-details.model.ts @@ -0,0 +1,84 @@ +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 + } +} diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts new file mode 100644 index 000000000..88d23a59f --- /dev/null +++ b/client/src/app/shared/video/video-edit.model.ts @@ -0,0 +1,50 @@ +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 + } + } +} diff --git a/client/src/app/shared/video/video-pagination.model.ts b/client/src/app/shared/video/video-pagination.model.ts new file mode 100644 index 000000000..9e71769cb --- /dev/null +++ b/client/src/app/shared/video/video-pagination.model.ts @@ -0,0 +1,5 @@ +export interface VideoPagination { + currentPage: number + itemsPerPage: number + totalItems: number +} diff --git a/client/src/app/shared/video/video-thumbnail.component.html b/client/src/app/shared/video/video-thumbnail.component.html new file mode 100644 index 000000000..5c698e8f6 --- /dev/null +++ b/client/src/app/shared/video/video-thumbnail.component.html @@ -0,0 +1,10 @@ + +video thumbnail + +
+ {{ video.durationLabel }} +
+
diff --git a/client/src/app/shared/video/video-thumbnail.component.scss b/client/src/app/shared/video/video-thumbnail.component.scss new file mode 100644 index 000000000..ab4f9bcb1 --- /dev/null +++ b/client/src/app/shared/video/video-thumbnail.component.scss @@ -0,0 +1,28 @@ +.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; + } +} diff --git a/client/src/app/shared/video/video-thumbnail.component.ts b/client/src/app/shared/video/video-thumbnail.component.ts new file mode 100644 index 000000000..e543e9903 --- /dev/null +++ b/client/src/app/shared/video/video-thumbnail.component.ts @@ -0,0 +1,12 @@ +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 +} diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts new file mode 100644 index 000000000..6929c8755 --- /dev/null +++ b/client/src/app/shared/video/video.model.ts @@ -0,0 +1,90 @@ +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)) + } +} diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts new file mode 100644 index 000000000..b2a26417c --- /dev/null +++ b/client/src/app/shared/video/video.service.ts @@ -0,0 +1,170 @@ +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 { + return this.authHttp.get(VideoService.BASE_VIDEO_URL + uuid) + .map(videoHash => new VideoDetails(videoHash)) + .catch((res) => this.restExtractor.handleError(res)) + } + + viewVideo (uuid: string): Observable { + 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>(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 { + 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) { + const videosJson = result.data + const totalVideos = result.total + const videos = [] + + for (const videoJson of videosJson) { + videos.push(new Video(videoJson)) + } + + return { videos, totalVideos } + } +} -- cgit v1.2.3 From f3aaa9a95cc2b61f1f255472d7014d08faa66561 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 5 Dec 2017 17:46:33 +0100 Subject: Fix client search --- client/src/app/shared/video/abstract-video-list.ts | 24 +++++++++++----------- client/src/app/shared/video/video.service.ts | 9 ++++---- 2 files changed, 16 insertions(+), 17 deletions(-) (limited to 'client/src/app/shared/video') diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index cf717cf4c..84ca5cbe4 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts @@ -1,25 +1,25 @@ -import { OnDestroy, OnInit } from '@angular/core' +import { 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 { +export abstract class AbstractVideoList implements OnInit { pagination: VideoPagination = { currentPage: 1, itemsPerPage: 25, totalItems: null } sort: SortField = '-createdAt' + defaultSort: SortField = '-createdAt' videos: Video[] = [] + loadOnInit = true protected notificationsService: NotificationsService protected router: Router protected route: ActivatedRoute - protected subActivatedRoute: Subscription protected abstract currentRoute: string @@ -32,13 +32,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { // Subscribe to route changes const routeParams = this.route.snapshot.params this.loadRouteParams(routeParams) - this.loadMoreVideos('after') - } - - ngOnDestroy () { - if (this.subActivatedRoute) { - this.subActivatedRoute.unsubscribe() - } + if (this.loadOnInit === true) this.loadMoreVideos('after') } onNearOfTop () { @@ -53,6 +47,12 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { } } + reloadVideos () { + this.videos = [] + this.loadedPages = {} + this.loadMoreVideos('before') + } + loadMoreVideos (where: 'before' | 'after') { if (this.loadedPages[this.pagination.currentPage] === true) return @@ -105,7 +105,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { } protected loadRouteParams (routeParams: { [ key: string ]: any }) { - this.sort = routeParams['sort'] as SortField || '-createdAt' + this.sort = routeParams['sort'] as SortField || this.defaultSort if (routeParams['page'] !== undefined) { this.pagination.currentPage = parseInt(routeParams['page'], 10) diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index b2a26417c..3f35b67c4 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts @@ -11,7 +11,7 @@ import { VideoRateType } from '../../../../../shared/models/videos/video-rate.ty 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 { Search } from '../header/search.model' import { UserService } from '../users/user.service' import { SortField } from './sort-field.type' import { VideoDetails } from './video-details.model' @@ -91,15 +91,14 @@ export class VideoService { .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) + searchVideos (search: string, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { + const url = VideoService.BASE_VIDEO_URL + 'search' const pagination = this.videoPaginationToRestPagination(videoPagination) let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) - - if (search.field) params.set('field', search.field) + params = params.append('search', search) return this.authHttp .get>(url, { params }) -- cgit v1.2.3 From a2b817d322ef4074bdaaf2589ada567f338323f4 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 6 Dec 2017 09:19:25 +0100 Subject: Better error messages --- client/src/app/shared/video/abstract-video-list.ts | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'client/src/app/shared/video') diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index 84ca5cbe4..ee1ed2cb2 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts @@ -32,6 +32,7 @@ export abstract class AbstractVideoList implements OnInit { // Subscribe to route changes const routeParams = this.route.snapshot.params this.loadRouteParams(routeParams) + if (this.loadOnInit === true) this.loadMoreVideos('after') } @@ -60,6 +61,13 @@ export abstract class AbstractVideoList implements OnInit { observable.subscribe( ({ videos, totalVideos }) => { + // Paging is too high, return to the first one + if (totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) { + this.pagination.currentPage = 1 + this.setNewRouteParams() + return this.reloadVideos() + } + this.loadedPages[this.pagination.currentPage] = true this.pagination.totalItems = totalVideos -- cgit v1.2.3 From b1fa3eba70dbd7d9e5b795ad251e293c88ebeee2 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 6 Dec 2017 17:15:59 +0100 Subject: Begin video watch design --- .../src/app/shared/video/abstract-video-list.html | 2 +- client/src/app/shared/video/video-details.model.ts | 9 +++-- .../shared/video/video-miniature.component.html | 17 +++++++++ .../shared/video/video-miniature.component.scss | 44 ++++++++++++++++++++++ .../app/shared/video/video-miniature.component.ts | 17 +++++++++ .../src/app/shared/video/video-pagination.model.ts | 2 +- client/src/app/shared/video/video.model.ts | 8 ++-- 7 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 client/src/app/shared/video/video-miniature.component.html create mode 100644 client/src/app/shared/video/video-miniature.component.scss create mode 100644 client/src/app/shared/video/video-miniature.component.ts (limited to 'client/src/app/shared/video') diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html index bd4f6b1f8..5d07a276b 100644 --- a/client/src/app/shared/video/abstract-video-list.html +++ b/client/src/app/shared/video/abstract-video-list.html @@ -12,7 +12,7 @@ > diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index 93c380b73..1a956da7c 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts @@ -1,3 +1,4 @@ +import { Account } from '../../../../../shared/models/accounts' import { Video } from '../../shared/video/video.model' import { AuthUser } from '../../core' import { @@ -10,7 +11,7 @@ import { } from '../../../../../shared' export class VideoDetails extends Video implements VideoDetailsServerModel { - account: string + accountName: string by: string createdAt: Date updatedAt: Date @@ -44,6 +45,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { channel: VideoChannel privacy: VideoPrivacy privacyLabel: string + account: Account constructor (hash: VideoDetailsServerModel) { super(hash) @@ -53,6 +55,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { this.descriptionPath = hash.descriptionPath this.files = hash.files this.channel = hash.channel + this.account = hash.account } getAppropriateMagnetUri (actualDownloadSpeed = 0) { @@ -71,7 +74,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { } isRemovableBy (user: AuthUser) { - return user && this.isLocal === true && (this.account === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) + return user && this.isLocal === true && (this.accountName === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) } isBlackistableBy (user: AuthUser) { @@ -79,6 +82,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { } isUpdatableBy (user: AuthUser) { - return user && this.isLocal === true && user.username === this.account + return user && this.isLocal === true && user.username === this.accountName } } diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html new file mode 100644 index 000000000..7ac017235 --- /dev/null +++ b/client/src/app/shared/video/video-miniature.component.html @@ -0,0 +1,17 @@ +
+ + +
+ + + {{ video.name }} + + + + {{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views + +
+
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss new file mode 100644 index 000000000..37e84897b --- /dev/null +++ b/client/src/app/shared/video/video-miniature.component.scss @@ -0,0 +1,44 @@ +.video-miniature { + display: inline-block; + padding-right: 15px; + margin-bottom: 30px; + height: 175px; + vertical-align: top; + + .video-miniature-information { + width: 200px; + margin-top: 2px; + line-height: normal; + + .video-miniature-name { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: bold; + transition: color 0.2s; + font-size: 16px; + font-weight: $font-semibold; + color: #000; + + &:hover { + text-decoration: none; + } + + &.blur-filter { + filter: blur(3px); + padding-left: 4px; + } + } + + .video-miniature-created-at-views { + display: block; + font-size: 13px; + } + + .video-miniature-account { + font-size: 13px; + color: #585858; + } + } +} diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts new file mode 100644 index 000000000..4d79a74bb --- /dev/null +++ b/client/src/app/shared/video/video-miniature.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core' +import { User } from '../users' +import { Video } from './video.model' + +@Component({ + selector: 'my-video-miniature', + styleUrls: [ './video-miniature.component.scss' ], + templateUrl: './video-miniature.component.html' +}) +export class VideoMiniatureComponent { + @Input() user: User + @Input() video: Video + + isVideoNSFWForThisUser () { + return this.video.isVideoNSFWForUser(this.user) + } +} diff --git a/client/src/app/shared/video/video-pagination.model.ts b/client/src/app/shared/video/video-pagination.model.ts index 9e71769cb..e9db61596 100644 --- a/client/src/app/shared/video/video-pagination.model.ts +++ b/client/src/app/shared/video/video-pagination.model.ts @@ -1,5 +1,5 @@ export interface VideoPagination { currentPage: number itemsPerPage: number - totalItems: number + totalItems?: number } diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index 6929c8755..d86ef8f92 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts @@ -1,8 +1,9 @@ import { Video as VideoServerModel } from '../../../../../shared' import { User } from '../' +import { Account } from '../../../../../shared/models/accounts' export class Video implements VideoServerModel { - account: string + accountName: string by: string createdAt: Date updatedAt: Date @@ -31,6 +32,7 @@ export class Video implements VideoServerModel { likes: number dislikes: number nsfw: boolean + account: Account private static createByString (account: string, serverHost: string) { return account + '@' + serverHost @@ -52,7 +54,7 @@ export class Video implements VideoServerModel { absoluteAPIUrl = window.location.origin } - this.account = hash.account + this.accountName = hash.accountName this.createdAt = new Date(hash.createdAt.toString()) this.categoryLabel = hash.categoryLabel this.category = hash.category @@ -80,7 +82,7 @@ export class Video implements VideoServerModel { this.dislikes = hash.dislikes this.nsfw = hash.nsfw - this.by = Video.createByString(hash.account, hash.serverHost) + this.by = Video.createByString(hash.accountName, hash.serverHost) } isVideoNSFWForUser (user: User) { -- cgit v1.2.3 From 6a9e1d42f878c55ac5e2af8a1c98e6fe28a04f36 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 6 Dec 2017 18:04:40 +0100 Subject: Add likes/dislikes bar --- client/src/app/shared/video/video-details.model.ts | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'client/src/app/shared/video') diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index 1a956da7c..b96f8f6c8 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts @@ -46,6 +46,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { privacy: VideoPrivacy privacyLabel: string account: Account + likesPercent: number + dislikesPercent: number constructor (hash: VideoDetailsServerModel) { super(hash) @@ -56,6 +58,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { this.files = hash.files this.channel = hash.channel this.account = hash.account + + this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 + this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 } getAppropriateMagnetUri (actualDownloadSpeed = 0) { -- cgit v1.2.3 From cadb46d832724ea1a17b085b992142aa32e212be Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 8 Dec 2017 08:39:15 +0100 Subject: Design second video upload step --- client/src/app/shared/video/video-edit.model.ts | 26 +++++++++++++------------ client/src/app/shared/video/video.service.ts | 11 +++++++---- 2 files changed, 21 insertions(+), 16 deletions(-) (limited to 'client/src/app/shared/video') diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts index 88d23a59f..955255bfa 100644 --- a/client/src/app/shared/video/video-edit.model.ts +++ b/client/src/app/shared/video/video-edit.model.ts @@ -14,18 +14,20 @@ export class VideoEdit { 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 + constructor (videoDetails?: VideoDetails) { + if (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) { diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index 3f35b67c4..1a0644c3d 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts @@ -42,14 +42,17 @@ export class VideoService { } updateVideo (video: VideoEdit) { - const language = video.language ? video.language : null + const language = video.language || undefined + const licence = video.licence || undefined + const category = video.category || undefined + const description = video.description || undefined const body: VideoUpdate = { name: video.name, - category: video.category, - licence: video.licence, + category, + licence, language, - description: video.description, + description, privacy: video.privacy, tags: video.tags, nsfw: video.nsfw -- cgit v1.2.3 From f595d3947708114deeed4312cc5ffd285745b090 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 8 Dec 2017 17:31:21 +0100 Subject: Finish admin design --- client/src/app/shared/video/abstract-video-list.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'client/src/app/shared/video') diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index ee1ed2cb2..ba1635a18 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts @@ -62,7 +62,7 @@ export abstract class AbstractVideoList implements OnInit { observable.subscribe( ({ videos, totalVideos }) => { // Paging is too high, return to the first one - if (totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) { + if (this.pagination.currentPage > 1 && totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) { this.pagination.currentPage = 1 this.setNewRouteParams() return this.reloadVideos() @@ -82,6 +82,10 @@ export abstract class AbstractVideoList implements OnInit { } protected hasMoreVideos () { + // No results + if (this.pagination.totalItems === 0) return false + + // Not loaded yet if (!this.pagination.totalItems) return true const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage -- cgit v1.2.3 From 3daf400219fe8cc94025886ba14876bc59a45244 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 11 Dec 2017 08:50:43 +0100 Subject: Responsive homepage --- client/src/app/shared/video/abstract-video-list.html | 1 + client/src/app/shared/video/abstract-video-list.scss | 7 +++++++ 2 files changed, 8 insertions(+) (limited to 'client/src/app/shared/video') diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html index 5d07a276b..5761f2c81 100644 --- a/client/src/app/shared/video/abstract-video-list.html +++ b/client/src/app/shared/video/abstract-video-list.html @@ -4,6 +4,7 @@