aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared/video/modals
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared/video/modals')
-rw-r--r--client/src/app/shared/video/modals/video-block.component.html45
-rw-r--r--client/src/app/shared/video/modals/video-block.component.scss6
-rw-r--r--client/src/app/shared/video/modals/video-block.component.ts75
-rw-r--r--client/src/app/shared/video/modals/video-download.component.html108
-rw-r--r--client/src/app/shared/video/modals/video-download.component.scss64
-rw-r--r--client/src/app/shared/video/modals/video-download.component.ts208
-rw-r--r--client/src/app/shared/video/modals/video-report.component.html97
-rw-r--r--client/src/app/shared/video/modals/video-report.component.scss27
-rw-r--r--client/src/app/shared/video/modals/video-report.component.ts163
9 files changed, 0 insertions, 793 deletions
diff --git a/client/src/app/shared/video/modals/video-block.component.html b/client/src/app/shared/video/modals/video-block.component.html
deleted file mode 100644
index 5e73d66c5..000000000
--- a/client/src/app/shared/video/modals/video-block.component.html
+++ /dev/null
@@ -1,45 +0,0 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Block video "{{ video.name }}"</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div>
6
7 <div class="modal-body">
8
9 <form novalidate [formGroup]="form" (ngSubmit)="block()">
10 <div class="form-group">
11 <textarea
12 i18n-placeholder placeholder="Please describe the reason..." formControlName="reason"
13 [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control"
14 ></textarea>
15 <div *ngIf="formErrors.reason" class="form-error">
16 {{ formErrors.reason }}
17 </div>
18 </div>
19
20 <div class="form-group" *ngIf="video.isLocal">
21 <my-peertube-checkbox
22 inputName="unfederate" formControlName="unfederate"
23 i18n-labelText labelText="Unfederate the video"
24 >
25 <ng-container ngProjectAs="description">
26 <span i18n>This will ask remote instances to delete it</span>
27 </ng-container>
28 </my-peertube-checkbox>
29 </div>
30
31 <div class="form-group inputs">
32 <input
33 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
34 (click)="hide()" (key.enter)="hide()"
35 >
36
37 <input
38 type="submit" i18n-value value="Submit" class="action-button-submit"
39 [disabled]="!form.valid"
40 >
41 </div>
42 </form>
43
44 </div>
45</ng-template>
diff --git a/client/src/app/shared/video/modals/video-block.component.scss b/client/src/app/shared/video/modals/video-block.component.scss
deleted file mode 100644
index afcdb9a16..000000000
--- a/client/src/app/shared/video/modals/video-block.component.scss
+++ /dev/null
@@ -1,6 +0,0 @@
1@import 'variables';
2@import 'mixins';
3
4textarea {
5 @include peertube-textarea(100%, 100px);
6}
diff --git a/client/src/app/shared/video/modals/video-block.component.ts b/client/src/app/shared/video/modals/video-block.component.ts
deleted file mode 100644
index 1a25e0578..000000000
--- a/client/src/app/shared/video/modals/video-block.component.ts
+++ /dev/null
@@ -1,75 +0,0 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier, RedirectService } from '@app/core'
3import { VideoBlockService } from '../../video-block'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
8import { FormReactive, VideoBlockValidatorsService } from '@app/shared/forms'
9import { Video } from '@app/shared/video/video.model'
10
11@Component({
12 selector: 'my-video-block',
13 templateUrl: './video-block.component.html',
14 styleUrls: [ './video-block.component.scss' ]
15})
16export class VideoBlockComponent extends FormReactive implements OnInit {
17 @Input() video: Video = null
18
19 @ViewChild('modal', { static: true }) modal: NgbModal
20
21 @Output() videoBlocked = new EventEmitter()
22
23 error: string = null
24
25 private openedModal: NgbModalRef
26
27 constructor (
28 protected formValidatorService: FormValidatorService,
29 private modalService: NgbModal,
30 private videoBlockValidatorsService: VideoBlockValidatorsService,
31 private videoBlocklistService: VideoBlockService,
32 private notifier: Notifier,
33 private i18n: I18n
34 ) {
35 super()
36 }
37
38 ngOnInit () {
39 const defaultValues = { unfederate: 'true' }
40
41 this.buildForm({
42 reason: this.videoBlockValidatorsService.VIDEO_BLOCK_REASON,
43 unfederate: null
44 }, defaultValues)
45 }
46
47 show () {
48 this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
49 }
50
51 hide () {
52 this.openedModal.close()
53 this.openedModal = null
54 }
55
56 block () {
57 const reason = this.form.value[ 'reason' ] || undefined
58 const unfederate = this.video.isLocal ? this.form.value[ 'unfederate' ] : undefined
59
60 this.videoBlocklistService.blockVideo(this.video.id, reason, unfederate)
61 .subscribe(
62 () => {
63 this.notifier.success(this.i18n('Video blocked.'))
64 this.hide()
65
66 this.video.blacklisted = true
67 this.video.blockedReason = reason
68
69 this.videoBlocked.emit()
70 },
71
72 err => this.notifier.error(err.message)
73 )
74 }
75}
diff --git a/client/src/app/shared/video/modals/video-download.component.html b/client/src/app/shared/video/modals/video-download.component.html
deleted file mode 100644
index c65e371ee..000000000
--- a/client/src/app/shared/video/modals/video-download.component.html
+++ /dev/null
@@ -1,108 +0,0 @@
1<ng-template #modal let-hide="close">
2 <div class="modal-header">
3 <h4 class="modal-title">
4 <ng-container i18n>Download</ng-container>
5
6 <div *ngIf="videoCaptions" ngbDropdown class="d-inline-block">
7 <span id="dropdownDownloadType" ngbDropdownToggle>
8 {{ type }}
9 </span>
10 <div ngbDropdownMenu aria-labelledby="dropdownDownloadType">
11 <button *ngIf="type === 'video'" (click)="switchToType('subtitles')" ngbDropdownItem i18n>subtitles</button>
12 <button *ngIf="type === 'subtitles'" (click)="switchToType('video')" ngbDropdownItem i18n>video</button>
13 </div>
14 </div>
15 </h4>
16 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
17 </div>
18
19 <div class="modal-body">
20 <div class="form-group">
21 <div class="input-group input-group-sm">
22 <div class="input-group-prepend peertube-select-container">
23 <select *ngIf="type === 'video'" [(ngModel)]="resolutionId" (ngModelChange)="onResolutionIdChange()">
24 <option *ngFor="let file of getVideoFiles()" [value]="file.resolution.id">{{ file.resolution.label }}</option>
25 </select>
26
27 <select *ngIf="type === 'subtitles'" [(ngModel)]="subtitleLanguageId">
28 <option *ngFor="let caption of videoCaptions" [value]="caption.language.id">{{ caption.language.label }}</option>
29 </select>
30 </div>
31
32 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
33 <div class="input-group-append">
34 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
35 <span class="glyphicon glyphicon-copy"></span>
36 </button>
37 </div>
38 </div>
39 </div>
40
41 <ng-container *ngIf="type === 'video' && videoFile?.metadata">
42 <div ngbNav #nav="ngbNav" class="nav-tabs">
43
44 <ng-container ngbNavItem>
45 <a ngbNavLink i18n>Format</a>
46 <ng-template ngbNavContent>
47 <div class="file-metadata">
48 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue">
49 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
50 <span class="metadata-attribute-value">{{ item.value.value }}</span>
51 </div>
52 </div>
53 </ng-template>
54 </ng-container>
55
56 <ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined">
57 <a ngbNavLink i18n>Video stream</a>
58 <ng-template ngbNavContent>
59 <div class="file-metadata">
60 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue">
61 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
62 <span class="metadata-attribute-value">{{ item.value.value }}</span>
63 </div>
64 </div>
65 </ng-template>
66 </ng-container>
67
68 <ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined">
69 <a ngbNavLink i18n>Audio stream</a>
70 <ng-template ngbNavContent>
71 <div class="file-metadata">
72 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue">
73 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
74 <span class="metadata-attribute-value">{{ item.value.value }}</span>
75 </div>
76 </div>
77 </ng-template>
78 </ng-container>
79 </div>
80
81 <div [ngbNavOutlet]="nav"></div>
82 </ng-container>
83
84 <div class="download-type" *ngIf="type === 'video'">
85 <div class="peertube-radio-container">
86 <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct">
87 <label i18n for="download-direct">Direct download</label>
88 </div>
89
90 <div class="peertube-radio-container">
91 <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent">
92 <label i18n for="download-torrent">Torrent (.torrent file)</label>
93 </div>
94 </div>
95 </div>
96
97 <div class="modal-footer inputs">
98 <input
99 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
100 (click)="hide()" (key.enter)="hide()"
101 >
102
103 <input
104 type="submit" i18n-value value="Download" class="action-button-submit"
105 (click)="download()"
106 >
107 </div>
108</ng-template>
diff --git a/client/src/app/shared/video/modals/video-download.component.scss b/client/src/app/shared/video/modals/video-download.component.scss
deleted file mode 100644
index b09078bea..000000000
--- a/client/src/app/shared/video/modals/video-download.component.scss
+++ /dev/null
@@ -1,64 +0,0 @@
1@import 'variables';
2@import 'mixins';
3
4.peertube-select-container {
5 @include peertube-select-container(100px);
6
7 border-top-right-radius: 0;
8 border-bottom-right-radius: 0;
9 border-right: none;
10
11 select {
12 height: inherit;
13 }
14}
15
16#dropdownDownloadType {
17 cursor: pointer;
18}
19
20.download-type {
21 margin-top: 30px;
22
23 .peertube-radio-container {
24 @include peertube-radio-container;
25
26 display: inline-block;
27 margin-right: 30px;
28 }
29}
30
31.file-metadata {
32 padding: 1rem;
33}
34
35.file-metadata .metadata-attribute {
36 font-size: 13px;
37 display: block;
38 margin-bottom: 12px;
39
40 .metadata-attribute-label {
41 min-width: 142px;
42 padding-right: 5px;
43 display: inline-block;
44 color: pvar(--greyForegroundColor);
45 font-weight: $font-bold;
46 }
47
48 a.metadata-attribute-value {
49 @include disable-default-a-behaviour;
50 color: pvar(--mainForegroundColor);
51
52 &:hover {
53 opacity: 0.9;
54 }
55 }
56
57 &.metadata-attribute-tags {
58 .metadata-attribute-value:not(:nth-child(2)) {
59 &::before {
60 content: ', '
61 }
62 }
63 }
64}
diff --git a/client/src/app/shared/video/modals/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts
deleted file mode 100644
index d77187821..000000000
--- a/client/src/app/shared/video/modals/video-download.component.ts
+++ /dev/null
@@ -1,208 +0,0 @@
1import { Component, ElementRef, ViewChild } from '@angular/core'
2import { VideoDetails } from '../../../shared/video/video-details.model'
3import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { AuthService, Notifier } from '@app/core'
6import { VideoPrivacy, VideoCaption, VideoFile } from '@shared/models'
7import { FfprobeFormat, FfprobeStream } from 'fluent-ffmpeg'
8import { mapValues, pick } from 'lodash-es'
9import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe'
10import { BytesPipe } from 'ngx-pipes'
11import { VideoService } from '../video.service'
12
13type DownloadType = 'video' | 'subtitles'
14type FileMetadata = { [key: string]: { label: string, value: string }}
15
16@Component({
17 selector: 'my-video-download',
18 templateUrl: './video-download.component.html',
19 styleUrls: [ './video-download.component.scss' ]
20})
21export class VideoDownloadComponent {
22 @ViewChild('modal', { static: true }) modal: ElementRef
23
24 downloadType: 'direct' | 'torrent' = 'torrent'
25 resolutionId: number | string = -1
26 subtitleLanguageId: string
27
28 video: VideoDetails
29 videoFile: VideoFile
30 videoFileMetadataFormat: FileMetadata
31 videoFileMetadataVideoStream: FileMetadata | undefined
32 videoFileMetadataAudioStream: FileMetadata | undefined
33 videoCaptions: VideoCaption[]
34 activeModal: NgbActiveModal
35
36 type: DownloadType = 'video'
37
38 private bytesPipe: BytesPipe
39 private numbersPipe: NumberFormatterPipe
40
41 constructor (
42 private notifier: Notifier,
43 private modalService: NgbModal,
44 private videoService: VideoService,
45 private auth: AuthService,
46 private i18n: I18n
47 ) {
48 this.bytesPipe = new BytesPipe()
49 this.numbersPipe = new NumberFormatterPipe()
50 }
51
52 get typeText () {
53 return this.type === 'video'
54 ? this.i18n('video')
55 : this.i18n('subtitles')
56 }
57
58 getVideoFiles () {
59 if (!this.video) return []
60
61 return this.video.getFiles()
62 }
63
64 show (video: VideoDetails, videoCaptions?: VideoCaption[]) {
65 this.video = video
66 this.videoCaptions = videoCaptions && videoCaptions.length ? videoCaptions : undefined
67
68 this.activeModal = this.modalService.open(this.modal, { centered: true })
69
70 this.resolutionId = this.getVideoFiles()[0].resolution.id
71 this.onResolutionIdChange()
72 if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id
73 }
74
75 onClose () {
76 this.video = undefined
77 this.videoCaptions = undefined
78 }
79
80 download () {
81 window.location.assign(this.getLink())
82 this.activeModal.close()
83 }
84
85 getLink () {
86 return this.type === 'subtitles' && this.videoCaptions
87 ? this.getSubtitlesLink()
88 : this.getVideoFileLink()
89 }
90
91 async onResolutionIdChange () {
92 this.videoFile = this.getVideoFile()
93 if (this.videoFile.metadata || !this.videoFile.metadataUrl) return
94
95 await this.hydrateMetadataFromMetadataUrl(this.videoFile)
96
97 this.videoFileMetadataFormat = this.videoFile
98 ? this.getMetadataFormat(this.videoFile.metadata.format)
99 : undefined
100 this.videoFileMetadataVideoStream = this.videoFile
101 ? this.getMetadataStream(this.videoFile.metadata.streams, 'video')
102 : undefined
103 this.videoFileMetadataAudioStream = this.videoFile
104 ? this.getMetadataStream(this.videoFile.metadata.streams, 'audio')
105 : undefined
106 }
107
108 getVideoFile () {
109 // HTML select send us a string, so convert it to a number
110 this.resolutionId = parseInt(this.resolutionId.toString(), 10)
111
112 const file = this.getVideoFiles().find(f => f.resolution.id === this.resolutionId)
113 if (!file) {
114 console.error('Could not find file with resolution %d.', this.resolutionId)
115 return
116 }
117 return file
118 }
119
120 getVideoFileLink () {
121 const file = this.videoFile
122 if (!file) return
123
124 const suffix = this.video.privacy.id === VideoPrivacy.PRIVATE || this.video.privacy.id === VideoPrivacy.INTERNAL
125 ? '?access_token=' + this.auth.getAccessToken()
126 : ''
127
128 switch (this.downloadType) {
129 case 'direct':
130 return file.fileDownloadUrl + suffix
131
132 case 'torrent':
133 return file.torrentDownloadUrl + suffix
134 }
135 }
136
137 getSubtitlesLink () {
138 return window.location.origin + this.videoCaptions.find(caption => caption.language.id === this.subtitleLanguageId).captionPath
139 }
140
141 activateCopiedMessage () {
142 this.notifier.success(this.i18n('Copied'))
143 }
144
145 switchToType (type: DownloadType) {
146 this.type = type
147 }
148
149 getMetadataFormat (format: FfprobeFormat) {
150 const keyToTranslateFunction = {
151 'encoder': (value: string) => ({ label: this.i18n('Encoder'), value }),
152 'format_long_name': (value: string) => ({ label: this.i18n('Format name'), value }),
153 'size': (value: number) => ({ label: this.i18n('Size'), value: this.bytesPipe.transform(value, 2) }),
154 'bit_rate': (value: number) => ({
155 label: this.i18n('Bitrate'),
156 value: `${this.numbersPipe.transform(value)}bps`
157 })
158 }
159
160 // flattening format
161 const sanitizedFormat = Object.assign(format, format.tags)
162 delete sanitizedFormat.tags
163
164 return mapValues(
165 pick(sanitizedFormat, Object.keys(keyToTranslateFunction)),
166 (val, key) => keyToTranslateFunction[key](val)
167 )
168 }
169
170 getMetadataStream (streams: FfprobeStream[], type: 'video' | 'audio') {
171 const stream = streams.find(s => s.codec_type === type)
172 if (!stream) return undefined
173
174 let keyToTranslateFunction = {
175 'codec_long_name': (value: string) => ({ label: this.i18n('Codec'), value }),
176 'profile': (value: string) => ({ label: this.i18n('Profile'), value }),
177 'bit_rate': (value: number) => ({
178 label: this.i18n('Bitrate'),
179 value: `${this.numbersPipe.transform(value)}bps`
180 })
181 }
182
183 if (type === 'video') {
184 keyToTranslateFunction = Object.assign(keyToTranslateFunction, {
185 'width': (value: number) => ({ label: this.i18n('Resolution'), value: `${value}x${stream.height}` }),
186 'display_aspect_ratio': (value: string) => ({ label: this.i18n('Aspect ratio'), value }),
187 'avg_frame_rate': (value: string) => ({ label: this.i18n('Average frame rate'), value }),
188 'pix_fmt': (value: string) => ({ label: this.i18n('Pixel format'), value })
189 })
190 } else {
191 keyToTranslateFunction = Object.assign(keyToTranslateFunction, {
192 'sample_rate': (value: number) => ({ label: this.i18n('Sample rate'), value }),
193 'channel_layout': (value: number) => ({ label: this.i18n('Channel Layout'), value })
194 })
195 }
196
197 return mapValues(
198 pick(stream, Object.keys(keyToTranslateFunction)),
199 (val, key) => keyToTranslateFunction[key](val)
200 )
201 }
202
203 private hydrateMetadataFromMetadataUrl (file: VideoFile) {
204 const observable = this.videoService.getVideoFileMetadata(file.metadataUrl)
205 observable.subscribe(res => file.metadata = res)
206 return observable.toPromise()
207 }
208}
diff --git a/client/src/app/shared/video/modals/video-report.component.html b/client/src/app/shared/video/modals/video-report.component.html
deleted file mode 100644
index d6beb6d2a..000000000
--- a/client/src/app/shared/video/modals/video-report.component.html
+++ /dev/null
@@ -1,97 +0,0 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Report video "{{ video.name }}"</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div>
6
7 <div class="modal-body">
8 <form novalidate [formGroup]="form" (ngSubmit)="report()">
9
10 <div class="row">
11 <div class="col-5 form-group">
12
13 <label i18n for="reportPredefinedReasons">What is the issue?</label>
14
15 <div class="ml-2 mt-2 d-flex flex-column">
16 <ng-container formGroupName="predefinedReasons">
17 <div class="form-group" *ngFor="let reason of predefinedReasons">
18 <my-peertube-checkbox formControlName="{{reason.id}}" labelText="{{reason.label}}">
19 <ng-template *ngIf="reason.help" ptTemplate="help">
20 <div [innerHTML]="reason.help"></div>
21 </ng-template>
22 <ng-container *ngIf="reason.description" ngProjectAs="description">
23 <div [innerHTML]="reason.description"></div>
24 </ng-container>
25 </my-peertube-checkbox>
26 </div>
27 </ng-container>
28 </div>
29
30 </div>
31
32 <div class="col-7">
33 <div class="row justify-content-center">
34 <div class="col-12 col-lg-9 mb-2">
35 <div class="screenratio">
36 <div [innerHTML]="embedHtml"></div>
37 </div>
38 </div>
39 </div>
40
41 <div class="mb-1 start-at" formGroupName="timestamp">
42 <my-peertube-checkbox
43 formControlName="hasStart"
44 i18n-labelText labelText="Start at"
45 ></my-peertube-checkbox>
46
47 <my-timestamp-input
48 [timestamp]="timestamp.startAt"
49 [maxTimestamp]="video.duration"
50 formControlName="startAt"
51 inputName="startAt"
52 >
53 </my-timestamp-input>
54 </div>
55
56 <div class="mb-3 stop-at" formGroupName="timestamp" *ngIf="timestamp.hasStart">
57 <my-peertube-checkbox
58 formControlName="hasEnd"
59 i18n-labelText labelText="Stop at"
60 ></my-peertube-checkbox>
61
62 <my-timestamp-input
63 [timestamp]="timestamp.endAt"
64 [maxTimestamp]="video.duration"
65 formControlName="endAt"
66 inputName="endAt"
67 >
68 </my-timestamp-input>
69 </div>
70
71 <div i18n class="information">
72 Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemoteVideo()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>.
73 </div>
74
75 <div class="form-group">
76 <textarea
77 i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus
78 [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control"
79 ></textarea>
80 <div *ngIf="formErrors.reason" class="form-error">
81 {{ formErrors.reason }}
82 </div>
83 </div>
84 </div>
85 </div>
86
87 <div class="form-group inputs">
88 <input
89 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
90 (click)="hide()" (key.enter)="hide()"
91 >
92 <input type="submit" i18n-value value="Submit" class="action-button-submit" [disabled]="!form.valid">
93 </div>
94
95 </form>
96 </div>
97</ng-template>
diff --git a/client/src/app/shared/video/modals/video-report.component.scss b/client/src/app/shared/video/modals/video-report.component.scss
deleted file mode 100644
index b2606cbd8..000000000
--- a/client/src/app/shared/video/modals/video-report.component.scss
+++ /dev/null
@@ -1,27 +0,0 @@
1@import 'variables';
2@import 'mixins';
3
4.information {
5 margin-bottom: 20px;
6}
7
8textarea {
9 @include peertube-textarea(100%, 100px);
10}
11
12.start-at,
13.stop-at {
14 width: 300px;
15 display: flex;
16 align-items: center;
17
18 my-timestamp-input {
19 margin-left: 10px;
20 }
21}
22
23.screenratio {
24 @include large-screen-ratio($selector: 'div, ::ng-deep iframe') {
25 left: 0;
26 };
27}
diff --git a/client/src/app/shared/video/modals/video-report.component.ts b/client/src/app/shared/video/modals/video-report.component.ts
deleted file mode 100644
index c2d441bba..000000000
--- a/client/src/app/shared/video/modals/video-report.component.ts
+++ /dev/null
@@ -1,163 +0,0 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { FormReactive } from '../../../shared/forms'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
6import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { VideoAbuseService } from '@app/shared/video-abuse'
10import { Video } from '@app/shared/video/video.model'
11import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
12import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
13import { VideoAbusePredefinedReasonsString, videoAbusePredefinedReasonsMap } from '@shared/models/videos/abuse/video-abuse-reason.model'
14import { mapValues, pickBy } from 'lodash-es'
15
16@Component({
17 selector: 'my-video-report',
18 templateUrl: './video-report.component.html',
19 styleUrls: [ './video-report.component.scss' ]
20})
21export class VideoReportComponent extends FormReactive implements OnInit {
22 @Input() video: Video = null
23
24 @ViewChild('modal', { static: true }) modal: NgbModal
25
26 error: string = null
27 predefinedReasons: { id: VideoAbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
28 embedHtml: SafeHtml
29
30 private openedModal: NgbModalRef
31
32 constructor (
33 protected formValidatorService: FormValidatorService,
34 private modalService: NgbModal,
35 private videoAbuseValidatorsService: VideoAbuseValidatorsService,
36 private videoAbuseService: VideoAbuseService,
37 private notifier: Notifier,
38 private sanitizer: DomSanitizer,
39 private i18n: I18n
40 ) {
41 super()
42 }
43
44 get currentHost () {
45 return window.location.host
46 }
47
48 get originHost () {
49 if (this.isRemoteVideo()) {
50 return this.video.account.host
51 }
52
53 return ''
54 }
55
56 get timestamp () {
57 return this.form.get('timestamp').value
58 }
59
60 getVideoEmbed () {
61 return this.sanitizer.bypassSecurityTrustHtml(
62 buildVideoEmbed(
63 buildVideoLink({
64 baseUrl: this.video.embedUrl,
65 title: false,
66 warningTitle: false
67 })
68 )
69 )
70 }
71
72 ngOnInit () {
73 this.buildForm({
74 reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON,
75 predefinedReasons: mapValues(videoAbusePredefinedReasonsMap, r => null),
76 timestamp: {
77 hasStart: null,
78 startAt: null,
79 hasEnd: null,
80 endAt: null
81 }
82 })
83
84 this.predefinedReasons = [
85 {
86 id: 'violentOrRepulsive',
87 label: this.i18n('Violent or repulsive'),
88 help: this.i18n('Contains offensive, violent, or coarse language or iconography.')
89 },
90 {
91 id: 'hatefulOrAbusive',
92 label: this.i18n('Hateful or abusive'),
93 help: this.i18n('Contains abusive, racist or sexist language or iconography.')
94 },
95 {
96 id: 'spamOrMisleading',
97 label: this.i18n('Spam, ad or false news'),
98 help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.')
99 },
100 {
101 id: 'privacy',
102 label: this.i18n('Privacy breach or doxxing'),
103 help: this.i18n('Contains personal information that could be used to track, identify, contact or impersonate someone (e.g. name, address, phone number, email, or credit card details).')
104 },
105 {
106 id: 'rights',
107 label: this.i18n('Intellectual property violation'),
108 help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.')
109 },
110 {
111 id: 'serverRules',
112 label: this.i18n('Breaks server rules'),
113 description: this.i18n('Anything not included in the above that breaks the terms of service, code of conduct, or general rules in place on the server.')
114 },
115 {
116 id: 'thumbnails',
117 label: this.i18n('Thumbnails'),
118 help: this.i18n('The above can only be seen in thumbnails.')
119 },
120 {
121 id: 'captions',
122 label: this.i18n('Captions'),
123 help: this.i18n('The above can only be seen in captions (please describe which).')
124 }
125 ]
126
127 this.embedHtml = this.getVideoEmbed()
128 }
129
130 show () {
131 this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' })
132 }
133
134 hide () {
135 this.openedModal.close()
136 this.openedModal = null
137 }
138
139 report () {
140 const reason = this.form.get('reason').value
141 const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as VideoAbusePredefinedReasonsString[]
142 const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value
143
144 this.videoAbuseService.reportVideo({
145 id: this.video.id,
146 reason,
147 predefinedReasons,
148 startAt: hasStart && startAt ? startAt : undefined,
149 endAt: hasEnd && endAt ? endAt : undefined
150 }).subscribe(
151 () => {
152 this.notifier.success(this.i18n('Video reported.'))
153 this.hide()
154 },
155
156 err => this.notifier.error(err.message)
157 )
158 }
159
160 isRemoteVideo () {
161 return !this.video.isLocal
162 }
163}