}
this.mergeUserInformation(obj)
- .do(() => this.userInformationLoaded.next(true))
.subscribe(
res => {
this.user.displayNSFW = res.displayNSFW
this.user.account = res.account
this.user.save()
+
+ this.userInformationLoaded.next(true)
}
)
}
notifier: ReplaySubject<boolean>
) {
return this.http.get(ServerService.BASE_VIDEO_URL + attributeName)
- .do(() => notifier.next(true))
.subscribe(data => {
Object.keys(data)
.forEach(dataKey => {
label: data[dataKey]
})
})
+
+ notifier.next(true)
})
}
}
.logged-in-email {
font-size: 13px;
color: #C6C6C6;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 140px;
}
}
}
export const VIDEO_CATEGORY = {
- VALIDATORS: [ Validators.required ],
- MESSAGES: {
- 'required': 'Video category is required.'
- }
+ VALIDATORS: [ ],
+ MESSAGES: {}
}
export const VIDEO_LICENCE = {
- VALIDATORS: [ Validators.required ],
- MESSAGES: {
- 'required': 'Video licence is required.'
- }
+ VALIDATORS: [ ],
+ MESSAGES: {}
}
export const VIDEO_LANGUAGE = {
}
export const VIDEO_DESCRIPTION = {
- VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(3000) ],
+ VALIDATORS: [ 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 3000 characters long.'
}
'maxlength': 'A tag should be less than 30 characters long.'
}
}
-
-export const VIDEO_FILE = {
- VALIDATORS: [ Validators.required ],
- MESSAGES: {
- 'required': 'Video file is required.'
- }
-}
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) {
}
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
--- /dev/null
+<textarea
+ [(ngModel)]="description" (ngModelChange)="onModelChange()"
+ id="description" 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 {
+ @include peertube-input-text(100%);
+
+ padding: 5px 15px;
+ font-size: 15px;
+ height: 150px;
+}
+
+.previews /deep/ {
+ font-size: 15px !important;
+
+ .nav {
+ margin-top: 10px;
+ font-size: 16px !important;
+ border: none !important;
+
+ .nav-item .nav-link {
+ color: #000 !important;
+ height: 30px !important;
+ margin-right: 30px;
+ padding: 0 15px;
+ display: flex;
+ align-items: center;
+ border-radius: 3px;
+ border: none !important;
+
+ &.active, &:hover {
+ background-color: #F0F0F0;
+ }
+
+ &.active {
+ font-weight: $font-semibold !important;
+ }
+ }
+ }
+
+ .tab-content {
+ min-height: 75px;
+ padding: 15px;
+ }
+}
--- /dev/null
+import { Component, forwardRef, Input, OnInit } from '@angular/core'
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
+import { truncate } from 'lodash'
+import 'rxjs/add/operator/debounceTime'
+import 'rxjs/add/operator/distinctUntilChanged'
+import { Subject } from 'rxjs/Subject'
+import { MarkdownService } from '../../shared'
+
+@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)
+ }
+}
position: relative;
bottom: $button-height;
+ .message-submit {
+ display: inline-block;
+ margin-right: 25px;
+
+ color: #585858;
+ font-size: 15px;
+ }
+
.submit-button {
@include peertube-button;
@include orange-button;
background-color: inherit;
border: none;
padding: 0;
+ outline: 0;
}
.icon.icon-validate {
import { TagInputModule } from 'ngx-chips'
import { TabsModule } from 'ngx-bootstrap/tabs'
-import { MarkdownService, VideoDescriptionComponent } from '../../shared'
+import { MarkdownService } from '../../shared'
import { SharedModule } from '../../../shared'
+import { VideoDescriptionComponent } from './video-description.component'
import { VideoEditComponent } from './video-edit.component'
@NgModule({
</div>
<div class="form-group">
- <select [(ngModel)]="firstStepPrivacy">
+ <select [(ngModel)]="firstStepPrivacyId">
<option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
</select>
</div>
<div class="form-group">
- <select [(ngModel)]="firstStepChannel">
+ <select [(ngModel)]="firstStepChannelId">
<option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
</select>
</div>
</div>
</div>
- <p-progressBar *ngIf="isUploadingVideo" [value]="videoUploadPercents"></p-progressBar>
+ <p-progressBar
+ *ngIf="isUploadingVideo" [value]="videoUploadPercents"
+ [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
+ ></p-progressBar>
<!-- Hidden because we need to load the component -->
<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
[validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies"
></my-video-edit>
+
<div class="submit-container">
- <div class="submit-button" [ngClass]="{ disabled: !form.valid }">
+ <div *ngIf="videoUploaded === false" class="message-submit">Publish will be available when upload is finished</div>
+
+ <div class="submit-button" (click)="updateSecondStep()" [ngClass]="{ disabled: !form.valid || videoUploaded !== true }">
<span class="icon icon-validate"></span>
- <input type="button" value="Publish" (click)="upload()" />
+ <input type="button" value="Publish" />
</div>
</div>
</form>
.icon.icon-upload {
@include icon(90px);
margin-bottom: 25px;
+ cursor: default;
background-image: url('../../../assets/images/video/upload.svg');
}
}
p-progressBar {
- margin-top: 50px;
- margin-bottom: 40px;
-
/deep/ .ui-progressbar {
+ margin-top: 25px !important;
+ margin-bottom: 40px !important;
font-size: 15px !important;
color: #fff !important;
height: 30px !important;
.ui-progressbar-label {
text-align: left;
padding-left: 18px;
+ margin-top: 0 !important;
+ }
+ }
+
+ &.processing {
+ /deep/ .ui-progressbar-label {
+ // Same color as background to hide "100%"
+ color: rgba(11, 204, 41, 0.16) !important;
+
+ &::before {
+ content: 'Processing...';
+ color: #fff;
+ }
}
}
}
import { NotificationsService } from 'angular2-notifications'
import { VideoService } from 'app/shared/video/video.service'
import { VideoCreate } from '../../../../../shared'
+import { VideoPrivacy } from '../../../../../shared/models/videos'
import { AuthService, ServerService } from '../../core'
import { FormReactive } from '../../shared'
import { ValidatorMessage } from '../../shared/forms/form-validators'
isUploadingVideo = false
videoUploaded = false
videoUploadPercents = 0
+ videoUploadedId = 0
error: string = null
form: FormGroup
userVideoChannels = []
videoPrivacies = []
- firstStepPrivacy = 0
- firstStepChannel = 0
+ firstStepPrivacyId = 0
+ firstStepChannelId = 0
constructor (
private formBuilder: FormBuilder,
.subscribe(
() => {
this.videoPrivacies = this.serverService.getVideoPrivacies()
- this.firstStepPrivacy = this.videoPrivacies[0].id
+
+ // Public by default
+ this.firstStepPrivacyId = VideoPrivacy.PUBLIC
})
this.authService.userInformationLoaded
if (Array.isArray(videoChannels) === false) return
this.userVideoChannels = videoChannels.map(v => ({ id: v.id, label: v.name }))
- this.firstStepChannel = this.userVideoChannels[0].id
+ this.firstStepChannelId = this.userVideoChannels[0].id
}
)
}
uploadFirstStep () {
const videofile = this.videofileInput.nativeElement.files[0]
- const name = videofile.name
- const privacy = this.firstStepPrivacy.toString()
+ const name = videofile.name.replace(/\.[^/.]+$/, '')
+ const privacy = this.firstStepPrivacyId.toString()
const nsfw = false
- const channelId = this.firstStepChannel.toString()
+ const channelId = this.firstStepChannelId.toString()
const formData = new FormData()
formData.append('name', name)
- formData.append('privacy', privacy.toString())
+ // Put the video "private" -> we wait he validates the second step
+ formData.append('privacy', VideoPrivacy.PRIVATE.toString())
formData.append('nsfw', '' + nsfw)
formData.append('channelId', '' + channelId)
formData.append('videofile', videofile)
console.log('Video uploaded.')
this.videoUploaded = true
+
+ this.videoUploadedId = event.body.video.id
}
},
return
}
- const video = new VideoEdit(this.form.value)
+ const video = new VideoEdit()
+ video.patch(this.form.value)
+ video.channel = this.firstStepChannelId
+ video.id = this.videoUploadedId
this.videoService.updateVideo(video)
.subscribe(
() => {
this.notificationsService.success('Success', 'Video published.')
- this.router.navigate([ '/videos/watch', video.uuid ])
+ this.router.navigate([ '/videos/watch', video.id ])
},
err => {
></my-video-edit>
<div class="submit-container">
- <div class="submit-button" [ngClass]="{ disabled: !form.valid }">
+ <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid }">
<span class="icon icon-validate"></span>
- <input type="button" value="Update" (click)="update()" />
+ <input type="button" value="Update" />
</div>
</div>
</form>
<div class="video-info-description">
<div class="video-info-description-html" [innerHTML]="videoHTMLDescription"></div>
- <div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description.length === 250" (click)="showMoreDescription()">
+ <div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description?.length === 250" (click)="showMoreDescription()">
Show more
<span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span>
<my-loader class="description-loading" [loading]="descriptionLoading"></my-loader>
}
private setVideoDescriptionHTML () {
+ if (!this.video.description) {
+ this.videoHTMLDescription = ''
+ return
+ }
+
this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description)
}
export * from './markdown.service'
-export * from './video-description.component'
+++ /dev/null
-<textarea
- [(ngModel)]="description" (ngModelChange)="onModelChange()"
- id="description" 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 {
- @include peertube-input-text(100%);
-
- font-size: 15px;
- height: 150px;
-}
-
-.previews /deep/ {
- font-size: 15px !important;
-
- .nav {
- margin-top: 10px;
- }
-
- .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)
- }
-}
@mixin disable-default-a-behaviour {
- &:hover, &:focus {
+ &:hover, &:focus, &:active {
text-decoration: none !important;
outline: none !important;
}
color: #fff;
background-color: $orange-color;
- &:hover, &:active, &:focus, &[disabled], &.disabled {
+ &:hover, &:active, &:focus {
color: #fff;
background-color: $orange-hoover-color;
}
&[disabled], &.disabled {
cursor: default;
+ color: #fff;
+ background-color: #C6C6C6;
}
}
margin-top: 30px;
margin-bottom: 25px;
}
+
+ &:hover, &:active, &:focus {
+ color: #000;
+}
}
// On small screen, menu is absolute and displayed over the page
import { CONFIG, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES } from '../../../initializers'
import { database as db } from '../../../initializers/database'
import { sendAddVideo } from '../../../lib/activitypub/send/send-add'
+import { sendCreateViewToOrigin } from '../../../lib/activitypub/send/send-create'
import { sendUpdateVideo } from '../../../lib/activitypub/send/send-update'
import { shareVideoByServer } from '../../../lib/activitypub/share'
import { getVideoActivityPubUrl } from '../../../lib/activitypub/url'
import { blacklistRouter } from './blacklist'
import { videoChannelRouter } from './channel'
import { rateVideoRouter } from './rate'
-import { sendCreateViewToOrigin } from '../../../lib/activitypub/send/send-create'
const videosRouter = express.Router()
errorMessage: 'Cannot insert the video with many retries.'
}
- await retryTransactionWrapper(addVideo, options)
+ const video = await retryTransactionWrapper(addVideo, options)
- // TODO : include Location of the new video -> 201
- res.type('json').status(204).end()
+ res.json({
+ video: {
+ id: video.id,
+ uuid: video.uuid
+ }
+ }).end()
}
-async function addVideo (req: express.Request, res: express.Response, videoPhysicalFile: Express.Multer.File) {
+function addVideo (req: express.Request, res: express.Response, videoPhysicalFile: Express.Multer.File) {
const videoInfo: VideoCreate = req.body
- let videoUUID = ''
- await db.sequelize.transaction(async t => {
+ return db.sequelize.transaction(async t => {
const sequelizeOptions = { transaction: t }
const videoData = {
const videoCreated = await video.save(sequelizeOptions)
// Do not forget to add video channel information to the created video
videoCreated.VideoChannel = res.locals.videoChannel
- videoUUID = videoCreated.uuid
videoFile.videoId = video.id
}
// Let transcoding job send the video to friends because the video file extension might change
- if (CONFIG.TRANSCODING.ENABLED === true) return undefined
+ if (CONFIG.TRANSCODING.ENABLED === true) return videoCreated
// Don't send video to remote servers, it is private
- if (video.privacy === VideoPrivacy.PRIVATE) return undefined
+ if (video.privacy === VideoPrivacy.PRIVATE) return videoCreated
await sendAddVideo(video, t)
await shareVideoByServer(video, t)
- })
- logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoUUID)
+ logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
+
+ return videoCreated
+ })
}
async function updateVideoRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
licence: 6,
tags: [ 'tag1', 'tag2', 'tag3' ]
}
- await uploadVideo(server.url, server.accessToken, videoAttributes)
+ const res = await uploadVideo(server.url, server.accessToken, videoAttributes)
+ expect(res.body.video).to.not.be.undefined
+ expect(res.body.video.id).to.equal(1)
+ expect(res.body.video.uuid).to.have.length.above(5)
})
it('Should seed the uploaded video', async function () {
}
}
-async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 204) {
+async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 201) {
const path = '/api/v1/videos/upload'
let defaultChannelId = '1'