dropdowns: true
button-groups: true
input-groups: true
- navs: false
+ navs: true
navbar: false
breadcrumbs: false
pagination: true
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'
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
}
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
-
- this.user.save()
- }
- )
+ .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()
+ }
+ )
}
private mergeUserInformation (obj: UserLoginWithUsername): Observable<UserLoginWithUserInformation> {
}
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.'
}
}
<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>
<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>
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: [ '' ]
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)
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
],
VideoAddComponent
],
- providers: [
- VideoService
- ]
+ providers: [ ]
})
export class VideoAddModule { }
--- /dev/null
+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 { }
</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"
<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>
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'
this.videoLanguages = this.serverService.getVideoLanguages()
const uuid: string = this.route.snapshot.params['uuid']
- this.videoService.getVideo(uuid)
- .subscribe(
- video => {
- this.video = new VideoEdit(video)
-
- this.hydrateFormFromVideo()
- },
- err => {
- console.error(err)
- this.error = 'Cannot fetch video.'
- }
- )
+ this.videoService.getVideo(uuid)
+ .switchMap(video => {
+ return this.videoService
+ .loadCompleteDescription(video.descriptionPath)
+ .do(description => video.description = description)
+ .map(() => video)
+ })
+ .subscribe(
+ video => {
+ this.video = new VideoEdit(video)
+
+ this.hydrateFormFromVideo()
+ },
+
+ err => {
+ console.error(err)
+ this.error = 'Cannot fetch video.'
+ }
+ )
}
checkForm () {
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
],
VideoUpdateComponent
],
- providers: [
- VideoService
- ]
+ providers: [ ]
})
export class VideoUpdateModule { }
<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"
</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">
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 {
video: VideoDetails = null
videoPlayerLoaded = false
videoNotFound = false
+
+ completeDescriptionShown = false
+ completeVideoDescription: string
+ shortVideoDescription: string
videoHTMLDescription = ''
private paramsSub: Subscription
)
}
+ 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()
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 = ''
})
})
- this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description)
+ this.setVideoDescriptionHTML()
this.setOpenGraphTags()
this.checkUserRating()
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
+<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>
--- /dev/null
+textarea {
+ height: 150px;
+}
+
+.previews /deep/ {
+ .nav {
+ margin-top: 10px;
+ font-size: 0.9em;
+ }
+
+ .tab-content {
+ min-height: 75px;
+ padding: 5px;
+ }
+}
--- /dev/null
+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)
+ }
+}
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
}
.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) {
"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"],
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>
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
}
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 })