+++ /dev/null
-import { FormReactive } from '@app/shared'
-import { OnInit } from '@angular/core'
-import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
-import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
-import { VideoConstant, VideoPrivacy } from '../../../../../../shared/models/videos'
-import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
-import { LoadingBarService } from '@ngx-loading-bar/core'
-import { NotificationsService } from 'angular2-notifications'
-import { AuthService, ServerService } from '@app/core'
-import { VideoService } from '@app/shared/video/video.service'
-import { VideoCaptionService } from '@app/shared/video-caption'
-import { catchError, switchMap, tap } from 'rxjs/operators'
-import { VideoEdit } from '@app/shared/video/video-edit.model'
-
-export abstract class VideoSend extends FormReactive implements OnInit, CanComponentDeactivate {
-
- userVideoChannels: { id: number, label: string, support: string }[] = []
- videoPrivacies: VideoConstant<string>[] = []
- videoCaptions: VideoCaptionEdit[] = []
-
- firstStepPrivacyId = 0
- firstStepChannelId = 0
-
- protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy
-
- protected loadingBar: LoadingBarService
- protected notificationsService: NotificationsService
- protected authService: AuthService
- protected serverService: ServerService
- protected videoService: VideoService
- protected videoCaptionService: VideoCaptionService
-
- abstract canDeactivate ()
-
- ngOnInit () {
- this.buildForm({})
-
- populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
- .then(() => this.firstStepChannelId = this.userVideoChannels[ 0 ].id)
-
- this.serverService.videoPrivaciesLoaded
- .subscribe(
- () => {
- this.videoPrivacies = this.serverService.getVideoPrivacies()
-
- this.firstStepPrivacyId = this.DEFAULT_VIDEO_PRIVACY
- })
- }
-
- checkForm () {
- this.forceCheck()
-
- return this.form.valid
- }
-
- protected updateVideoAndCaptions (video: VideoEdit) {
- this.loadingBar.start()
-
- return this.videoService.updateVideo(video)
- .pipe(
- // Then update captions
- switchMap(() => this.videoCaptionService.updateCaptions(video.id, this.videoCaptions)),
- tap(() => this.loadingBar.complete()),
- catchError(err => {
- this.loadingBar.complete()
- throw err
- })
- )
- }
-}
--- /dev/null
+<div *ngIf="!hasImportedVideo" class="upload-video-container">
+ <div class="import-video">
+ <div class="icon icon-upload"></div>
+
+ <div class="form-group">
+ <label i18n for="targetUrl">URL</label>
+ <my-help
+ helpType="custom" i18n-customHtml
+ customHtml="You can import any URL <a href='https://rg3.github.io/youtube-dl/supportedsites.html' target='_blank' rel='noopener noreferrer'>supported by youtube-dl</a> or URL that points to a raw MP4 file. You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance."
+ ></my-help>
+
+ <input type="text" id="targetUrl" [(ngModel)]="targetUrl" />
+ </div>
+
+ <div class="form-group">
+ <label i18n for="first-step-channel">Channel</label>
+ <div class="peertube-select-container">
+ <select id="first-step-channel" [(ngModel)]="firstStepChannelId">
+ <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label i18n for="first-step-privacy">Privacy</label>
+ <div class="peertube-select-container">
+ <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId">
+ <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
+ </select>
+ </div>
+ </div>
+
+ <input
+ type="button" i18n-value value="Import"
+ [disabled]="!isTargetUrlValid() || isImportingVideo" (click)="importVideo()"
+ />
+ </div>
+</div>
+
+<div *ngIf="hasImportedVideo" class="alert alert-info" i18n>
+ Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
+</div>
+
+<!-- Hidden because we want to load the component -->
+<form [hidden]="!hasImportedVideo" novalidate [formGroup]="form">
+ <my-video-edit
+ [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
+ [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
+ ></my-video-edit>
+
+ <div class="submit-container">
+ <div class="submit-button"
+ (click)="updateSecondStep()"
+ [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"
+ >
+ <span class="icon icon-validate"></span>
+ <input type="button" i18n-value value="Update" />
+ </div>
+ </div>
+</form>
--- /dev/null
+@import 'variables';
+@import 'mixins';
+
+$width-size: 190px;
+
+.peertube-select-container {
+ @include peertube-select-container($width-size);
+}
+
+.import-video {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .icon.icon-upload {
+ @include icon(90px);
+ margin-bottom: 25px;
+ cursor: default;
+
+ background-image: url('../../../../assets/images/video/upload.svg');
+ }
+
+ input[type=text] {
+ @include peertube-input-text($width-size);
+ display: block;
+ }
+
+ input[type=button] {
+ @include peertube-button;
+ @include orange-button;
+
+ width: $width-size;
+ margin-top: 30px;
+ }
+}
+
+
--- /dev/null
+import { Component, EventEmitter, OnInit, Output } from '@angular/core'
+import { Router } from '@angular/router'
+import { NotificationsService } from 'angular2-notifications'
+import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos'
+import { AuthService, ServerService } from '../../../core'
+import { VideoService } from '../../../shared/video/video.service'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { LoadingBarService } from '@ngx-loading-bar/core'
+import { VideoSend } from '@app/videos/+video-edit/video-add-components/video-send'
+import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
+import { VideoEdit } from '@app/shared/video/video-edit.model'
+import { FormValidatorService } from '@app/shared'
+import { VideoCaptionService } from '@app/shared/video-caption'
+import { VideoImportService } from '@app/shared/video-import'
+
+@Component({
+ selector: 'my-video-import-url',
+ templateUrl: './video-import-url.component.html',
+ styleUrls: [
+ '../shared/video-edit.component.scss',
+ './video-import-url.component.scss'
+ ]
+})
+export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
+ @Output() firstStepDone = new EventEmitter<string>()
+
+ targetUrl = ''
+ videoFileName: string
+
+ isImportingVideo = false
+ hasImportedVideo = false
+ isUpdatingVideo = false
+
+ video: VideoEdit
+
+ protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PRIVATE
+
+ constructor (
+ protected formValidatorService: FormValidatorService,
+ protected loadingBar: LoadingBarService,
+ protected notificationsService: NotificationsService,
+ protected authService: AuthService,
+ protected serverService: ServerService,
+ protected videoService: VideoService,
+ protected videoCaptionService: VideoCaptionService,
+ private router: Router,
+ private videoImportService: VideoImportService,
+ private i18n: I18n
+ ) {
+ super()
+ }
+
+ ngOnInit () {
+ super.ngOnInit()
+ }
+
+ canDeactivate () {
+ return { canDeactivate: true }
+ }
+
+ isTargetUrlValid () {
+ return this.targetUrl && this.targetUrl.match(/https?:\/\//)
+ }
+
+ importVideo () {
+ this.isImportingVideo = true
+
+ const videoUpdate: VideoUpdate = {
+ privacy: this.firstStepPrivacyId,
+ waitTranscoding: false,
+ commentsEnabled: true,
+ channelId: this.firstStepChannelId
+ }
+
+ this.loadingBar.start()
+
+ this.videoImportService.importVideo(this.targetUrl, videoUpdate).subscribe(
+ res => {
+ this.loadingBar.complete()
+ this.firstStepDone.emit(res.video.name)
+ this.isImportingVideo = false
+ this.hasImportedVideo = true
+
+ this.video = new VideoEdit(Object.assign(res.video, {
+ commentsEnabled: videoUpdate.commentsEnabled,
+ support: null,
+ thumbnailUrl: null,
+ previewUrl: null
+ }))
+ this.hydrateFormFromVideo()
+ },
+
+ err => {
+ this.loadingBar.complete()
+ this.isImportingVideo = false
+ this.notificationsService.error(this.i18n('Error'), err.message)
+ }
+ )
+ }
+
+ updateSecondStep () {
+ if (this.checkForm() === false) {
+ return
+ }
+
+ this.video.patch(this.form.value)
+
+ this.isUpdatingVideo = true
+
+ // Update the video
+ this.updateVideoAndCaptions(this.video)
+ .subscribe(
+ () => {
+ this.isUpdatingVideo = false
+ this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.'))
+
+ this.router.navigate([ '/my-account', 'video-imports' ])
+ },
+
+ err => {
+ this.isUpdatingVideo = false
+ this.notificationsService.error(this.i18n('Error'), err.message)
+ console.error(err)
+ }
+ )
+
+ }
+
+ private hydrateFormFromVideo () {
+ this.form.patchValue(this.video.toFormPatch())
+ }
+}
--- /dev/null
+import { EventEmitter, OnInit } from '@angular/core'
+import { LoadingBarService } from '@ngx-loading-bar/core'
+import { NotificationsService } from 'angular2-notifications'
+import { catchError, switchMap, tap } from 'rxjs/operators'
+import { FormReactive } from '@app/shared'
+import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
+import { VideoConstant, VideoPrivacy } from '../../../../../../shared'
+import { AuthService, ServerService } from '@app/core'
+import { VideoService } from '@app/shared/video/video.service'
+import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
+import { VideoCaptionService } from '@app/shared/video-caption'
+import { VideoEdit } from '@app/shared/video/video-edit.model'
+import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
+
+export abstract class VideoSend extends FormReactive implements OnInit, CanComponentDeactivate {
+
+ userVideoChannels: { id: number, label: string, support: string }[] = []
+ videoPrivacies: VideoConstant<string>[] = []
+ videoCaptions: VideoCaptionEdit[] = []
+
+ firstStepPrivacyId = 0
+ firstStepChannelId = 0
+
+ abstract firstStepDone: EventEmitter<string>
+ protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy
+
+ protected loadingBar: LoadingBarService
+ protected notificationsService: NotificationsService
+ protected authService: AuthService
+ protected serverService: ServerService
+ protected videoService: VideoService
+ protected videoCaptionService: VideoCaptionService
+
+ abstract canDeactivate ()
+
+ ngOnInit () {
+ this.buildForm({})
+
+ populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
+ .then(() => this.firstStepChannelId = this.userVideoChannels[ 0 ].id)
+
+ this.serverService.videoPrivaciesLoaded
+ .subscribe(
+ () => {
+ this.videoPrivacies = this.serverService.getVideoPrivacies()
+
+ this.firstStepPrivacyId = this.DEFAULT_VIDEO_PRIVACY
+ })
+ }
+
+ checkForm () {
+ this.forceCheck()
+
+ return this.form.valid
+ }
+
+ protected updateVideoAndCaptions (video: VideoEdit) {
+ this.loadingBar.start()
+
+ return this.videoService.updateVideo(video)
+ .pipe(
+ // Then update captions
+ switchMap(() => this.videoCaptionService.updateCaptions(video.id, this.videoCaptions)),
+ tap(() => this.loadingBar.complete()),
+ catchError(err => {
+ this.loadingBar.complete()
+ throw err
+ })
+ )
+ }
+}
--- /dev/null
+<div *ngIf="!isUploadingVideo" class="upload-video-container">
+ <div class="upload-video">
+ <div class="icon icon-upload"></div>
+
+ <div class="button-file">
+ <span i18n>Select the file to upload</span>
+ <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" />
+ </div>
+ <span class="button-file-extension">(.mp4, .webm, .ogv)</span>
+
+ <div class="form-group form-group-channel">
+ <label i18n for="first-step-channel">Channel</label>
+ <div class="peertube-select-container">
+ <select id="first-step-channel" [(ngModel)]="firstStepChannelId">
+ <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label i18n for="first-step-privacy">Privacy</label>
+ <div class="peertube-select-container">
+ <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId">
+ <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
+ <option [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
+ </select>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div *ngIf="isUploadingVideo" class="upload-progress-cancel">
+ <p-progressBar
+ [value]="videoUploadPercents"
+ [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
+ ></p-progressBar>
+ <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
+</div>
+
+<!-- Hidden because we want to load the component -->
+<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
+ <my-video-edit
+ [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
+ [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
+ ></my-video-edit>
+
+ <div class="submit-container">
+ <div i18n *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 || isUpdatingVideo === true || videoUploaded !== true }"
+ >
+ <span class="icon icon-validate"></span>
+ <input type="button" i18n-value value="Publish" />
+ </div>
+ </div>
+</form>
\ No newline at end of file
--- /dev/null
+@import 'variables';
+@import 'mixins';
+
+.peertube-select-container {
+ @include peertube-select-container(190px);
+}
+
+.upload-video {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .form-group-channel {
+ margin-bottom: 20px;
+ margin-top: 35px;
+ }
+
+ .icon.icon-upload {
+ @include icon(90px);
+ margin-bottom: 25px;
+ cursor: default;
+
+ background-image: url('../../../../assets/images/video/upload.svg');
+ }
+
+ .button-file {
+ @include peertube-button-file(auto);
+
+ min-width: 190px;
+ }
+
+ .button-file-extension {
+ display: block;
+ font-size: 12px;
+ margin-top: 5px;
+ }
+}
+
+.upload-progress-cancel {
+ display: flex;
+ margin-top: 25px;
+ margin-bottom: 40px;
+
+ p-progressBar {
+ flex-grow: 1;
+
+ /deep/ .ui-progressbar {
+ font-size: 15px !important;
+ color: #fff !important;
+ height: 30px !important;
+ line-height: 30px !important;
+ border-radius: 3px !important;
+ background-color: rgba(11, 204, 41, 0.16) !important;
+
+ .ui-progressbar-value {
+ background-color: #0BCC29 !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;
+ }
+ }
+ }
+ }
+
+ input {
+ @include peertube-button;
+ @include grey-button;
+
+ margin-left: 10px;
+ }
+}
\ No newline at end of file
--- /dev/null
+import { HttpEventType, HttpResponse } from '@angular/common/http'
+import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
+import { Router } from '@angular/router'
+import { LoadingBarService } from '@ngx-loading-bar/core'
+import { NotificationsService } from 'angular2-notifications'
+import { BytesPipe } from 'ngx-pipes'
+import { Subscription } from 'rxjs'
+import { VideoPrivacy } from '../../../../../../shared/models/videos'
+import { AuthService, ServerService } from '../../../core'
+import { VideoEdit } from '../../../shared/video/video-edit.model'
+import { VideoService } from '../../../shared/video/video.service'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { VideoSend } from '@app/videos/+video-edit/video-add-components/video-send'
+import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
+import { FormValidatorService, UserService } from '@app/shared'
+import { VideoCaptionService } from '@app/shared/video-caption'
+
+@Component({
+ selector: 'my-video-upload',
+ templateUrl: './video-upload.component.html',
+ styleUrls: [
+ '../shared/video-edit.component.scss',
+ './video-upload.component.scss'
+ ]
+})
+export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
+ @Output() firstStepDone = new EventEmitter<string>()
+ @ViewChild('videofileInput') videofileInput
+
+ // So that it can be accessed in the template
+ readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
+
+ userVideoQuotaUsed = 0
+
+ isUploadingVideo = false
+ isUpdatingVideo = false
+ videoUploaded = false
+ videoUploadObservable: Subscription = null
+ videoUploadPercents = 0
+ videoUploadedIds = {
+ id: 0,
+ uuid: ''
+ }
+
+ protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
+
+ constructor (
+ protected formValidatorService: FormValidatorService,
+ protected loadingBar: LoadingBarService,
+ protected notificationsService: NotificationsService,
+ protected authService: AuthService,
+ protected serverService: ServerService,
+ protected videoService: VideoService,
+ protected videoCaptionService: VideoCaptionService,
+ private userService: UserService,
+ private router: Router,
+ private i18n: I18n
+ ) {
+ super()
+ }
+
+ get videoExtensions () {
+ return this.serverService.getConfig().video.file.extensions.join(',')
+ }
+
+ ngOnInit () {
+ super.ngOnInit()
+
+ this.userService.getMyVideoQuotaUsed()
+ .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
+ }
+
+ ngOnDestroy () {
+ if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe()
+ }
+
+ canDeactivate () {
+ let text = ''
+
+ if (this.videoUploaded === true) {
+ // FIXME: cannot concatenate strings inside i18n service :/
+ text = this.i18n('Your video was uploaded to your account and is private.') +
+ this.i18n('But associated data (tags, description...) will be lost, are you sure you want to leave this page?')
+ } else {
+ text = this.i18n('Your video is not uploaded yet, are you sure you want to leave this page?')
+ }
+
+ return {
+ canDeactivate: !this.isUploadingVideo,
+ text
+ }
+ }
+
+ fileChange () {
+ this.uploadFirstStep()
+ }
+
+ cancelUpload () {
+ if (this.videoUploadObservable !== null) {
+ this.videoUploadObservable.unsubscribe()
+ this.isUploadingVideo = false
+ this.videoUploadPercents = 0
+ this.videoUploadObservable = null
+ this.notificationsService.info(this.i18n('Info'), this.i18n('Upload cancelled'))
+ }
+ }
+
+ uploadFirstStep () {
+ const videofile = this.videofileInput.nativeElement.files[0] as File
+ if (!videofile) return
+
+ // Cannot upload videos > 8GB for now
+ if (videofile.size > 8 * 1024 * 1024 * 1024) {
+ this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 8GB'))
+ return
+ }
+
+ const videoQuota = this.authService.getUser().videoQuota
+ if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
+ const bytePipes = new BytesPipe()
+
+ const msg = this.i18n(
+ 'Your video quota is exceeded with this video (video size: {{ videoSize }}, used: {{ videoQuotaUsed }}, quota: {{ videoQuota }})',
+ {
+ videoSize: bytePipes.transform(videofile.size, 0),
+ videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0),
+ videoQuota: bytePipes.transform(videoQuota, 0)
+ }
+ )
+ this.notificationsService.error(this.i18n('Error'), msg)
+ return
+ }
+
+ const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
+ let name: string
+
+ // If the name of the file is very small, keep the extension
+ if (nameWithoutExtension.length < 3) name = videofile.name
+ else name = nameWithoutExtension
+
+ const privacy = this.firstStepPrivacyId.toString()
+ const nsfw = false
+ const waitTranscoding = true
+ const commentsEnabled = true
+ const channelId = this.firstStepChannelId.toString()
+
+ const formData = new FormData()
+ formData.append('name', name)
+ // Put the video "private" -> we are waiting the user validation of the second step
+ formData.append('privacy', VideoPrivacy.PRIVATE.toString())
+ formData.append('nsfw', '' + nsfw)
+ formData.append('commentsEnabled', '' + commentsEnabled)
+ formData.append('waitTranscoding', '' + waitTranscoding)
+ formData.append('channelId', '' + channelId)
+ formData.append('videofile', videofile)
+
+ this.isUploadingVideo = true
+ this.firstStepDone.emit(name)
+
+ this.form.patchValue({
+ name,
+ privacy,
+ nsfw,
+ channelId
+ })
+
+ this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe(
+ event => {
+ if (event.type === HttpEventType.UploadProgress) {
+ this.videoUploadPercents = Math.round(100 * event.loaded / event.total)
+ } else if (event instanceof HttpResponse) {
+ this.videoUploaded = true
+
+ this.videoUploadedIds = event.body.video
+
+ this.videoUploadObservable = null
+ }
+ },
+
+ err => {
+ // Reset progress
+ this.isUploadingVideo = false
+ this.videoUploadPercents = 0
+ this.videoUploadObservable = null
+ this.notificationsService.error(this.i18n('Error'), err.message)
+ }
+ )
+ }
+
+ updateSecondStep () {
+ if (this.checkForm() === false) {
+ return
+ }
+
+ const video = new VideoEdit()
+ video.patch(this.form.value)
+ video.id = this.videoUploadedIds.id
+ video.uuid = this.videoUploadedIds.uuid
+
+ this.isUpdatingVideo = true
+
+ this.updateVideoAndCaptions(video)
+ .subscribe(
+ () => {
+ this.isUpdatingVideo = false
+ this.isUploadingVideo = false
+
+ this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.'))
+ this.router.navigate([ '/videos/watch', video.uuid ])
+ },
+
+ err => {
+ this.isUpdatingVideo = false
+ this.notificationsService.error(this.i18n('Error'), err.message)
+ console.error(err)
+ }
+ )
+ }
+}
import { Component, ViewChild } from '@angular/core'
import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
-import { VideoImportUrlComponent } from '@app/videos/+video-edit/video-import-url.component'
-import { VideoUploadComponent } from '@app/videos/+video-edit/video-upload.component'
+import { VideoImportUrlComponent } from '@app/videos/+video-edit/video-add-components/video-import-url.component'
+import { VideoUploadComponent } from '@app/videos/+video-edit/video-add-components/video-upload.component'
import { ServerService } from '@app/core'
@Component({
import { VideoAddRoutingModule } from './video-add-routing.module'
import { VideoAddComponent } from './video-add.component'
import { CanDeactivateGuard } from '../../shared/guards/can-deactivate-guard.service'
-import { VideoUploadComponent } from '@app/videos/+video-edit/video-upload.component'
-import { VideoImportUrlComponent } from '@app/videos/+video-edit/video-import-url.component'
+import { VideoUploadComponent } from '@app/videos/+video-edit/video-add-components/video-upload.component'
+import { VideoImportUrlComponent } from '@app/videos/+video-edit/video-add-components/video-import-url.component'
@NgModule({
imports: [
+++ /dev/null
-<div *ngIf="!hasImportedVideo" class="upload-video-container">
- <div class="import-video">
- <div class="icon icon-upload"></div>
-
- <div class="form-group">
- <label i18n for="targetUrl">URL</label>
- <my-help
- helpType="custom" i18n-customHtml
- customHtml="You can import any URL <a href='https://rg3.github.io/youtube-dl/supportedsites.html' target='_blank' rel='noopener noreferrer'>supported by youtube-dl</a> or URL that points to a raw MP4 file. You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance."
- ></my-help>
-
- <input type="text" id="targetUrl" [(ngModel)]="targetUrl" />
- </div>
-
- <div class="form-group">
- <label i18n for="first-step-channel">Channel</label>
- <div class="peertube-select-container">
- <select id="first-step-channel" [(ngModel)]="firstStepChannelId">
- <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
- </select>
- </div>
- </div>
-
- <div class="form-group">
- <label i18n for="first-step-privacy">Privacy</label>
- <div class="peertube-select-container">
- <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId">
- <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
- </select>
- </div>
- </div>
-
- <input
- type="button" i18n-value value="Import"
- [disabled]="!isTargetUrlValid() || isImportingVideo" (click)="importVideo()"
- />
- </div>
-</div>
-
-<div *ngIf="hasImportedVideo" class="alert alert-info" i18n>
- Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
-</div>
-
-<!-- Hidden because we want to load the component -->
-<form [hidden]="!hasImportedVideo" novalidate [formGroup]="form">
- <my-video-edit
- [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
- [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
- ></my-video-edit>
-
- <div class="submit-container">
- <div class="submit-button"
- (click)="updateSecondStep()"
- [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"
- >
- <span class="icon icon-validate"></span>
- <input type="button" i18n-value value="Update" />
- </div>
- </div>
-</form>
+++ /dev/null
-@import '_variables';
-@import '_mixins';
-
-$width-size: 190px;
-
-.peertube-select-container {
- @include peertube-select-container($width-size);
-}
-
-.import-video {
- display: flex;
- flex-direction: column;
- align-items: center;
-
- .icon.icon-upload {
- @include icon(90px);
- margin-bottom: 25px;
- cursor: default;
-
- background-image: url('../../../assets/images/video/upload.svg');
- }
-
- input[type=text] {
- @include peertube-input-text($width-size);
- display: block;
- }
-
- input[type=button] {
- @include peertube-button;
- @include orange-button;
-
- width: $width-size;
- margin-top: 30px;
- }
-}
-
-
+++ /dev/null
-import { Component, EventEmitter, OnInit, Output } from '@angular/core'
-import { Router } from '@angular/router'
-import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
-import { NotificationsService } from 'angular2-notifications'
-import { VideoPrivacy, VideoUpdate } from '../../../../../shared/models/videos'
-import { AuthService, ServerService } from '../../core'
-import { VideoService } from '../../shared/video/video.service'
-import { I18n } from '@ngx-translate/i18n-polyfill'
-import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
-import { VideoImportService } from '@app/shared/video-import'
-import { VideoEdit } from '@app/shared/video/video-edit.model'
-import { LoadingBarService } from '@ngx-loading-bar/core'
-import { VideoCaptionService } from '@app/shared/video-caption'
-import { VideoSend } from '@app/videos/+video-edit/shared/video-send'
-
-@Component({
- selector: 'my-video-import-url',
- templateUrl: './video-import-url.component.html',
- styleUrls: [
- './shared/video-edit.component.scss',
- './video-import-url.component.scss'
- ]
-})
-export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
- @Output() firstStepDone = new EventEmitter<string>()
-
- targetUrl = ''
- videoFileName: string
-
- isImportingVideo = false
- hasImportedVideo = false
- isUpdatingVideo = false
-
- video: VideoEdit
-
- protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PRIVATE
-
- constructor (
- protected formValidatorService: FormValidatorService,
- protected loadingBar: LoadingBarService,
- protected notificationsService: NotificationsService,
- protected authService: AuthService,
- protected serverService: ServerService,
- protected videoService: VideoService,
- protected videoCaptionService: VideoCaptionService,
- private router: Router,
- private videoImportService: VideoImportService,
- private i18n: I18n
- ) {
- super()
- }
-
- ngOnInit () {
- super.ngOnInit()
- }
-
- canDeactivate () {
- return { canDeactivate: true }
- }
-
- isTargetUrlValid () {
- return this.targetUrl && this.targetUrl.match(/https?:\/\//)
- }
-
- importVideo () {
- this.isImportingVideo = true
-
- const videoUpdate: VideoUpdate = {
- privacy: this.firstStepPrivacyId,
- waitTranscoding: false,
- commentsEnabled: true,
- channelId: this.firstStepChannelId
- }
-
- this.loadingBar.start()
-
- this.videoImportService.importVideo(this.targetUrl, videoUpdate).subscribe(
- res => {
- this.loadingBar.complete()
- this.firstStepDone.emit(res.video.name)
- this.isImportingVideo = false
- this.hasImportedVideo = true
-
- this.video = new VideoEdit(Object.assign(res.video, {
- commentsEnabled: videoUpdate.commentsEnabled,
- support: null,
- thumbnailUrl: null,
- previewUrl: null
- }))
- this.hydrateFormFromVideo()
- },
-
- err => {
- this.loadingBar.complete()
- this.isImportingVideo = false
- this.notificationsService.error(this.i18n('Error'), err.message)
- }
- )
- }
-
- updateSecondStep () {
- if (this.checkForm() === false) {
- return
- }
-
- this.video.patch(this.form.value)
-
- this.isUpdatingVideo = true
-
- // Update the video
- this.updateVideoAndCaptions(this.video)
- .subscribe(
- () => {
- this.isUpdatingVideo = false
- this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.'))
-
- this.router.navigate([ '/my-account', 'video-imports' ])
- },
-
- err => {
- this.isUpdatingVideo = false
- this.notificationsService.error(this.i18n('Error'), err.message)
- console.error(err)
- }
- )
-
- }
-
- private hydrateFormFromVideo () {
- this.form.patchValue(this.video.toFormPatch())
- }
-}
+++ /dev/null
-<div *ngIf="!isUploadingVideo" class="upload-video-container">
- <div class="upload-video">
- <div class="icon icon-upload"></div>
-
- <div class="button-file">
- <span i18n>Select the file to upload</span>
- <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" />
- </div>
- <span class="button-file-extension">(.mp4, .webm, .ogv)</span>
-
- <div class="form-group form-group-channel">
- <label i18n for="first-step-channel">Channel</label>
- <div class="peertube-select-container">
- <select id="first-step-channel" [(ngModel)]="firstStepChannelId">
- <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
- </select>
- </div>
- </div>
-
- <div class="form-group">
- <label i18n for="first-step-privacy">Privacy</label>
- <div class="peertube-select-container">
- <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId">
- <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
- <option [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
- </select>
- </div>
- </div>
- </div>
-</div>
-
-<div *ngIf="isUploadingVideo" class="upload-progress-cancel">
- <p-progressBar
- [value]="videoUploadPercents"
- [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
- ></p-progressBar>
- <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
-</div>
-
-<!-- Hidden because we want to load the component -->
-<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
- <my-video-edit
- [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
- [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
- ></my-video-edit>
-
- <div class="submit-container">
- <div i18n *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 || isUpdatingVideo === true || videoUploaded !== true }"
- >
- <span class="icon icon-validate"></span>
- <input type="button" i18n-value value="Publish" />
- </div>
- </div>
-</form>
\ No newline at end of file
+++ /dev/null
-@import '_variables';
-@import '_mixins';
-
-.peertube-select-container {
- @include peertube-select-container(190px);
-}
-
-.upload-video {
- display: flex;
- flex-direction: column;
- align-items: center;
-
- .form-group-channel {
- margin-bottom: 20px;
- margin-top: 35px;
- }
-
- .icon.icon-upload {
- @include icon(90px);
- margin-bottom: 25px;
- cursor: default;
-
- background-image: url('../../../assets/images/video/upload.svg');
- }
-
- .button-file {
- @include peertube-button-file(auto);
-
- min-width: 190px;
- }
-
- .button-file-extension {
- display: block;
- font-size: 12px;
- margin-top: 5px;
- }
-}
-
-.upload-progress-cancel {
- display: flex;
- margin-top: 25px;
- margin-bottom: 40px;
-
- p-progressBar {
- flex-grow: 1;
-
- /deep/ .ui-progressbar {
- font-size: 15px !important;
- color: #fff !important;
- height: 30px !important;
- line-height: 30px !important;
- border-radius: 3px !important;
- background-color: rgba(11, 204, 41, 0.16) !important;
-
- .ui-progressbar-value {
- background-color: #0BCC29 !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;
- }
- }
- }
- }
-
- input {
- @include peertube-button;
- @include grey-button;
-
- margin-left: 10px;
- }
-}
\ No newline at end of file
+++ /dev/null
-import { HttpEventType, HttpResponse } from '@angular/common/http'
-import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
-import { Router } from '@angular/router'
-import { UserService } from '@app/shared'
-import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
-import { LoadingBarService } from '@ngx-loading-bar/core'
-import { NotificationsService } from 'angular2-notifications'
-import { BytesPipe } from 'ngx-pipes'
-import { Subscription } from 'rxjs'
-import { VideoPrivacy } from '../../../../../shared/models/videos'
-import { AuthService, ServerService } from '../../core'
-import { VideoEdit } from '../../shared/video/video-edit.model'
-import { VideoService } from '../../shared/video/video.service'
-import { I18n } from '@ngx-translate/i18n-polyfill'
-import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
-import { VideoCaptionService } from '@app/shared/video-caption'
-import { VideoSend } from '@app/videos/+video-edit/shared/video-send'
-
-@Component({
- selector: 'my-video-upload',
- templateUrl: './video-upload.component.html',
- styleUrls: [
- './shared/video-edit.component.scss',
- './video-upload.component.scss'
- ]
-})
-export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
- @Output() firstStepDone = new EventEmitter<string>()
- @ViewChild('videofileInput') videofileInput
-
- // So that it can be accessed in the template
- readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
-
- userVideoQuotaUsed = 0
-
- isUploadingVideo = false
- isUpdatingVideo = false
- videoUploaded = false
- videoUploadObservable: Subscription = null
- videoUploadPercents = 0
- videoUploadedIds = {
- id: 0,
- uuid: ''
- }
-
- protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
-
- constructor (
- protected formValidatorService: FormValidatorService,
- protected loadingBar: LoadingBarService,
- protected notificationsService: NotificationsService,
- protected authService: AuthService,
- protected serverService: ServerService,
- protected videoService: VideoService,
- protected videoCaptionService: VideoCaptionService,
- private userService: UserService,
- private router: Router,
- private i18n: I18n
- ) {
- super()
- }
-
- get videoExtensions () {
- return this.serverService.getConfig().video.file.extensions.join(',')
- }
-
- ngOnInit () {
- super.ngOnInit()
-
- this.userService.getMyVideoQuotaUsed()
- .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
- }
-
- ngOnDestroy () {
- if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe()
- }
-
- canDeactivate () {
- let text = ''
-
- if (this.videoUploaded === true) {
- // FIXME: cannot concatenate strings inside i18n service :/
- text = this.i18n('Your video was uploaded to your account and is private.') +
- this.i18n('But associated data (tags, description...) will be lost, are you sure you want to leave this page?')
- } else {
- text = this.i18n('Your video is not uploaded yet, are you sure you want to leave this page?')
- }
-
- return {
- canDeactivate: !this.isUploadingVideo,
- text
- }
- }
-
- fileChange () {
- this.uploadFirstStep()
- }
-
- cancelUpload () {
- if (this.videoUploadObservable !== null) {
- this.videoUploadObservable.unsubscribe()
- this.isUploadingVideo = false
- this.videoUploadPercents = 0
- this.videoUploadObservable = null
- this.notificationsService.info(this.i18n('Info'), this.i18n('Upload cancelled'))
- }
- }
-
- uploadFirstStep () {
- const videofile = this.videofileInput.nativeElement.files[0] as File
- if (!videofile) return
-
- // Cannot upload videos > 8GB for now
- if (videofile.size > 8 * 1024 * 1024 * 1024) {
- this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 8GB'))
- return
- }
-
- const videoQuota = this.authService.getUser().videoQuota
- if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
- const bytePipes = new BytesPipe()
-
- const msg = this.i18n(
- 'Your video quota is exceeded with this video (video size: {{ videoSize }}, used: {{ videoQuotaUsed }}, quota: {{ videoQuota }})',
- {
- videoSize: bytePipes.transform(videofile.size, 0),
- videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0),
- videoQuota: bytePipes.transform(videoQuota, 0)
- }
- )
- this.notificationsService.error(this.i18n('Error'), msg)
- return
- }
-
- const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
- let name: string
-
- // If the name of the file is very small, keep the extension
- if (nameWithoutExtension.length < 3) name = videofile.name
- else name = nameWithoutExtension
-
- const privacy = this.firstStepPrivacyId.toString()
- const nsfw = false
- const waitTranscoding = true
- const commentsEnabled = true
- const channelId = this.firstStepChannelId.toString()
-
- const formData = new FormData()
- formData.append('name', name)
- // Put the video "private" -> we are waiting the user validation of the second step
- formData.append('privacy', VideoPrivacy.PRIVATE.toString())
- formData.append('nsfw', '' + nsfw)
- formData.append('commentsEnabled', '' + commentsEnabled)
- formData.append('waitTranscoding', '' + waitTranscoding)
- formData.append('channelId', '' + channelId)
- formData.append('videofile', videofile)
-
- this.isUploadingVideo = true
- this.firstStepDone.emit(name)
-
- this.form.patchValue({
- name,
- privacy,
- nsfw,
- channelId
- })
-
- this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe(
- event => {
- if (event.type === HttpEventType.UploadProgress) {
- this.videoUploadPercents = Math.round(100 * event.loaded / event.total)
- } else if (event instanceof HttpResponse) {
- this.videoUploaded = true
-
- this.videoUploadedIds = event.body.video
-
- this.videoUploadObservable = null
- }
- },
-
- err => {
- // Reset progress
- this.isUploadingVideo = false
- this.videoUploadPercents = 0
- this.videoUploadObservable = null
- this.notificationsService.error(this.i18n('Error'), err.message)
- }
- )
- }
-
- updateSecondStep () {
- if (this.checkForm() === false) {
- return
- }
-
- const video = new VideoEdit()
- video.patch(this.form.value)
- video.id = this.videoUploadedIds.id
- video.uuid = this.videoUploadedIds.uuid
-
- this.isUpdatingVideo = true
-
- this.updateVideoAndCaptions(video)
- .subscribe(
- () => {
- this.isUpdatingVideo = false
- this.isUploadingVideo = false
-
- this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.'))
- this.router.navigate([ '/videos/watch', video.uuid ])
- },
-
- err => {
- this.isUpdatingVideo = false
- this.notificationsService.error(this.i18n('Error'), err.message)
- console.error(err)
- }
- )
- }
-}