Lazy description and previews to video form
This commit is contained in:
parent
8bf89b095a
commit
2de96f4d6b
|
@ -81,7 +81,7 @@ styles:
|
|||
dropdowns: true
|
||||
button-groups: true
|
||||
input-groups: true
|
||||
navs: false
|
||||
navs: true
|
||||
navbar: false
|
||||
breadcrumbs: false
|
||||
pagination: true
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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.'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 { }
|
||||
|
|
|
@ -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 { }
|
|
@ -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>
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 { }
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,15 @@
|
|||
textarea {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.previews /deep/ {
|
||||
.nav {
|
||||
margin-top: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
min-height: 75px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 })
|
||||
|
|
Loading…
Reference in New Issue