Lazy description and previews to video form

pull/125/head
Chocobozzz 2017-10-30 20:26:06 +01:00
parent 8bf89b095a
commit 2de96f4d6b
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
23 changed files with 266 additions and 73 deletions

View File

@ -81,7 +81,7 @@ styles:
dropdowns: true
button-groups: true
input-groups: true
navs: false
navs: true
navbar: false
breadcrumbs: false
pagination: true

View File

@ -3,6 +3,8 @@ import { Router } from '@angular/router'
import { Observable } from 'rxjs/Observable'
import { Subject } from 'rxjs/Subject'
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
import { ReplaySubject } from 'rxjs/ReplaySubject'
import 'rxjs/add/operator/do'
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/mergeMap'
import 'rxjs/add/observable/throw'
@ -54,6 +56,7 @@ export class AuthService {
private static BASE_USER_INFORMATION_URL = API_URL + '/api/v1/users/me'
loginChangedSource: Observable<AuthStatus>
userInformationLoaded = new ReplaySubject<boolean>(1)
private clientId: string
private clientSecret: string
@ -199,16 +202,17 @@ export class AuthService {
}
this.mergeUserInformation(obj)
.subscribe(
res => {
this.user.displayNSFW = res.displayNSFW
this.user.role = res.role
this.user.videoChannels = res.videoChannels
this.user.author = res.author
.do(() => this.userInformationLoaded.next(true))
.subscribe(
res => {
this.user.displayNSFW = res.displayNSFW
this.user.role = res.role
this.user.videoChannels = res.videoChannels
this.user.author = res.author
this.user.save()
}
)
this.user.save()
}
)
}
private mergeUserInformation (obj: UserLoginWithUsername): Observable<UserLoginWithUserInformation> {

View File

@ -36,11 +36,11 @@ export const VIDEO_CHANNEL = {
}
export const VIDEO_DESCRIPTION = {
VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(250) ],
VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(3000) ],
MESSAGES: {
'required': 'Video description is required.',
'minlength': 'Video description must be at least 3 characters long.',
'maxlength': 'Video description cannot be more than 250 characters long.'
'maxlength': 'Video description cannot be more than 3000 characters long.'
}
}

View File

@ -28,7 +28,6 @@
<div class="form-group">
<label for="category">Channel</label>
<select class="form-control" id="channelId" formControlName="channelId">
<option></option>
<option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
</select>
@ -103,11 +102,8 @@
<div class="form-group">
<label for="description">Description</label>
<textarea
id="description" class="form-control" placeholder="Description..."
formControlName="description"
>
</textarea>
<my-video-description formControlName="description"></my-video-description>
<div *ngIf="formErrors.description" class="alert alert-danger">
{{ formErrors.description }}
</div>

View File

@ -82,7 +82,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
category: [ '', VIDEO_CATEGORY.VALIDATORS ],
licence: [ '', VIDEO_LICENCE.VALIDATORS ],
language: [ '', VIDEO_LANGUAGE.VALIDATORS ],
channelId: [ this.userVideoChannels[0].id, VIDEO_CHANNEL.VALIDATORS ],
channelId: [ '', VIDEO_CHANNEL.VALIDATORS ],
description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
videofile: [ '', VIDEO_FILE.VALIDATORS ],
tags: [ '' ]
@ -96,10 +96,22 @@ export class VideoAddComponent extends FormReactive implements OnInit {
this.videoLicences = this.serverService.getVideoLicences()
this.videoLanguages = this.serverService.getVideoLanguages()
const user = this.authService.getUser()
this.userVideoChannels = user.videoChannels.map(v => ({ id: v.id, label: v.name }))
this.buildForm()
this.authService.userInformationLoaded
.subscribe(
() => {
const user = this.authService.getUser()
if (!user) return
const videoChannels = user.videoChannels
if (Array.isArray(videoChannels) === false) return
this.userVideoChannels = videoChannels.map(v => ({ id: v.id, label: v.name }))
this.form.patchValue({ channelId: this.userVideoChannels[0].id })
}
)
}
// The goal is to keep reactive form validation (required field)

View File

@ -1,17 +1,14 @@
import { NgModule } from '@angular/core'
import { TagInputModule } from 'ngx-chips'
import { VideoAddRoutingModule } from './video-add-routing.module'
import { VideoAddComponent } from './video-add.component'
import { VideoService } from '../shared'
import { VideoEditModule } from './video-edit.module'
import { SharedModule } from '../../shared'
@NgModule({
imports: [
TagInputModule,
VideoAddRoutingModule,
VideoEditModule,
SharedModule
],
@ -23,8 +20,6 @@ import { SharedModule } from '../../shared'
VideoAddComponent
],
providers: [
VideoService
]
providers: [ ]
})
export class VideoAddModule { }

View File

@ -0,0 +1,33 @@
import { NgModule } from '@angular/core'
import { TagInputModule } from 'ngx-chips'
import { TabsModule } from 'ngx-bootstrap/tabs'
import { VideoService, MarkdownService, VideoDescriptionComponent } from '../shared'
import { SharedModule } from '../../shared'
@NgModule({
imports: [
TagInputModule,
TabsModule.forRoot(),
SharedModule
],
declarations: [
VideoDescriptionComponent
],
exports: [
TagInputModule,
TabsModule,
VideoDescriptionComponent
],
providers: [
VideoService,
MarkdownService
]
})
export class VideoEditModule { }

View File

@ -62,7 +62,7 @@
</div>
<div class="form-group">
<label for="tags" class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span>
<label class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span>
<tag-input
[ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
formControlName="tags" maxItems="5" modelAsStrings="true"
@ -71,11 +71,8 @@
<div class="form-group">
<label for="description">Description</label>
<textarea
id="description" class="form-control" placeholder="Description..."
formControlName="description"
>
</textarea>
<my-video-description formControlName="description"></my-video-description>
<div *ngIf="formErrors.description" class="alert alert-danger">
{{ formErrors.description }}
</div>

View File

@ -1,6 +1,8 @@
import { Component, OnInit } from '@angular/core'
import { FormBuilder, FormGroup } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router'
import { Observable } from 'rxjs/Observable'
import 'rxjs/add/observable/forkJoin'
import { NotificationsService } from 'angular2-notifications'
@ -84,19 +86,26 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
this.videoLanguages = this.serverService.getVideoLanguages()
const uuid: string = this.route.snapshot.params['uuid']
this.videoService.getVideo(uuid)
.subscribe(
video => {
this.video = new VideoEdit(video)
.switchMap(video => {
return this.videoService
.loadCompleteDescription(video.descriptionPath)
.do(description => video.description = description)
.map(() => video)
})
.subscribe(
video => {
this.video = new VideoEdit(video)
this.hydrateFormFromVideo()
},
this.hydrateFormFromVideo()
},
err => {
console.error(err)
this.error = 'Cannot fetch video.'
}
)
err => {
console.error(err)
this.error = 'Cannot fetch video.'
}
)
}
checkForm () {

View File

@ -1,17 +1,14 @@
import { NgModule } from '@angular/core'
import { TagInputModule } from 'ngx-chips'
import { VideoUpdateRoutingModule } from './video-update-routing.module'
import { VideoUpdateComponent } from './video-update.component'
import { VideoService } from '../shared'
import { VideoEditModule } from './video-edit.module'
import { SharedModule } from '../../shared'
@NgModule({
imports: [
TagInputModule,
VideoUpdateRoutingModule,
VideoEditModule,
SharedModule
],
@ -23,8 +20,6 @@ import { SharedModule } from '../../shared'
VideoUpdateComponent
],
providers: [
VideoService
]
providers: [ ]
})
export class VideoUpdateModule { }

View File

@ -13,7 +13,7 @@
<form novalidate [formGroup]="form">
<div class="form-group">
<label for="description">Reason</label>
<label for="reason">Reason</label>
<textarea
id="reason" class="form-control" placeholder="Reason..."
formControlName="reason"

View File

@ -129,6 +129,16 @@
</div>
<div class="video-details-description" [innerHTML]="videoHTMLDescription"></div>
<div *ngIf="completeDescriptionShown === false && video.description.length === 250" (click)="showMoreDescription()" class="video-details-description-more">
Show more
<span class="glyphicon glyphicon-menu-down"></span>
</div>
<div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-details-description-more">
Show less
<span class="glyphicon glyphicon-menu-up"></span>
</div>
</div>
<div class="video-details-attributes col-xs-4 col-md-3">

View File

@ -170,6 +170,18 @@
font-weight: bold;
margin-bottom: 30px;
}
.video-details-description-more {
cursor: pointer;
margin-top: 15px;
font-weight: bold;
color: #acaeb7;
.glyphicon {
position: relative;
top: 2px;
}
}
}
.video-details-attributes {

View File

@ -38,6 +38,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
video: VideoDetails = null
videoPlayerLoaded = false
videoNotFound = false
completeDescriptionShown = false
completeVideoDescription: string
shortVideoDescription: string
videoHTMLDescription = ''
private paramsSub: Subscription
@ -154,6 +158,36 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
)
}
showMoreDescription () {
this.completeDescriptionShown = true
if (this.completeVideoDescription === undefined) {
return this.loadCompleteDescription()
}
this.updateVideoDescription(this.completeVideoDescription)
}
showLessDescription () {
this.completeDescriptionShown = false
this.updateVideoDescription(this.shortVideoDescription)
}
loadCompleteDescription () {
this.videoService.loadCompleteDescription(this.video.descriptionPath)
.subscribe(
description => {
this.shortVideoDescription = this.video.description
this.completeVideoDescription = description
this.updateVideoDescription(this.completeVideoDescription)
},
error => this.notificationsService.error('Error', error.text)
)
}
showReportModal (event: Event) {
event.preventDefault()
this.videoReportModal.show()
@ -184,6 +218,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
return this.video.isBlackistableBy(this.authService.getUser())
}
private updateVideoDescription (description: string) {
this.video.description = description
this.setVideoDescriptionHTML()
}
private setVideoDescriptionHTML () {
this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description)
}
private handleError (err: any) {
const errorMessage: string = typeof err === 'string' ? err : err.message
let message = ''
@ -264,7 +307,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
})
})
this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description)
this.setVideoDescriptionHTML()
this.setOpenGraphTags()
this.checkUserRating()

View File

@ -4,4 +4,5 @@ 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'

View File

@ -0,0 +1,9 @@
<textarea
[(ngModel)]="description" (ngModelChange)="onModelChange()"
id="description" class="form-control" placeholder="My super video">
</textarea>
<tabset #staticTabs class="previews">
<tab heading="Truncated description preview" [innerHTML]="truncatedDescriptionHTML"></tab>
<tab heading="Complete description preview" [innerHTML]="descriptionHTML"></tab>
</tabset>

View File

@ -0,0 +1,15 @@
textarea {
height: 150px;
}
.previews /deep/ {
.nav {
margin-top: 10px;
font-size: 0.9em;
}
.tab-content {
min-height: 75px;
padding: 5px;
}
}

View File

@ -0,0 +1,68 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import { Subject } from 'rxjs/Subject'
import 'rxjs/add/operator/debounceTime'
import 'rxjs/add/operator/distinctUntilChanged'
import { truncate } from 'lodash'
import { MarkdownService } from './markdown.service'
@Component({
selector: 'my-video-description',
templateUrl: './video-description.component.html',
styleUrls: [ './video-description.component.scss' ],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => VideoDescriptionComponent),
multi: true
}
]
})
export class VideoDescriptionComponent implements ControlValueAccessor, OnInit {
@Input() description = ''
truncatedDescriptionHTML = ''
descriptionHTML = ''
private descriptionChanged = new Subject<string>()
constructor (private markdownService: MarkdownService) {}
ngOnInit () {
this.descriptionChanged
.debounceTime(150)
.distinctUntilChanged()
.subscribe(() => this.updateDescriptionPreviews())
this.descriptionChanged.next(this.description)
}
propagateChange = (_: any) => { /* empty */ }
writeValue (description: string) {
this.description = description
this.descriptionChanged.next(this.description)
}
registerOnChange (fn: (_: any) => void) {
this.propagateChange = fn
}
registerOnTouched () {
// Unused
}
onModelChange () {
this.propagateChange(this.description)
this.descriptionChanged.next(this.description)
}
private updateDescriptionPreviews () {
this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 }))
this.descriptionHTML = this.markdownService.markdownToHTML(this.description)
}
}

View File

@ -38,12 +38,14 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
likes: number
dislikes: number
nsfw: boolean
descriptionPath: string
files: VideoFile[]
channel: VideoChannel
constructor (hash: VideoDetailsServerModel) {
super(hash)
this.descriptionPath = hash.descriptionPath
this.files = hash.files
this.channel = hash.channel
}

View File

@ -99,15 +99,11 @@ export class VideoService {
.catch((res) => this.restExtractor.handleError(res))
}
reportVideo (id: number, reason: string) {
const url = VideoService.BASE_VIDEO_URL + id + '/abuse'
const body: VideoAbuseCreate = {
reason
}
return this.authHttp.post(url, body)
.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) {

View File

@ -21,7 +21,7 @@
"no-attribute-parameter-decorator": true,
"no-input-rename": true,
"no-output-rename": true,
"no-forward-ref": true,
"no-forward-ref": false,
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"pipe-naming": [true, "camelCase", "my"],

View File

@ -138,7 +138,7 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
getEmbedPath: VideoMethods.GetEmbedPath
getDescriptionPath: VideoMethods.GetDescriptionPath
getTruncatedDescription : VideoMethods.GetTruncatedDescription
getTruncatedDescription: VideoMethods.GetTruncatedDescription
setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>

View File

@ -280,9 +280,7 @@ describe('Test videos API validator', function () {
licence: 1,
language: 6,
nsfw: false,
description: 'my super description which is very very very very very very very very very very very very very very' +
'very very very very very very very very very very very very very very very very very very very very very' +
'very very very very very very very very very very very very very very very long',
description: 'my super description which is very very very very very very very very very very very very very very long'.repeat(35),
tags: [ 'tag1', 'tag2' ],
channelId
}
@ -617,9 +615,7 @@ describe('Test videos API validator', function () {
licence: 2,
language: 6,
nsfw: false,
description: 'my super description which is very very very very very very very very very very very very very very' +
'very very very very very very very very very very very very very very very very very very very very very' +
'very very very very very very very very very very very very very very very long',
description: 'my super description which is very very very very very very very very very very very very very long'.repeat(35),
tags: [ 'tag1', 'tag2' ]
}
await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })