Design second video upload step

pull/159/head
Chocobozzz 2017-12-08 08:39:15 +01:00
parent c182778e26
commit cadb46d832
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
24 changed files with 173 additions and 98 deletions

View File

@ -194,7 +194,6 @@ export class AuthService {
}
this.mergeUserInformation(obj)
.do(() => this.userInformationLoaded.next(true))
.subscribe(
res => {
this.user.displayNSFW = res.displayNSFW
@ -203,6 +202,8 @@ export class AuthService {
this.user.account = res.account
this.user.save()
this.userInformationLoaded.next(true)
}
)
}

View File

@ -77,7 +77,6 @@ export class ServerService {
notifier: ReplaySubject<boolean>
) {
return this.http.get(ServerService.BASE_VIDEO_URL + attributeName)
.do(() => notifier.next(true))
.subscribe(data => {
Object.keys(data)
.forEach(dataKey => {
@ -86,6 +85,8 @@ export class ServerService {
label: data[dataKey]
})
})
notifier.next(true)
})
}
}

View File

@ -43,6 +43,10 @@ menu {
.logged-in-email {
font-size: 13px;
color: #C6C6C6;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 140px;
}
}

View File

@ -23,17 +23,13 @@ export const VIDEO_PRIVACY = {
}
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 = {
@ -49,9 +45,8 @@ export const VIDEO_CHANNEL = {
}
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.'
}
@ -64,10 +59,3 @@ export const VIDEO_TAGS = {
'maxlength': 'A tag should be less than 30 characters long.'
}
}
export const VIDEO_FILE = {
VALIDATORS: [ Validators.required ],
MESSAGES: {
'required': 'Video file is required.'
}
}

View File

@ -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) {

View File

@ -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

View File

@ -0,0 +1,41 @@
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;
}
}

View File

@ -1,12 +1,10 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import { Subject } from 'rxjs/Subject'
import { truncate } from 'lodash'
import 'rxjs/add/operator/debounceTime'
import 'rxjs/add/operator/distinctUntilChanged'
import { truncate } from 'lodash'
import { MarkdownService } from './markdown.service'
import { Subject } from 'rxjs/Subject'
import { MarkdownService } from '../../shared'
@Component({
selector: 'my-video-description',

View File

@ -43,6 +43,14 @@
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;
@ -54,6 +62,7 @@
background-color: inherit;
border: none;
padding: 0;
outline: 0;
}
.icon.icon-validate {

View File

@ -3,8 +3,9 @@ import { NgModule } from '@angular/core'
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({

View File

@ -15,20 +15,23 @@
</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">
@ -37,10 +40,13 @@
[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>

View File

@ -18,6 +18,7 @@
.icon.icon-upload {
@include icon(90px);
margin-bottom: 25px;
cursor: default;
background-image: url('../../../assets/images/video/upload.svg');
}
@ -58,10 +59,9 @@
}
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;
@ -76,6 +76,19 @@ p-progressBar {
.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;
}
}
}
}

View File

@ -5,6 +5,7 @@ import { Router } from '@angular/router'
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'
@ -25,6 +26,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
isUploadingVideo = false
videoUploaded = false
videoUploadPercents = 0
videoUploadedId = 0
error: string = null
form: FormGroup
@ -33,8 +35,8 @@ export class VideoAddComponent extends FormReactive implements OnInit {
userVideoChannels = []
videoPrivacies = []
firstStepPrivacy = 0
firstStepChannel = 0
firstStepPrivacyId = 0
firstStepChannelId = 0
constructor (
private formBuilder: FormBuilder,
@ -59,7 +61,9 @@ export class VideoAddComponent extends FormReactive implements OnInit {
.subscribe(
() => {
this.videoPrivacies = this.serverService.getVideoPrivacies()
this.firstStepPrivacy = this.videoPrivacies[0].id
// Public by default
this.firstStepPrivacyId = VideoPrivacy.PUBLIC
})
this.authService.userInformationLoaded
@ -72,7 +76,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
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
}
)
}
@ -89,14 +93,15 @@ export class VideoAddComponent extends FormReactive implements OnInit {
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)
@ -117,6 +122,8 @@ export class VideoAddComponent extends FormReactive implements OnInit {
console.log('Video uploaded.')
this.videoUploaded = true
this.videoUploadedId = event.body.video.id
}
},
@ -133,13 +140,16 @@ export class VideoAddComponent extends FormReactive implements OnInit {
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 => {

View File

@ -11,9 +11,9 @@
></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>

View File

@ -78,7 +78,7 @@
<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>

View File

@ -219,6 +219,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
}
private setVideoDescriptionHTML () {
if (!this.video.description) {
this.videoHTMLDescription = ''
return
}
this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description)
}

View File

@ -1,2 +1 @@
export * from './markdown.service'
export * from './video-description.component'

View File

@ -1,19 +0,0 @@
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;
}
}

View File

@ -1,5 +1,5 @@
@mixin disable-default-a-behaviour {
&:hover, &:focus {
&:hover, &:focus, &:active {
text-decoration: none !important;
outline: none !important;
}
@ -23,13 +23,15 @@
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;
}
}

View File

@ -86,6 +86,10 @@ label {
margin-top: 30px;
margin-bottom: 25px;
}
&:hover, &:active, &:focus {
color: #000;
}
}
// On small screen, menu is absolute and displayed over the page

View File

@ -15,6 +15,7 @@ import { getServerAccount } from '../../../helpers/utils'
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'
@ -39,7 +40,6 @@ import { abuseVideoRouter } from './abuse'
import { blacklistRouter } from './blacklist'
import { videoChannelRouter } from './channel'
import { rateVideoRouter } from './rate'
import { sendCreateViewToOrigin } from '../../../lib/activitypub/send/send-create'
const videosRouter = express.Router()
@ -154,17 +154,20 @@ async function addVideoRetryWrapper (req: express.Request, res: express.Response
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 = {
@ -223,7 +226,6 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
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
@ -238,15 +240,17 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
}
// 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) {

View File

@ -104,7 +104,10 @@ describe('Test a single server', function () {
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 () {

View File

@ -201,7 +201,7 @@ async function testVideoImage (url: string, imageName: string, imagePath: string
}
}
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'