From 1942f11d5ee6926ad93dc1b79fae18325ba5de18 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 23 Jun 2020 14:49:20 +0200 Subject: Lazy load all routes --- .../shared/i18n-primeng-calendar.service.ts | 94 +++++++ .../shared/video-caption-add-modal.component.html | 47 ++++ .../shared/video-caption-add-modal.component.scss | 20 ++ .../shared/video-caption-add-modal.component.ts | 85 +++++++ .../+video-edit/shared/video-edit.component.html | 280 +++++++++++++++++++++ .../+video-edit/shared/video-edit.component.scss | 197 +++++++++++++++ .../+video-edit/shared/video-edit.component.ts | 274 ++++++++++++++++++++ .../+video-edit/shared/video-edit.module.ts | 38 +++ 8 files changed, 1035 insertions(+) create mode 100644 client/src/app/+videos/+video-edit/shared/i18n-primeng-calendar.service.ts create mode 100644 client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html create mode 100644 client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss create mode 100644 client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts create mode 100644 client/src/app/+videos/+video-edit/shared/video-edit.component.html create mode 100644 client/src/app/+videos/+video-edit/shared/video-edit.component.scss create mode 100644 client/src/app/+videos/+video-edit/shared/video-edit.component.ts create mode 100644 client/src/app/+videos/+video-edit/shared/video-edit.module.ts (limited to 'client/src/app/+videos/+video-edit/shared') diff --git a/client/src/app/+videos/+video-edit/shared/i18n-primeng-calendar.service.ts b/client/src/app/+videos/+video-edit/shared/i18n-primeng-calendar.service.ts new file mode 100644 index 000000000..b05852ff8 --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/i18n-primeng-calendar.service.ts @@ -0,0 +1,94 @@ +import { I18n } from '@ngx-translate/i18n-polyfill' +import { Injectable } from '@angular/core' + +@Injectable() +export class I18nPrimengCalendarService { + private readonly calendarLocale: any = {} + + constructor (private i18n: I18n) { + this.calendarLocale = { + firstDayOfWeek: 0, + dayNames: [ + this.i18n('Sunday'), + this.i18n('Monday'), + this.i18n('Tuesday'), + this.i18n('Wednesday'), + this.i18n('Thursday'), + this.i18n('Friday'), + this.i18n('Saturday') + ], + + dayNamesShort: [ + this.i18n({ value: 'Sun', description: 'Day name short' }), + this.i18n({ value: 'Mon', description: 'Day name short' }), + this.i18n({ value: 'Tue', description: 'Day name short' }), + this.i18n({ value: 'Wed', description: 'Day name short' }), + this.i18n({ value: 'Thu', description: 'Day name short' }), + this.i18n({ value: 'Fri', description: 'Day name short' }), + this.i18n({ value: 'Sat', description: 'Day name short' }) + ], + + dayNamesMin: [ + this.i18n({ value: 'Su', description: 'Day name min' }), + this.i18n({ value: 'Mo', description: 'Day name min' }), + this.i18n({ value: 'Tu', description: 'Day name min' }), + this.i18n({ value: 'We', description: 'Day name min' }), + this.i18n({ value: 'Th', description: 'Day name min' }), + this.i18n({ value: 'Fr', description: 'Day name min' }), + this.i18n({ value: 'Sa', description: 'Day name min' }) + ], + + monthNames: [ + this.i18n('January'), + this.i18n('February'), + this.i18n('March'), + this.i18n('April'), + this.i18n('May'), + this.i18n('June'), + this.i18n('July'), + this.i18n('August'), + this.i18n('September'), + this.i18n('October'), + this.i18n('November'), + this.i18n('December') + ], + + monthNamesShort: [ + this.i18n({ value: 'Jan', description: 'Month name short' }), + this.i18n({ value: 'Feb', description: 'Month name short' }), + this.i18n({ value: 'Mar', description: 'Month name short' }), + this.i18n({ value: 'Apr', description: 'Month name short' }), + this.i18n({ value: 'May', description: 'Month name short' }), + this.i18n({ value: 'Jun', description: 'Month name short' }), + this.i18n({ value: 'Jul', description: 'Month name short' }), + this.i18n({ value: 'Aug', description: 'Month name short' }), + this.i18n({ value: 'Sep', description: 'Month name short' }), + this.i18n({ value: 'Oct', description: 'Month name short' }), + this.i18n({ value: 'Nov', description: 'Month name short' }), + this.i18n({ value: 'Dec', description: 'Month name short' }) + ], + + today: this.i18n('Today'), + + clear: this.i18n('Clear') + } + } + + getCalendarLocale () { + return this.calendarLocale + } + + getTimezone () { + const gmt = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)[1] + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone + + return `${timezone} - ${gmt}` + } + + getDateFormat () { + return this.i18n({ + value: 'yy-mm-dd ', + description: 'Date format in this locale.' + }) + } +} diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html new file mode 100644 index 000000000..6a9e31b5a --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss new file mode 100644 index 000000000..b257a16a9 --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss @@ -0,0 +1,20 @@ +@import '_variables'; +@import '_mixins'; + +.peertube-select-container { + @include peertube-select-container(auto); +} + +.caption-file { + margin-top: 20px; + width: max-content; + + ::ng-deep .root { + width: max-content; + } +} + +.warning-replace-caption { + color: red; + margin-top: 10px; +} \ No newline at end of file diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts new file mode 100644 index 000000000..a90d04ce8 --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts @@ -0,0 +1,85 @@ +import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' +import { ServerService } from '@app/core' +import { FormReactive, FormValidatorService, VideoCaptionsValidatorsService } from '@app/shared/shared-forms' +import { VideoCaptionEdit } from '@app/shared/shared-main' +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' +import { ServerConfig, VideoConstant } from '@shared/models' + +@Component({ + selector: 'my-video-caption-add-modal', + styleUrls: [ './video-caption-add-modal.component.scss' ], + templateUrl: './video-caption-add-modal.component.html' +}) + +export class VideoCaptionAddModalComponent extends FormReactive implements OnInit { + @Input() existingCaptions: string[] + @Input() serverConfig: ServerConfig + + @Output() captionAdded = new EventEmitter() + + @ViewChild('modal', { static: true }) modal: ElementRef + + videoCaptionLanguages: VideoConstant[] = [] + + private openedModal: NgbModalRef + private closingModal = false + + constructor ( + protected formValidatorService: FormValidatorService, + private modalService: NgbModal, + private serverService: ServerService, + private videoCaptionsValidatorsService: VideoCaptionsValidatorsService + ) { + super() + } + + get videoCaptionExtensions () { + return this.serverConfig.videoCaption.file.extensions + } + + get videoCaptionMaxSize () { + return this.serverConfig.videoCaption.file.size.max + } + + ngOnInit () { + this.serverService.getVideoLanguages() + .subscribe(languages => this.videoCaptionLanguages = languages) + + this.buildForm({ + language: this.videoCaptionsValidatorsService.VIDEO_CAPTION_LANGUAGE, + captionfile: this.videoCaptionsValidatorsService.VIDEO_CAPTION_FILE + }) + } + + show () { + this.closingModal = false + + this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false }) + } + + hide () { + this.closingModal = true + this.openedModal.close() + this.form.reset() + } + + isReplacingExistingCaption () { + if (this.closingModal === true) return false + + const languageId = this.form.value[ 'language' ] + + return languageId && this.existingCaptions.indexOf(languageId) !== -1 + } + + async addCaption () { + const languageId = this.form.value[ 'language' ] + const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId) + + this.captionAdded.emit({ + language: languageObject, + captionfile: this.form.value[ 'captionfile' ] + }) + + this.hide() + } +} diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html new file mode 100644 index 000000000..c11a60dce --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html @@ -0,0 +1,280 @@ +
+ + +
+
+ + diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss new file mode 100644 index 000000000..69b907288 --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss @@ -0,0 +1,197 @@ +// Bootstrap grid utilities require functions, variables and mixins +@import 'node_modules/bootstrap/scss/functions'; +@import 'node_modules/bootstrap/scss/variables'; +@import 'node_modules/bootstrap/scss/mixins'; +@import 'node_modules/bootstrap/scss/grid'; + +@import 'variables'; +@import 'mixins'; + +label { + font-weight: $font-regular; + font-size: 100%; +} + +.peertube-select-container { + @include peertube-select-container(auto); +} + +.title-page a { + color: pvar(--mainForegroundColor); + + &:hover { + text-decoration: none; + opacity: .8; + } +} + +my-peertube-checkbox { + display: block; + margin-bottom: 1rem; +} + +.nav-tabs { + margin-bottom: 15px; +} + +.video-edit { + height: 100%; + min-height: 300px; + + .form-group { + margin-bottom: 25px; + } + + input { + @include peertube-input-text(100%); + display: block; + } + + .label-tags + span { + font-size: 15px; + } + + .advanced-settings .form-group { + margin-bottom: 20px; + } +} + +.captions { + + .captions-header { + text-align: right; + margin-bottom: 1rem; + + .create-caption { + @include create-button; + } + } + + .caption-entry { + display: flex; + height: 40px; + align-items: center; + + a.caption-entry-label { + @include disable-default-a-behaviour; + + flex-grow: 1; + color: #000; + + &:hover { + opacity: 0.8; + } + } + + .caption-entry-label { + font-size: 15px; + font-weight: bold; + + margin-right: 20px; + width: 150px; + } + + .caption-entry-state { + width: 200px; + + &.caption-entry-state-create { + color: #39CC0B; + } + + &.caption-entry-state-delete { + color: #FF0000; + } + } + + .caption-entry-delete { + @include peertube-button; + @include grey-button; + } + } + + .no-caption { + text-align: center; + font-size: 15px; + } +} + +.submit-container { + text-align: right; + + .message-submit { + display: inline-block; + margin-right: 25px; + + color: pvar(--greyForegroundColor); + font-size: 15px; + } + + .submit-button { + @include peertube-button; + @include orange-button; + @include button-with-icon(20px, 1px); + + display: inline-block; + + input { + cursor: inherit; + background-color: inherit; + border: none; + padding: 0; + outline: 0; + color: inherit; + font-weight: $font-semibold; + } + } +} + +p-calendar { + display: block; + + ::ng-deep { + input, + .ui-calendar { + width: 100%; + } + + input { + @include peertube-input-text(100%); + color: #000; + } + } +} + +@include ng2-tags; + +// columns for the video +.col-video-edit { + @include make-col-ready(); + + @include media-breakpoint-up(md) { + @include make-col(7); + + & + .col-video-edit { + @include make-col(5); + } + } + + @include media-breakpoint-up(xl) { + @include make-col(8); + + & + .col-video-edit { + @include make-col(4); + } + } +} + +:host-context(.expanded) { + .col-video-edit { + @include media-breakpoint-up(md) { + @include make-col(8); + + & + .col-video-edit { + @include make-col(4); + } + } + } +} diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts new file mode 100644 index 000000000..239e453ad --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts @@ -0,0 +1,274 @@ +import { map } from 'rxjs/operators' +import { Component, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' +import { ServerService } from '@app/core' +import { removeElementFromArray } from '@app/helpers' +import { FormReactiveValidationMessages, FormValidatorService, VideoValidatorsService } from '@app/shared/shared-forms' +import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' +import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models' +import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' +import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' + +@Component({ + selector: 'my-video-edit', + styleUrls: [ './video-edit.component.scss' ], + templateUrl: './video-edit.component.html' +}) +export class VideoEditComponent implements OnInit, OnDestroy { + @Input() form: FormGroup + @Input() formErrors: { [ id: string ]: string } = {} + @Input() validationMessages: FormReactiveValidationMessages = {} + @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] + @Input() schedulePublicationPossible = true + @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] + @Input() waitTranscodingEnabled = true + + @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent + + // So that it can be accessed in the template + readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY + + videoPrivacies: VideoConstant[] = [] + videoCategories: VideoConstant[] = [] + videoLicences: VideoConstant[] = [] + videoLanguages: VideoConstant[] = [] + + tagValidators: ValidatorFn[] + tagValidatorsMessages: { [ name: string ]: string } + + schedulePublicationEnabled = false + + calendarLocale: any = {} + minScheduledDate = new Date() + myYearRange = '1880:' + (new Date()).getFullYear() + + calendarTimezone: string + calendarDateFormat: string + + serverConfig: ServerConfig + + private schedulerInterval: any + private firstPatchDone = false + private initialVideoCaptions: string[] = [] + + constructor ( + private formValidatorService: FormValidatorService, + private videoValidatorsService: VideoValidatorsService, + private videoService: VideoService, + private serverService: ServerService, + private i18nPrimengCalendarService: I18nPrimengCalendarService, + private ngZone: NgZone + ) { + this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS + this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES + + this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale() + this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone() + this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat() + } + + get existingCaptions () { + return this.videoCaptions + .filter(c => c.action !== 'REMOVE') + .map(c => c.language.id) + } + + updateForm () { + const defaultValues: any = { + nsfw: 'false', + commentsEnabled: 'true', + downloadEnabled: 'true', + waitTranscoding: 'true', + tags: [] + } + const obj: any = { + name: this.videoValidatorsService.VIDEO_NAME, + privacy: this.videoValidatorsService.VIDEO_PRIVACY, + channelId: this.videoValidatorsService.VIDEO_CHANNEL, + nsfw: null, + commentsEnabled: null, + downloadEnabled: null, + waitTranscoding: null, + category: this.videoValidatorsService.VIDEO_CATEGORY, + licence: this.videoValidatorsService.VIDEO_LICENCE, + language: this.videoValidatorsService.VIDEO_LANGUAGE, + description: this.videoValidatorsService.VIDEO_DESCRIPTION, + tags: null, + previewfile: null, + support: this.videoValidatorsService.VIDEO_SUPPORT, + schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT, + originallyPublishedAt: this.videoValidatorsService.VIDEO_ORIGINALLY_PUBLISHED_AT + } + + this.formValidatorService.updateForm( + this.form, + this.formErrors, + this.validationMessages, + obj, + defaultValues + ) + + this.form.addControl('captions', new FormArray([ + new FormGroup({ + language: new FormControl(), + captionfile: new FormControl() + }) + ])) + + this.trackChannelChange() + this.trackPrivacyChange() + } + + ngOnInit () { + this.updateForm() + + this.serverService.getVideoCategories() + .subscribe(res => this.videoCategories = res) + this.serverService.getVideoLicences() + .subscribe(res => this.videoLicences = res) + this.serverService.getVideoLanguages() + .subscribe(res => this.videoLanguages = res) + + this.serverService.getVideoPrivacies() + .subscribe(privacies => this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies)) + + this.serverConfig = this.serverService.getTmpConfig() + this.serverService.getConfig() + .subscribe(config => this.serverConfig = config) + + this.initialVideoCaptions = this.videoCaptions.map(c => c.language.id) + + this.ngZone.runOutsideAngular(() => { + this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute + }) + } + + ngOnDestroy () { + if (this.schedulerInterval) clearInterval(this.schedulerInterval) + } + + onCaptionAdded (caption: VideoCaptionEdit) { + const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id) + + // Replace existing caption? + if (existingCaption) { + Object.assign(existingCaption, caption, { action: 'CREATE' as 'CREATE' }) + } else { + this.videoCaptions.push( + Object.assign(caption, { action: 'CREATE' as 'CREATE' }) + ) + } + + this.sortVideoCaptions() + } + + async deleteCaption (caption: VideoCaptionEdit) { + // Caption recovers his former state + if (caption.action && this.initialVideoCaptions.indexOf(caption.language.id) !== -1) { + caption.action = undefined + return + } + + // This caption is not on the server, just remove it from our array + if (caption.action === 'CREATE') { + removeElementFromArray(this.videoCaptions, caption) + return + } + + caption.action = 'REMOVE' as 'REMOVE' + } + + openAddCaptionModal () { + this.videoCaptionAddModal.show() + } + + private sortVideoCaptions () { + this.videoCaptions.sort((v1, v2) => { + if (v1.language.label < v2.language.label) return -1 + if (v1.language.label === v2.language.label) return 0 + + return 1 + }) + } + + private trackPrivacyChange () { + // We will update the schedule input and the wait transcoding checkbox validators + this.form.controls[ 'privacy' ] + .valueChanges + .pipe(map(res => parseInt(res.toString(), 10))) + .subscribe( + newPrivacyId => { + + this.schedulePublicationEnabled = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY + + // Value changed + const scheduleControl = this.form.get('schedulePublicationAt') + const waitTranscodingControl = this.form.get('waitTranscoding') + + if (this.schedulePublicationEnabled) { + scheduleControl.setValidators([ Validators.required ]) + + waitTranscodingControl.disable() + waitTranscodingControl.setValue(false) + } else { + scheduleControl.clearValidators() + + waitTranscodingControl.enable() + + // Do not update the control value on first patch (values come from the server) + if (this.firstPatchDone === true) { + waitTranscodingControl.setValue(true) + } + } + + scheduleControl.updateValueAndValidity() + waitTranscodingControl.updateValueAndValidity() + + this.firstPatchDone = true + + } + ) + } + + private trackChannelChange () { + // We will update the "support" field depending on the channel + this.form.controls[ 'channelId' ] + .valueChanges + .pipe(map(res => parseInt(res.toString(), 10))) + .subscribe( + newChannelId => { + const oldChannelId = parseInt(this.form.value[ 'channelId' ], 10) + + // Not initialized yet + if (isNaN(newChannelId)) return + const newChannel = this.userVideoChannels.find(c => c.id === newChannelId) + if (!newChannel) return + + // Wait support field update + setTimeout(() => { + const currentSupport = this.form.value[ 'support' ] + + // First time we set the channel? + if (isNaN(oldChannelId) && !currentSupport) return this.updateSupportField(newChannel.support) + + const oldChannel = this.userVideoChannels.find(c => c.id === oldChannelId) + if (!newChannel || !oldChannel) { + console.error('Cannot find new or old channel.') + return + } + + // If the current support text is not the same than the old channel, the user updated it. + // We don't want the user to lose his text, so stop here + if (currentSupport && currentSupport !== oldChannel.support) return + + // Update the support text with our new channel + this.updateSupportField(newChannel.support) + }) + } + ) + } + + private updateSupportField (support: string) { + return this.form.patchValue({ support: support || '' }) + } +} diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.module.ts b/client/src/app/+videos/+video-edit/shared/video-edit.module.ts new file mode 100644 index 000000000..96061a300 --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/video-edit.module.ts @@ -0,0 +1,38 @@ +import { TagInputModule } from 'ngx-chips' +import { CalendarModule } from 'primeng/calendar' +import { NgModule } from '@angular/core' +import { SharedFormModule } from '@app/shared/shared-forms' +import { SharedGlobalIconModule } from '@app/shared/shared-icons' +import { SharedMainModule } from '@app/shared/shared-main' +import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' +import { VideoEditComponent } from './video-edit.component' + +@NgModule({ + imports: [ + TagInputModule, + CalendarModule, + + SharedMainModule, + SharedFormModule, + SharedGlobalIconModule + ], + + declarations: [ + VideoEditComponent, + VideoCaptionAddModalComponent + ], + + exports: [ + TagInputModule, + CalendarModule, + + SharedMainModule, + SharedFormModule, + SharedGlobalIconModule, + + VideoEditComponent + ], + + providers: [] +}) +export class VideoEditModule { } -- cgit v1.2.3