aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared')
-rw-r--r--client/src/app/shared/angular/highlight.pipe.ts54
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.ts6
-rw-r--r--client/src/app/shared/buttons/button.component.scss23
-rw-r--r--client/src/app/shared/buttons/edit-button.component.ts2
-rw-r--r--client/src/app/shared/forms/form-validators/custom-config-validators.service.ts2
-rw-r--r--client/src/app/shared/forms/form-validators/user-validators.service.ts12
-rw-r--r--client/src/app/shared/forms/input-readonly-copy.component.html2
-rw-r--r--client/src/app/shared/forms/markdown-textarea.component.ts2
-rw-r--r--client/src/app/shared/images/global-icon.component.ts1
-rw-r--r--client/src/app/shared/images/preview-upload.component.ts4
-rw-r--r--client/src/app/shared/instance/instance-features-table.component.html15
-rw-r--r--client/src/app/shared/instance/instance-statistics.component.ts23
-rw-r--r--client/src/app/shared/menu/top-menu-dropdown.component.ts3
-rw-r--r--client/src/app/shared/misc/list-overflow.component.html35
-rw-r--r--client/src/app/shared/misc/list-overflow.component.scss61
-rw-r--r--client/src/app/shared/misc/list-overflow.component.ts119
-rw-r--r--client/src/app/shared/moderation/user-ban-modal.component.ts2
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.ts2
-rw-r--r--client/src/app/shared/renderer/markdown.service.ts4
-rw-r--r--client/src/app/shared/rest/rest-table.ts8
-rw-r--r--client/src/app/shared/rest/rest.service.ts2
-rw-r--r--client/src/app/shared/shared.module.ts12
-rw-r--r--client/src/app/shared/user-subscription/subscribe-button.component.html2
-rw-r--r--client/src/app/shared/video-abuse/video-abuse.service.ts2
-rw-r--r--client/src/app/shared/video-blacklist/video-blacklist.service.ts2
-rw-r--r--client/src/app/shared/video-import/video-import.service.ts2
-rw-r--r--client/src/app/shared/video-ownership/video-ownership.service.ts2
-rw-r--r--client/src/app/shared/video-playlist/video-add-to-playlist.component.html2
-rw-r--r--client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts2
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts3
-rw-r--r--client/src/app/shared/video/infinite-scroller.directive.ts4
-rw-r--r--client/src/app/shared/video/modals/video-blacklist.component.ts6
-rw-r--r--client/src/app/shared/video/modals/video-download.component.html4
-rw-r--r--client/src/app/shared/video/modals/video-download.component.ts2
-rw-r--r--client/src/app/shared/video/modals/video-report.component.ts6
-rw-r--r--client/src/app/shared/video/redundancy.service.ts73
-rw-r--r--client/src/app/shared/video/video-actions-dropdown.component.ts38
-rw-r--r--client/src/app/shared/video/video-miniature.component.html2
-rw-r--r--client/src/app/shared/video/video-miniature.component.ts5
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.scss2
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.ts2
-rw-r--r--client/src/app/shared/video/video.model.ts19
42 files changed, 497 insertions, 77 deletions
diff --git a/client/src/app/shared/angular/highlight.pipe.ts b/client/src/app/shared/angular/highlight.pipe.ts
new file mode 100644
index 000000000..fb6042280
--- /dev/null
+++ b/client/src/app/shared/angular/highlight.pipe.ts
@@ -0,0 +1,54 @@
1import { PipeTransform, Pipe } from '@angular/core'
2import { SafeHtml } from '@angular/platform-browser'
3
4// Thanks https://gist.github.com/adamrecsko/0f28f474eca63e0279455476cc11eca7#gistcomment-2917369
5@Pipe({ name: 'highlight' })
6export class HighlightPipe implements PipeTransform {
7 /* use this for single match search */
8 static SINGLE_MATCH = 'Single-Match'
9 /* use this for single match search with a restriction that target should start with search string */
10 static SINGLE_AND_STARTS_WITH_MATCH = 'Single-And-StartsWith-Match'
11 /* use this for global search */
12 static MULTI_MATCH = 'Multi-Match'
13
14 // tslint:disable-next-line:no-empty
15 constructor () {}
16
17 transform (
18 contentString: string = null,
19 stringToHighlight: string = null,
20 option = 'Single-And-StartsWith-Match',
21 caseSensitive = false,
22 highlightStyleName = 'search-highlight'
23 ): SafeHtml {
24 if (stringToHighlight && contentString && option) {
25 let regex: any = ''
26 const caseFlag: string = !caseSensitive ? 'i' : ''
27 switch (option) {
28 case 'Single-Match': {
29 regex = new RegExp(stringToHighlight, caseFlag)
30 break
31 }
32 case 'Single-And-StartsWith-Match': {
33 regex = new RegExp('^' + stringToHighlight, caseFlag)
34 break
35 }
36 case 'Multi-Match': {
37 regex = new RegExp(stringToHighlight, 'g' + caseFlag)
38 break
39 }
40 default: {
41 // default will be a global case-insensitive match
42 regex = new RegExp(stringToHighlight, 'gi')
43 }
44 }
45 const replaced = contentString.replace(
46 regex,
47 (match) => `<span class="${highlightStyleName}">${match}</span>`
48 )
49 return replaced
50 } else {
51 return contentString
52 }
53 }
54}
diff --git a/client/src/app/shared/buttons/action-dropdown.component.ts b/client/src/app/shared/buttons/action-dropdown.component.ts
index a8b3ab16c..6649b092a 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.ts
+++ b/client/src/app/shared/buttons/action-dropdown.component.ts
@@ -34,10 +34,10 @@ export class ActionDropdownComponent<T> {
34 @Input() label: string 34 @Input() label: string
35 @Input() theme: DropdownTheme = 'grey' 35 @Input() theme: DropdownTheme = 'grey'
36 36
37 getActions () { 37 getActions (): DropdownAction<T>[][] {
38 if (this.actions.length !== 0 && Array.isArray(this.actions[0])) return this.actions 38 if (this.actions.length !== 0 && Array.isArray(this.actions[0])) return this.actions as DropdownAction<T>[][]
39 39
40 return [ this.actions ] 40 return [ this.actions as DropdownAction<T>[] ]
41 } 41 }
42 42
43 areActionsDisplayed (actions: Array<DropdownAction<T> | DropdownAction<T>[]>, entry: T): boolean { 43 areActionsDisplayed (actions: Array<DropdownAction<T> | DropdownAction<T>[]>, entry: T): boolean {
diff --git a/client/src/app/shared/buttons/button.component.scss b/client/src/app/shared/buttons/button.component.scss
index 2a8cfc748..3ccfefd7e 100644
--- a/client/src/app/shared/buttons/button.component.scss
+++ b/client/src/app/shared/buttons/button.component.scss
@@ -10,11 +10,26 @@ my-small-loader ::ng-deep .root {
10.action-button { 10.action-button {
11 @include peertube-button-link; 11 @include peertube-button-link;
12 @include button-with-icon(21px, 0, -2px); 12 @include button-with-icon(21px, 0, -2px);
13}
13 14
14 // FIXME: Firefox does not apply global .orange-button icon color 15.orange-button {
15 &.orange-button { 16 @include peertube-button;
16 @include apply-svg-color(#fff) 17 @include orange-button;
17 } 18}
19
20.orange-button-link {
21 @include peertube-button-link;
22 @include orange-button;
23}
24
25.grey-button {
26 @include peertube-button;
27 @include grey-button;
28}
29
30.grey-button-link {
31 @include peertube-button-link;
32 @include grey-button;
18} 33}
19 34
20// In a table, try to minimize the space taken by this button 35// In a table, try to minimize the space taken by this button
diff --git a/client/src/app/shared/buttons/edit-button.component.ts b/client/src/app/shared/buttons/edit-button.component.ts
index 1fe4f7b30..9cfe1a3bb 100644
--- a/client/src/app/shared/buttons/edit-button.component.ts
+++ b/client/src/app/shared/buttons/edit-button.component.ts
@@ -8,5 +8,5 @@ import { Component, Input } from '@angular/core'
8 8
9export class EditButtonComponent { 9export class EditButtonComponent {
10 @Input() label: string 10 @Input() label: string
11 @Input() routerLink: string[] = [] 11 @Input() routerLink: string[] | string = []
12} 12}
diff --git a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
index 767e3f026..d20754d11 100644
--- a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
@@ -56,7 +56,7 @@ export class CustomConfigValidatorsService {
56 } 56 }
57 57
58 this.SIGNUP_LIMIT = { 58 this.SIGNUP_LIMIT = {
59 VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], 59 VALIDATORS: [ Validators.required, Validators.min(-1), Validators.pattern('-?[0-9]+') ],
60 MESSAGES: { 60 MESSAGES: {
61 'required': this.i18n('Signup limit is required.'), 61 'required': this.i18n('Signup limit is required.'),
62 'min': this.i18n('Signup limit must be greater than 1.'), 62 'min': this.i18n('Signup limit must be greater than 1.'),
diff --git a/client/src/app/shared/forms/form-validators/user-validators.service.ts b/client/src/app/shared/forms/form-validators/user-validators.service.ts
index 4dff3e422..13b9228d4 100644
--- a/client/src/app/shared/forms/form-validators/user-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/user-validators.service.ts
@@ -8,6 +8,7 @@ export class UserValidatorsService {
8 readonly USER_USERNAME: BuildFormValidator 8 readonly USER_USERNAME: BuildFormValidator
9 readonly USER_EMAIL: BuildFormValidator 9 readonly USER_EMAIL: BuildFormValidator
10 readonly USER_PASSWORD: BuildFormValidator 10 readonly USER_PASSWORD: BuildFormValidator
11 readonly USER_PASSWORD_OPTIONAL: BuildFormValidator
11 readonly USER_CONFIRM_PASSWORD: BuildFormValidator 12 readonly USER_CONFIRM_PASSWORD: BuildFormValidator
12 readonly USER_VIDEO_QUOTA: BuildFormValidator 13 readonly USER_VIDEO_QUOTA: BuildFormValidator
13 readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator 14 readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator
@@ -56,6 +57,17 @@ export class UserValidatorsService {
56 } 57 }
57 } 58 }
58 59
60 this.USER_PASSWORD_OPTIONAL = {
61 VALIDATORS: [
62 Validators.minLength(6),
63 Validators.maxLength(255)
64 ],
65 MESSAGES: {
66 'minlength': this.i18n('Password must be at least 6 characters long.'),
67 'maxlength': this.i18n('Password cannot be more than 255 characters long.')
68 }
69 }
70
59 this.USER_CONFIRM_PASSWORD = { 71 this.USER_CONFIRM_PASSWORD = {
60 VALIDATORS: [], 72 VALIDATORS: [],
61 MESSAGES: { 73 MESSAGES: {
diff --git a/client/src/app/shared/forms/input-readonly-copy.component.html b/client/src/app/shared/forms/input-readonly-copy.component.html
index 27571b63f..b6a56ec44 100644
--- a/client/src/app/shared/forms/input-readonly-copy.component.html
+++ b/client/src/app/shared/forms/input-readonly-copy.component.html
@@ -2,7 +2,7 @@
2 <input #urlInput (click)="urlInput.select()" type="text" class="form-control readonly" readonly [value]="value" /> 2 <input #urlInput (click)="urlInput.select()" type="text" class="form-control readonly" readonly [value]="value" />
3 3
4 <div class="input-group-append"> 4 <div class="input-group-append">
5 <button [ngxClipboard]="urlInput" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> 5 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
6 <span class="glyphicon glyphicon-copy"></span> 6 <span class="glyphicon glyphicon-copy"></span>
7 </button> 7 </button>
8 </div> 8 </div>
diff --git a/client/src/app/shared/forms/markdown-textarea.component.ts b/client/src/app/shared/forms/markdown-textarea.component.ts
index 19cd37573..cbcfdfe78 100644
--- a/client/src/app/shared/forms/markdown-textarea.component.ts
+++ b/client/src/app/shared/forms/markdown-textarea.component.ts
@@ -21,7 +21,7 @@ import { MarkdownService } from '@app/shared/renderer'
21 21
22export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { 22export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
23 @Input() content = '' 23 @Input() content = ''
24 @Input() classes: string[] = [] 24 @Input() classes: string[] | { [klass: string]: any[] | any } = []
25 @Input() textareaWidth = '100%' 25 @Input() textareaWidth = '100%'
26 @Input() textareaHeight = '150px' 26 @Input() textareaHeight = '150px'
27 @Input() previewColumn = false 27 @Input() previewColumn = false
diff --git a/client/src/app/shared/images/global-icon.component.ts b/client/src/app/shared/images/global-icon.component.ts
index 806aca347..b6e641228 100644
--- a/client/src/app/shared/images/global-icon.component.ts
+++ b/client/src/app/shared/images/global-icon.component.ts
@@ -1,6 +1,5 @@
1import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core' 1import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core'
2import { HooksService } from '@app/core/plugins/hooks.service' 2import { HooksService } from '@app/core/plugins/hooks.service'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4 3
5const icons = { 4const icons = {
6 'add': require('!!raw-loader?!../../../assets/images/global/add.svg'), 5 'add': require('!!raw-loader?!../../../assets/images/global/add.svg'),
diff --git a/client/src/app/shared/images/preview-upload.component.ts b/client/src/app/shared/images/preview-upload.component.ts
index f56f5b1f8..85a2173e9 100644
--- a/client/src/app/shared/images/preview-upload.component.ts
+++ b/client/src/app/shared/images/preview-upload.component.ts
@@ -26,7 +26,7 @@ export class PreviewUploadComponent implements OnInit, ControlValueAccessor {
26 allowedExtensionsMessage = '' 26 allowedExtensionsMessage = ''
27 27
28 private serverConfig: ServerConfig 28 private serverConfig: ServerConfig
29 private file: File 29 private file: Blob
30 30
31 constructor ( 31 constructor (
32 private sanitizer: DomSanitizer, 32 private sanitizer: DomSanitizer,
@@ -49,7 +49,7 @@ export class PreviewUploadComponent implements OnInit, ControlValueAccessor {
49 this.allowedExtensionsMessage = this.videoImageExtensions.join(', ') 49 this.allowedExtensionsMessage = this.videoImageExtensions.join(', ')
50 } 50 }
51 51
52 onFileChanged (file: File) { 52 onFileChanged (file: Blob) {
53 this.file = file 53 this.file = file
54 54
55 this.propagateChange(this.file) 55 this.propagateChange(this.file)
diff --git a/client/src/app/shared/instance/instance-features-table.component.html b/client/src/app/shared/instance/instance-features-table.component.html
index fd8b3354f..99b854d13 100644
--- a/client/src/app/shared/instance/instance-features-table.component.html
+++ b/client/src/app/shared/instance/instance-features-table.component.html
@@ -37,8 +37,8 @@
37 <tr> 37 <tr>
38 <td i18n class="sub-label">Video uploads</td> 38 <td i18n class="sub-label">Video uploads</td>
39 <td> 39 <td>
40 <span *ngIf="serverConfig.autoBlacklist.videos.ofUsers.enabled">Requires manual validation by moderators</span> 40 <span i18n *ngIf="serverConfig.autoBlacklist.videos.ofUsers.enabled">Requires manual validation by moderators</span>
41 <span *ngIf="!serverConfig.autoBlacklist.videos.ofUsers.enabled">Automatically published</span> 41 <span i18n *ngIf="!serverConfig.autoBlacklist.videos.ofUsers.enabled">Automatically published</span>
42 </td> 42 </td>
43 </tr> 43 </tr>
44 44
@@ -91,5 +91,16 @@
91 <my-feature-boolean [value]="serverConfig.tracker.enabled"></my-feature-boolean> 91 <my-feature-boolean [value]="serverConfig.tracker.enabled"></my-feature-boolean>
92 </td> 92 </td>
93 </tr> 93 </tr>
94
95 <tr>
96 <td i18n class="label" colspan="2">Search</td>
97 </tr>
98
99 <tr>
100 <td i18n class="sub-label">Users can resolve distant content</td>
101 <td>
102 <my-feature-boolean [value]="serverConfig.search.remoteUri.users"></my-feature-boolean>
103 </td>
104 </tr>
94 </table> 105 </table>
95</div> 106</div>
diff --git a/client/src/app/shared/instance/instance-statistics.component.ts b/client/src/app/shared/instance/instance-statistics.component.ts
index 8ec728f05..40aa8a4c0 100644
--- a/client/src/app/shared/instance/instance-statistics.component.ts
+++ b/client/src/app/shared/instance/instance-statistics.component.ts
@@ -1,9 +1,6 @@
1import { map } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http'
3import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { ServerStats } from '@shared/models/server' 2import { ServerStats } from '@shared/models/server'
6import { environment } from '../../../environments/environment' 3import { ServerService } from '@app/core'
7 4
8@Component({ 5@Component({
9 selector: 'my-instance-statistics', 6 selector: 'my-instance-statistics',
@@ -11,27 +8,15 @@ import { environment } from '../../../environments/environment'
11 styleUrls: [ './instance-statistics.component.scss' ] 8 styleUrls: [ './instance-statistics.component.scss' ]
12}) 9})
13export class InstanceStatisticsComponent implements OnInit { 10export class InstanceStatisticsComponent implements OnInit {
14 private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
15
16 serverStats: ServerStats = null 11 serverStats: ServerStats = null
17 12
18 constructor ( 13 constructor (
19 private http: HttpClient, 14 private serverService: ServerService
20 private i18n: I18n
21 ) { 15 ) {
22 } 16 }
23 17
24 ngOnInit () { 18 ngOnInit () {
25 this.getStats() 19 this.serverService.getServerStats()
26 .subscribe( 20 .subscribe(res => this.serverStats = res)
27 res => {
28 this.serverStats = res
29 }
30 )
31 }
32
33 getStats () {
34 return this.http
35 .get<ServerStats>(InstanceStatisticsComponent.BASE_STATS_URL)
36 } 21 }
37} 22}
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.ts b/client/src/app/shared/menu/top-menu-dropdown.component.ts
index 5ccdafb54..24a083654 100644
--- a/client/src/app/shared/menu/top-menu-dropdown.component.ts
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.ts
@@ -49,8 +49,7 @@ export class TopMenuDropdownComponent implements OnInit, OnDestroy {
49 e => e.children && e.children.some(c => !!c.iconName) 49 e => e.children && e.children.some(c => !!c.iconName)
50 ) 50 )
51 51
52 // FIXME: We have to set body for the container to avoid because of scroll overflow on mobile view 52 // We have to set body for the container to avoid scroll overflow on mobile view
53 // But this break our hovering system
54 if (this.screen.isInMobileView()) { 53 if (this.screen.isInMobileView()) {
55 this.container = 'body' 54 this.container = 'body'
56 } 55 }
diff --git a/client/src/app/shared/misc/list-overflow.component.html b/client/src/app/shared/misc/list-overflow.component.html
new file mode 100644
index 000000000..986572801
--- /dev/null
+++ b/client/src/app/shared/misc/list-overflow.component.html
@@ -0,0 +1,35 @@
1<div #itemsParent class="d-flex align-items-center text-nowrap w-100 list-overflow-parent">
2 <span [id]="getId(id)" #itemsRendered *ngFor="let item of items; index as id">
3 <ng-container *ngTemplateOutlet="itemTemplate; context: {item: item}"></ng-container>
4 </span>
5
6 <ng-container *ngIf="isMenuDisplayed()">
7 <button *ngIf="isInMobileView" class="btn btn-outline-secondary btn-sm list-overflow-menu" (click)="toggleModal()">
8 <span class="glyphicon glyphicon-chevron-down"></span>
9 </button>
10
11 <div *ngIf="!isInMobileView" class="list-overflow-menu" ngbDropdown container="body" #dropdown="ngbDropdown" (mouseleave)="closeDropdownIfHovered(dropdown)" (mouseenter)="openDropdownOnHover(dropdown)">
12 <button class="btn btn-outline-secondary btn-sm" [ngClass]="{ routeActive: active }"
13 ngbDropdownAnchor (click)="dropdownAnchorClicked(dropdown)" role="button"
14 >
15 <span class="glyphicon glyphicon-chevron-down"></span>
16 </button>
17
18 <div ngbDropdownMenu>
19 <a *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length"
20 [routerLink]="item.routerLink" routerLinkActive="active" class="dropdown-item">
21 {{ item.label }}
22 </a>
23 </div>
24 </div>
25 </ng-container>
26</div >
27
28<ng-template #modal let-close="close" let-dismiss="dismiss">
29 <div class="modal-body">
30 <a *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length"
31 [routerLink]="item.routerLink" routerLinkActive="active" (click)="dismissOtherModals()">
32 {{ item.label }}
33 </a>
34 </div>
35</ng-template>
diff --git a/client/src/app/shared/misc/list-overflow.component.scss b/client/src/app/shared/misc/list-overflow.component.scss
new file mode 100644
index 000000000..1e5fe4c10
--- /dev/null
+++ b/client/src/app/shared/misc/list-overflow.component.scss
@@ -0,0 +1,61 @@
1@import '_mixins';
2
3:host {
4 width: 100%;
5}
6
7.list-overflow-parent {
8 overflow: hidden;
9}
10
11.list-overflow-menu {
12 position: absolute;
13 right: 25px;
14}
15
16button {
17 width: 30px;
18 border: none;
19
20 &::after {
21 display: none;
22 }
23
24 &.routeActive {
25 &::after {
26 display: inherit;
27 border: 2px solid var(--mainColor);
28 position: relative;
29 right: 95%;
30 top: 50%;
31 }
32 }
33}
34
35::ng-deep .dropdown-menu {
36 margin-top: 0 !important;
37 position: static;
38 right: auto;
39 bottom: auto
40}
41
42.modal-body {
43 a {
44 @include disable-default-a-behaviour;
45
46 color: currentColor;
47 box-sizing: border-box;
48 display: block;
49 font-size: 1.2rem;
50 padding: 9px 12px;
51 text-align: initial;
52 text-transform: unset;
53 width: 100%;
54
55 &.active {
56 color: var(--mainBackgroundColor) !important;
57 background-color: var(--mainHoverColor);
58 opacity: .9;
59 }
60 }
61}
diff --git a/client/src/app/shared/misc/list-overflow.component.ts b/client/src/app/shared/misc/list-overflow.component.ts
new file mode 100644
index 000000000..c493ab795
--- /dev/null
+++ b/client/src/app/shared/misc/list-overflow.component.ts
@@ -0,0 +1,119 @@
1import {
2 AfterViewInit,
3 ChangeDetectionStrategy,
4 ChangeDetectorRef,
5 Component,
6 ElementRef,
7 HostListener,
8 Input,
9 QueryList,
10 TemplateRef,
11 ViewChild,
12 ViewChildren
13} from '@angular/core'
14import { NgbDropdown, NgbModal } from '@ng-bootstrap/ng-bootstrap'
15import { lowerFirst, uniqueId } from 'lodash-es'
16import { ScreenService } from './screen.service'
17import { take } from 'rxjs/operators'
18
19export interface ListOverflowItem {
20 label: string
21 routerLink: string | any[]
22}
23
24@Component({
25 selector: 'list-overflow',
26 templateUrl: './list-overflow.component.html',
27 styleUrls: [ './list-overflow.component.scss' ],
28 changeDetection: ChangeDetectionStrategy.OnPush
29})
30export class ListOverflowComponent<T extends ListOverflowItem> implements AfterViewInit {
31 @ViewChild('modal', { static: true }) modal: ElementRef
32 @ViewChild('itemsParent', { static: true }) parent: ElementRef<HTMLDivElement>
33 @ViewChildren('itemsRendered') itemsRendered: QueryList<ElementRef>
34 @Input() items: T[]
35 @Input() itemTemplate: TemplateRef<{item: T}>
36
37 showItemsUntilIndexExcluded: number
38 active = false
39 isInTouchScreen = false
40 isInMobileView = false
41
42 private openedOnHover = false
43
44 constructor (
45 private cdr: ChangeDetectorRef,
46 private modalService: NgbModal,
47 private screenService: ScreenService
48 ) {}
49
50 ngAfterViewInit () {
51 setTimeout(() => this.onWindowResize(), 0)
52 }
53
54 isMenuDisplayed () {
55 return !!this.showItemsUntilIndexExcluded
56 }
57
58 @HostListener('window:resize', ['$event'])
59 onWindowResize () {
60 this.isInTouchScreen = !!this.screenService.isInTouchScreen()
61 this.isInMobileView = !!this.screenService.isInMobileView()
62
63 const parentWidth = this.parent.nativeElement.getBoundingClientRect().width
64 let showItemsUntilIndexExcluded: number
65 let accWidth = 0
66
67 for (const [index, el] of this.itemsRendered.toArray().entries()) {
68 accWidth += el.nativeElement.getBoundingClientRect().width
69 if (showItemsUntilIndexExcluded === undefined) {
70 showItemsUntilIndexExcluded = (parentWidth < accWidth) ? index : undefined
71 }
72
73 const e = document.getElementById(this.getId(index))
74 const shouldBeVisible = showItemsUntilIndexExcluded ? index < showItemsUntilIndexExcluded : true
75 e.style.visibility = shouldBeVisible ? 'inherit' : 'hidden'
76 }
77
78 this.showItemsUntilIndexExcluded = showItemsUntilIndexExcluded
79 this.cdr.markForCheck()
80 }
81
82 openDropdownOnHover (dropdown: NgbDropdown) {
83 this.openedOnHover = true
84 dropdown.open()
85
86 // Menu was closed
87 dropdown.openChange
88 .pipe(take(1))
89 .subscribe(() => this.openedOnHover = false)
90 }
91
92 dropdownAnchorClicked (dropdown: NgbDropdown) {
93 if (this.openedOnHover) {
94 this.openedOnHover = false
95 return
96 }
97
98 return dropdown.toggle()
99 }
100
101 closeDropdownIfHovered (dropdown: NgbDropdown) {
102 if (this.openedOnHover === false) return
103
104 dropdown.close()
105 this.openedOnHover = false
106 }
107
108 toggleModal () {
109 this.modalService.open(this.modal, { centered: true })
110 }
111
112 dismissOtherModals () {
113 this.modalService.dismissAll()
114 }
115
116 getId (id: number | string = uniqueId()): string {
117 return lowerFirst(this.constructor.name) + '_' + id
118 }
119}
diff --git a/client/src/app/shared/moderation/user-ban-modal.component.ts b/client/src/app/shared/moderation/user-ban-modal.component.ts
index cf0e1577a..1647e3691 100644
--- a/client/src/app/shared/moderation/user-ban-modal.component.ts
+++ b/client/src/app/shared/moderation/user-ban-modal.component.ts
@@ -39,7 +39,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
39 39
40 openModal (user: User | User[]) { 40 openModal (user: User | User[]) {
41 this.usersToBan = user 41 this.usersToBan = user
42 this.openedModal = this.modalService.open(this.modal) 42 this.openedModal = this.modalService.open(this.modal, { centered: true })
43 } 43 }
44 44
45 hide () { 45 hide () {
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
index 11d8588f4..9197556b0 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
@@ -14,7 +14,7 @@ import { ServerConfig } from '@shared/models'
14 templateUrl: './user-moderation-dropdown.component.html' 14 templateUrl: './user-moderation-dropdown.component.html'
15}) 15})
16export class UserModerationDropdownComponent implements OnInit, OnChanges { 16export class UserModerationDropdownComponent implements OnInit, OnChanges {
17 @ViewChild('userBanModal', { static: false }) userBanModal: UserBanModalComponent 17 @ViewChild('userBanModal') userBanModal: UserBanModalComponent
18 18
19 @Input() user: User 19 @Input() user: User
20 @Input() account: Account 20 @Input() account: Account
diff --git a/client/src/app/shared/renderer/markdown.service.ts b/client/src/app/shared/renderer/markdown.service.ts
index 0d3fde537..f0c87326f 100644
--- a/client/src/app/shared/renderer/markdown.service.ts
+++ b/client/src/app/shared/renderer/markdown.service.ts
@@ -1,7 +1,7 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { MarkdownIt } from 'markdown-it'
3import { buildVideoLink } from '../../../assets/player/utils' 2import { buildVideoLink } from '../../../assets/player/utils'
4import { HtmlRendererService } from '@app/shared/renderer/html-renderer.service' 3import { HtmlRendererService } from '@app/shared/renderer/html-renderer.service'
4import * as MarkdownIt from 'markdown-it'
5 5
6type MarkdownParsers = { 6type MarkdownParsers = {
7 textMarkdownIt: MarkdownIt 7 textMarkdownIt: MarkdownIt
@@ -100,7 +100,7 @@ export class MarkdownService {
100 } 100 }
101 101
102 private async createMarkdownIt (config: MarkdownConfig) { 102 private async createMarkdownIt (config: MarkdownConfig) {
103 // FIXME: import('...') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function 103 // FIXME: import('...') returns a struct module, containing a "default" field
104 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default 104 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
105 105
106 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html }) 106 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html })
diff --git a/client/src/app/shared/rest/rest-table.ts b/client/src/app/shared/rest/rest-table.ts
index c180346af..a33e99e25 100644
--- a/client/src/app/shared/rest/rest-table.ts
+++ b/client/src/app/shared/rest/rest-table.ts
@@ -1,6 +1,5 @@
1import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' 1import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage'
2import { LazyLoadEvent } from 'primeng/components/common/lazyloadevent' 2import { LazyLoadEvent, SortMeta } from 'primeng/api'
3import { SortMeta } from 'primeng/components/common/sortmeta'
4import { RestPagination } from './rest-pagination' 3import { RestPagination } from './rest-pagination'
5import { Subject } from 'rxjs' 4import { Subject } from 'rxjs'
6import { debounceTime, distinctUntilChanged } from 'rxjs/operators' 5import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
@@ -66,8 +65,9 @@ export abstract class RestTable {
66 }) 65 })
67 } 66 }
68 67
69 onSearch (search: string) { 68 onSearch (event: Event) {
70 this.searchStream.next(search) 69 const target = event.target as HTMLInputElement
70 this.searchStream.next(target.value)
71 } 71 }
72 72
73 protected abstract loadData (): void 73 protected abstract loadData (): void
diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts
index 16bb6d82c..5bd2b5e43 100644
--- a/client/src/app/shared/rest/rest.service.ts
+++ b/client/src/app/shared/rest/rest.service.ts
@@ -1,6 +1,6 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { HttpParams } from '@angular/common/http' 2import { HttpParams } from '@angular/common/http'
3import { SortMeta } from 'primeng/components/common/sortmeta' 3import { SortMeta } from 'primeng/api'
4import { ComponentPagination, ComponentPaginationLight } from './component-pagination.model' 4import { ComponentPagination, ComponentPaginationLight } from './component-pagination.model'
5 5
6import { RestPagination } from './rest-pagination' 6import { RestPagination } from './rest-pagination'
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index b2eb13f73..30b3ba0c1 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -5,9 +5,10 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
5import { RouterModule } from '@angular/router' 5import { RouterModule } from '@angular/router'
6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' 6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
7import { HelpComponent } from '@app/shared/misc/help.component' 7import { HelpComponent } from '@app/shared/misc/help.component'
8import { ListOverflowComponent } from '@app/shared/misc/list-overflow.component'
8import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' 9import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
9import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' 10import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
10import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' 11import { SharedModule as PrimeSharedModule } from 'primeng/api'
11import { AUTH_INTERCEPTOR_PROVIDER } from './auth' 12import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
12import { ButtonComponent } from './buttons/button.component' 13import { ButtonComponent } from './buttons/button.component'
13import { DeleteButtonComponent } from './buttons/delete-button.component' 14import { DeleteButtonComponent } from './buttons/delete-button.component'
@@ -88,16 +89,18 @@ import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe'
88import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe' 89import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe'
89import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe' 90import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe'
90import { FromNowPipe } from '@app/shared/angular/from-now.pipe' 91import { FromNowPipe } from '@app/shared/angular/from-now.pipe'
92import { HighlightPipe } from '@app/shared/angular/highlight.pipe'
91import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' 93import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
92import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component' 94import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component'
93import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component' 95import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component'
94import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' 96import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component'
95import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' 97import { VideoReportComponent } from '@app/shared/video/modals/video-report.component'
96import { ClipboardModule } from 'ngx-clipboard'
97import { FollowService } from '@app/shared/instance/follow.service' 98import { FollowService } from '@app/shared/instance/follow.service'
98import { MultiSelectModule } from 'primeng/multiselect' 99import { MultiSelectModule } from 'primeng/multiselect'
99import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' 100import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component'
100import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' 101import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component'
102import { RedundancyService } from '@app/shared/video/redundancy.service'
103import { ClipboardModule } from '@angular/cdk/clipboard'
101 104
102@NgModule({ 105@NgModule({
103 imports: [ 106 imports: [
@@ -147,6 +150,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop
147 NumberFormatterPipe, 150 NumberFormatterPipe,
148 ObjectLengthPipe, 151 ObjectLengthPipe,
149 FromNowPipe, 152 FromNowPipe,
153 HighlightPipe,
150 PeerTubeTemplateDirective, 154 PeerTubeTemplateDirective,
151 VideoDurationPipe, 155 VideoDurationPipe,
152 156
@@ -155,6 +159,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop
155 InfiniteScrollerDirective, 159 InfiniteScrollerDirective,
156 TextareaAutoResizeDirective, 160 TextareaAutoResizeDirective,
157 HelpComponent, 161 HelpComponent,
162 ListOverflowComponent,
158 163
159 ReactiveFileComponent, 164 ReactiveFileComponent,
160 PeertubeCheckboxComponent, 165 PeertubeCheckboxComponent,
@@ -226,6 +231,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop
226 InfiniteScrollerDirective, 231 InfiniteScrollerDirective,
227 TextareaAutoResizeDirective, 232 TextareaAutoResizeDirective,
228 HelpComponent, 233 HelpComponent,
234 ListOverflowComponent,
229 InputReadonlyCopyComponent, 235 InputReadonlyCopyComponent,
230 236
231 ReactiveFileComponent, 237 ReactiveFileComponent,
@@ -250,6 +256,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop
250 NumberFormatterPipe, 256 NumberFormatterPipe,
251 ObjectLengthPipe, 257 ObjectLengthPipe,
252 FromNowPipe, 258 FromNowPipe,
259 HighlightPipe,
253 PeerTubeTemplateDirective, 260 PeerTubeTemplateDirective,
254 VideoDurationPipe 261 VideoDurationPipe
255 ], 262 ],
@@ -300,6 +307,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop
300 UserNotificationService, 307 UserNotificationService,
301 308
302 FollowService, 309 FollowService,
310 RedundancyService,
303 311
304 I18n 312 I18n
305 ] 313 ]
diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.html b/client/src/app/shared/user-subscription/subscribe-button.component.html
index f08c88f3c..85b3d1fdb 100644
--- a/client/src/app/shared/user-subscription/subscribe-button.component.html
+++ b/client/src/app/shared/user-subscription/subscribe-button.component.html
@@ -55,7 +55,7 @@
55 </button> 55 </button>
56 56
57 <button class="dropdown-item dropdown-item-neutral" i18n>Subscribe with a Mastodon account:</button> 57 <button class="dropdown-item dropdown-item-neutral" i18n>Subscribe with a Mastodon account:</button>
58 <my-remote-subscribe showHelp="true" [uri]="uri"></my-remote-subscribe> 58 <my-remote-subscribe [showHelp]="true" [uri]="uri"></my-remote-subscribe>
59 59
60 <div class="dropdown-divider"></div> 60 <div class="dropdown-divider"></div>
61 61
diff --git a/client/src/app/shared/video-abuse/video-abuse.service.ts b/client/src/app/shared/video-abuse/video-abuse.service.ts
index b0b59ea0c..61a328575 100644
--- a/client/src/app/shared/video-abuse/video-abuse.service.ts
+++ b/client/src/app/shared/video-abuse/video-abuse.service.ts
@@ -1,7 +1,7 @@
1import { catchError, map } from 'rxjs/operators' 1import { catchError, map } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http' 2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { SortMeta } from 'primeng/components/common/sortmeta' 4import { SortMeta } from 'primeng/api'
5import { Observable } from 'rxjs' 5import { Observable } from 'rxjs'
6import { ResultList, VideoAbuse, VideoAbuseUpdate } from '../../../../../shared' 6import { ResultList, VideoAbuse, VideoAbuseUpdate } from '../../../../../shared'
7import { environment } from '../../../environments/environment' 7import { environment } from '../../../environments/environment'
diff --git a/client/src/app/shared/video-blacklist/video-blacklist.service.ts b/client/src/app/shared/video-blacklist/video-blacklist.service.ts
index 491fa698b..116177c4a 100644
--- a/client/src/app/shared/video-blacklist/video-blacklist.service.ts
+++ b/client/src/app/shared/video-blacklist/video-blacklist.service.ts
@@ -1,7 +1,7 @@
1import { catchError, map, concatMap, toArray } from 'rxjs/operators' 1import { catchError, map, concatMap, toArray } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http' 2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { SortMeta } from 'primeng/components/common/sortmeta' 4import { SortMeta } from 'primeng/api'
5import { from as observableFrom, Observable } from 'rxjs' 5import { from as observableFrom, Observable } from 'rxjs'
6import { VideoBlacklist, VideoBlacklistType, ResultList } from '../../../../../shared' 6import { VideoBlacklist, VideoBlacklistType, ResultList } from '../../../../../shared'
7import { Video } from '../video/video.model' 7import { Video } from '../video/video.model'
diff --git a/client/src/app/shared/video-import/video-import.service.ts b/client/src/app/shared/video-import/video-import.service.ts
index 3e3fb7dfb..afd9e3fb5 100644
--- a/client/src/app/shared/video-import/video-import.service.ts
+++ b/client/src/app/shared/video-import/video-import.service.ts
@@ -9,7 +9,7 @@ import { VideoImportCreate, VideoUpdate } from '../../../../../shared/models/vid
9import { objectToFormData } from '@app/shared/misc/utils' 9import { objectToFormData } from '@app/shared/misc/utils'
10import { ResultList } from '../../../../../shared/models/result-list.model' 10import { ResultList } from '../../../../../shared/models/result-list.model'
11import { UserService } from '@app/shared/users/user.service' 11import { UserService } from '@app/shared/users/user.service'
12import { SortMeta } from 'primeng/components/common/sortmeta' 12import { SortMeta } from 'primeng/api'
13import { RestPagination } from '@app/shared/rest' 13import { RestPagination } from '@app/shared/rest'
14import { ServerService } from '@app/core' 14import { ServerService } from '@app/core'
15 15
diff --git a/client/src/app/shared/video-ownership/video-ownership.service.ts b/client/src/app/shared/video-ownership/video-ownership.service.ts
index aa9e4839a..b95d5b792 100644
--- a/client/src/app/shared/video-ownership/video-ownership.service.ts
+++ b/client/src/app/shared/video-ownership/video-ownership.service.ts
@@ -5,7 +5,7 @@ import { environment } from '../../../environments/environment'
5import { RestExtractor, RestService } from '../rest' 5import { RestExtractor, RestService } from '../rest'
6import { VideoChangeOwnershipCreate } from '../../../../../shared/models/videos' 6import { VideoChangeOwnershipCreate } from '../../../../../shared/models/videos'
7import { Observable } from 'rxjs/index' 7import { Observable } from 'rxjs/index'
8import { SortMeta } from 'primeng/components/common/sortmeta' 8import { SortMeta } from 'primeng/api'
9import { ResultList, VideoChangeOwnership } from '../../../../../shared' 9import { ResultList, VideoChangeOwnership } from '../../../../../shared'
10import { RestPagination } from '@app/shared/rest' 10import { RestPagination } from '@app/shared/rest'
11import { VideoChangeOwnershipAccept } from '../../../../../shared/models/videos/video-change-ownership-accept.model' 11import { VideoChangeOwnershipAccept } from '../../../../../shared/models/videos/video-change-ownership-accept.model'
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.html b/client/src/app/shared/video-playlist/video-add-to-playlist.component.html
index 6e0989227..58108584b 100644
--- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.html
+++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.html
@@ -62,7 +62,7 @@
62 <div class="new-playlist-button dropdown-item" (click)="openCreateBlock($event)" [hidden]="isNewPlaylistBlockOpened"> 62 <div class="new-playlist-button dropdown-item" (click)="openCreateBlock($event)" [hidden]="isNewPlaylistBlockOpened">
63 <my-global-icon iconName="add"></my-global-icon> 63 <my-global-icon iconName="add"></my-global-icon>
64 64
65 Create a private playlist 65 <span i18n>Create a private playlist</span>
66 </div> 66 </div>
67 67
68 <form class="new-playlist-block dropdown-item" *ngIf="isNewPlaylistBlockOpened" (ngSubmit)="createPlaylist()" [formGroup]="form"> 68 <form class="new-playlist-block dropdown-item" *ngIf="isNewPlaylistBlockOpened" (ngSubmit)="createPlaylist()" [formGroup]="form">
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts
index 4864581b5..a2c0724cd 100644
--- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts
+++ b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts
@@ -18,7 +18,7 @@ import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-
18 changeDetection: ChangeDetectionStrategy.OnPush 18 changeDetection: ChangeDetectionStrategy.OnPush
19}) 19})
20export class VideoPlaylistElementMiniatureComponent implements OnInit { 20export class VideoPlaylistElementMiniatureComponent implements OnInit {
21 @ViewChild('moreDropdown', { static: false }) moreDropdown: NgbDropdown 21 @ViewChild('moreDropdown') moreDropdown: NgbDropdown
22 22
23 @Input() playlist: VideoPlaylist 23 @Input() playlist: VideoPlaylist
24 @Input() playlistElement: VideoPlaylistElement 24 @Input() playlistElement: VideoPlaylistElement
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
index c2fe6f754..2f5f82aa3 100644
--- a/client/src/app/shared/video/abstract-video-list.ts
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -14,6 +14,7 @@ import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
14import { I18n } from '@ngx-translate/i18n-polyfill' 14import { I18n } from '@ngx-translate/i18n-polyfill'
15import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date' 15import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date'
16import { ServerConfig } from '@shared/models' 16import { ServerConfig } from '@shared/models'
17import { GlobalIconName } from '@app/shared/images/global-icon.component'
17 18
18enum GroupDate { 19enum GroupDate {
19 UNKNOWN = 0, 20 UNKNOWN = 0,
@@ -61,7 +62,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
61 62
62 actions: { 63 actions: {
63 routerLink: string 64 routerLink: string
64 iconName: string 65 iconName: GlobalIconName
65 label: string 66 label: string
66 }[] = [] 67 }[] = []
67 68
diff --git a/client/src/app/shared/video/infinite-scroller.directive.ts b/client/src/app/shared/video/infinite-scroller.directive.ts
index 9f613c5fa..f09c3d1fc 100644
--- a/client/src/app/shared/video/infinite-scroller.directive.ts
+++ b/client/src/app/shared/video/infinite-scroller.directive.ts
@@ -1,4 +1,4 @@
1import { distinctUntilChanged, filter, map, share, startWith, tap, throttleTime } from 'rxjs/operators' 1import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators'
2import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' 2import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'
3import { fromEvent, Observable, Subscription } from 'rxjs' 3import { fromEvent, Observable, Subscription } from 'rxjs'
4 4
@@ -53,7 +53,7 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterConten
53 const scrollableElement = this.onItself ? this.container : window 53 const scrollableElement = this.onItself ? this.container : window
54 const scrollObservable = fromEvent(scrollableElement, 'scroll') 54 const scrollObservable = fromEvent(scrollableElement, 'scroll')
55 .pipe( 55 .pipe(
56 startWith(null as string), // FIXME: typings 56 startWith(true),
57 throttleTime(200, undefined, throttleOptions), 57 throttleTime(200, undefined, throttleOptions),
58 map(() => this.getScrollInfo()), 58 map(() => this.getScrollInfo()),
59 distinctUntilChanged((o1, o2) => o1.current === o2.current), 59 distinctUntilChanged((o1, o2) => o1.current === o2.current),
diff --git a/client/src/app/shared/video/modals/video-blacklist.component.ts b/client/src/app/shared/video/modals/video-blacklist.component.ts
index f0c70a365..6ef9c250b 100644
--- a/client/src/app/shared/video/modals/video-blacklist.component.ts
+++ b/client/src/app/shared/video/modals/video-blacklist.component.ts
@@ -1,12 +1,12 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier, RedirectService } from '@app/core' 2import { Notifier, RedirectService } from '@app/core'
3import { VideoBlacklistService } from '../../../shared/video-blacklist' 3import { VideoBlacklistService } from '../../../shared/video-blacklist'
4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 5import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { FormReactive, VideoBlacklistValidatorsService } from '@app/shared/forms' 8import { FormReactive, VideoBlacklistValidatorsService } from '@app/shared/forms'
9import { Video } from '@app/shared/video/video.model'
10 10
11@Component({ 11@Component({
12 selector: 'my-video-blacklist', 12 selector: 'my-video-blacklist',
@@ -14,7 +14,7 @@ import { FormReactive, VideoBlacklistValidatorsService } from '@app/shared/forms
14 styleUrls: [ './video-blacklist.component.scss' ] 14 styleUrls: [ './video-blacklist.component.scss' ]
15}) 15})
16export class VideoBlacklistComponent extends FormReactive implements OnInit { 16export class VideoBlacklistComponent extends FormReactive implements OnInit {
17 @Input() video: VideoDetails = null 17 @Input() video: Video = null
18 18
19 @ViewChild('modal', { static: true }) modal: NgbModal 19 @ViewChild('modal', { static: true }) modal: NgbModal
20 20
@@ -46,7 +46,7 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
46 } 46 }
47 47
48 show () { 48 show () {
49 this.openedModal = this.modalService.open(this.modal, { keyboard: false }) 49 this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
50 } 50 }
51 51
52 hide () { 52 hide () {
diff --git a/client/src/app/shared/video/modals/video-download.component.html b/client/src/app/shared/video/modals/video-download.component.html
index 8cca985b1..976da03f3 100644
--- a/client/src/app/shared/video/modals/video-download.component.html
+++ b/client/src/app/shared/video/modals/video-download.component.html
@@ -23,13 +23,15 @@
23 <select *ngIf="type === 'video'" [(ngModel)]="resolutionId"> 23 <select *ngIf="type === 'video'" [(ngModel)]="resolutionId">
24 <option *ngFor="let file of getVideoFiles()" [value]="file.resolution.id">{{ file.resolution.label }}</option> 24 <option *ngFor="let file of getVideoFiles()" [value]="file.resolution.id">{{ file.resolution.label }}</option>
25 </select> 25 </select>
26
26 <select *ngIf="type === 'subtitles'" [(ngModel)]="subtitleLanguageId"> 27 <select *ngIf="type === 'subtitles'" [(ngModel)]="subtitleLanguageId">
27 <option *ngFor="let caption of videoCaptions" [value]="caption.language.id">{{ caption.language.label }}</option> 28 <option *ngFor="let caption of videoCaptions" [value]="caption.language.id">{{ caption.language.label }}</option>
28 </select> 29 </select>
29 </div> 30 </div>
31
30 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> 32 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
31 <div class="input-group-append"> 33 <div class="input-group-append">
32 <button [ngxClipboard]="urlInput" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> 34 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
33 <span class="glyphicon glyphicon-copy"></span> 35 <span class="glyphicon glyphicon-copy"></span>
34 </button> 36 </button>
35 </div> 37 </div>
diff --git a/client/src/app/shared/video/modals/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts
index c1ceca263..6909c4279 100644
--- a/client/src/app/shared/video/modals/video-download.component.ts
+++ b/client/src/app/shared/video/modals/video-download.component.ts
@@ -48,7 +48,7 @@ export class VideoDownloadComponent {
48 this.video = video 48 this.video = video
49 this.videoCaptions = videoCaptions && videoCaptions.length ? videoCaptions : undefined 49 this.videoCaptions = videoCaptions && videoCaptions.length ? videoCaptions : undefined
50 50
51 this.activeModal = this.modalService.open(this.modal) 51 this.activeModal = this.modalService.open(this.modal, { centered: true })
52 52
53 this.resolutionId = this.getVideoFiles()[0].resolution.id 53 this.resolutionId = this.getVideoFiles()[0].resolution.id
54 if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id 54 if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id
diff --git a/client/src/app/shared/video/modals/video-report.component.ts b/client/src/app/shared/video/modals/video-report.component.ts
index 1d368ff17..988fa03d4 100644
--- a/client/src/app/shared/video/modals/video-report.component.ts
+++ b/client/src/app/shared/video/modals/video-report.component.ts
@@ -1,13 +1,13 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core' 1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { FormReactive } from '../../../shared/forms' 3import { FormReactive } from '../../../shared/forms'
4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 5import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service' 6import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service'
8import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
9import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
10import { VideoAbuseService } from '@app/shared/video-abuse' 9import { VideoAbuseService } from '@app/shared/video-abuse'
10import { Video } from '@app/shared/video/video.model'
11 11
12@Component({ 12@Component({
13 selector: 'my-video-report', 13 selector: 'my-video-report',
@@ -15,7 +15,7 @@ import { VideoAbuseService } from '@app/shared/video-abuse'
15 styleUrls: [ './video-report.component.scss' ] 15 styleUrls: [ './video-report.component.scss' ]
16}) 16})
17export class VideoReportComponent extends FormReactive implements OnInit { 17export class VideoReportComponent extends FormReactive implements OnInit {
18 @Input() video: VideoDetails = null 18 @Input() video: Video = null
19 19
20 @ViewChild('modal', { static: true }) modal: NgbModal 20 @ViewChild('modal', { static: true }) modal: NgbModal
21 21
@@ -53,7 +53,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
53 } 53 }
54 54
55 show () { 55 show () {
56 this.openedModal = this.modalService.open(this.modal, { keyboard: false }) 56 this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
57 } 57 }
58 58
59 hide () { 59 hide () {
diff --git a/client/src/app/shared/video/redundancy.service.ts b/client/src/app/shared/video/redundancy.service.ts
new file mode 100644
index 000000000..fb918d73b
--- /dev/null
+++ b/client/src/app/shared/video/redundancy.service.ts
@@ -0,0 +1,73 @@
1import { catchError, map, toArray } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { RestExtractor, RestPagination, RestService } from '@app/shared/rest'
5import { SortMeta } from 'primeng/api'
6import { ResultList, Video, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
7import { concat, Observable } from 'rxjs'
8import { environment } from '../../../environments/environment'
9
10@Injectable()
11export class RedundancyService {
12 static BASE_REDUNDANCY_URL = environment.apiUrl + '/api/v1/server/redundancy'
13
14 constructor (
15 private authHttp: HttpClient,
16 private restService: RestService,
17 private restExtractor: RestExtractor
18 ) { }
19
20 updateRedundancy (host: string, redundancyAllowed: boolean) {
21 const url = RedundancyService.BASE_REDUNDANCY_URL + '/' + host
22
23 const body = { redundancyAllowed }
24
25 return this.authHttp.put(url, body)
26 .pipe(
27 map(this.restExtractor.extractDataBool),
28 catchError(err => this.restExtractor.handleError(err))
29 )
30 }
31
32 listVideoRedundancies (options: {
33 pagination: RestPagination,
34 sort: SortMeta,
35 target?: VideoRedundanciesTarget
36 }): Observable<ResultList<VideoRedundancy>> {
37 const { pagination, sort, target } = options
38
39 let params = new HttpParams()
40 params = this.restService.addRestGetParams(params, pagination, sort)
41
42 if (target) params = params.append('target', target)
43
44 return this.authHttp.get<ResultList<VideoRedundancy>>(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { params })
45 .pipe(
46 catchError(res => this.restExtractor.handleError(res))
47 )
48 }
49
50 addVideoRedundancy (video: Video) {
51 return this.authHttp.post(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { videoId: video.id })
52 .pipe(
53 catchError(res => this.restExtractor.handleError(res))
54 )
55 }
56
57 removeVideoRedundancies (redundancy: VideoRedundancy) {
58 const observables = redundancy.redundancies.streamingPlaylists.map(r => r.id)
59 .concat(redundancy.redundancies.files.map(r => r.id))
60 .map(id => this.removeRedundancy(id))
61
62 return concat(...observables)
63 .pipe(toArray())
64 }
65
66 private removeRedundancy (redundancyId: number) {
67 return this.authHttp.delete(RedundancyService.BASE_REDUNDANCY_URL + '/videos/' + redundancyId)
68 .pipe(
69 map(this.restExtractor.extractDataBool),
70 catchError(res => this.restExtractor.handleError(res))
71 )
72 }
73}
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.ts b/client/src/app/shared/video/video-actions-dropdown.component.ts
index afdeab18d..69f45346e 100644
--- a/client/src/app/shared/video/video-actions-dropdown.component.ts
+++ b/client/src/app/shared/video/video-actions-dropdown.component.ts
@@ -14,6 +14,7 @@ import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklis
14import { VideoBlacklistService } from '@app/shared/video-blacklist' 14import { VideoBlacklistService } from '@app/shared/video-blacklist'
15import { ScreenService } from '@app/shared/misc/screen.service' 15import { ScreenService } from '@app/shared/misc/screen.service'
16import { VideoCaption } from '@shared/models' 16import { VideoCaption } from '@shared/models'
17import { RedundancyService } from '@app/shared/video/redundancy.service'
17 18
18export type VideoActionsDisplayType = { 19export type VideoActionsDisplayType = {
19 playlist?: boolean 20 playlist?: boolean
@@ -22,6 +23,7 @@ export type VideoActionsDisplayType = {
22 blacklist?: boolean 23 blacklist?: boolean
23 delete?: boolean 24 delete?: boolean
24 report?: boolean 25 report?: boolean
26 duplicate?: boolean
25} 27}
26 28
27@Component({ 29@Component({
@@ -30,12 +32,12 @@ export type VideoActionsDisplayType = {
30 styleUrls: [ './video-actions-dropdown.component.scss' ] 32 styleUrls: [ './video-actions-dropdown.component.scss' ]
31}) 33})
32export class VideoActionsDropdownComponent implements OnChanges { 34export class VideoActionsDropdownComponent implements OnChanges {
33 @ViewChild('playlistDropdown', { static: false }) playlistDropdown: NgbDropdown 35 @ViewChild('playlistDropdown') playlistDropdown: NgbDropdown
34 @ViewChild('playlistAdd', { static: false }) playlistAdd: VideoAddToPlaylistComponent 36 @ViewChild('playlistAdd') playlistAdd: VideoAddToPlaylistComponent
35 37
36 @ViewChild('videoDownloadModal', { static: false }) videoDownloadModal: VideoDownloadComponent 38 @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent
37 @ViewChild('videoReportModal', { static: false }) videoReportModal: VideoReportComponent 39 @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
38 @ViewChild('videoBlacklistModal', { static: false }) videoBlacklistModal: VideoBlacklistComponent 40 @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
39 41
40 @Input() video: Video | VideoDetails 42 @Input() video: Video | VideoDetails
41 @Input() videoCaptions: VideoCaption[] = [] 43 @Input() videoCaptions: VideoCaption[] = []
@@ -46,7 +48,8 @@ export class VideoActionsDropdownComponent implements OnChanges {
46 update: true, 48 update: true,
47 blacklist: true, 49 blacklist: true,
48 delete: true, 50 delete: true,
49 report: true 51 report: true,
52 duplicate: true
50 } 53 }
51 @Input() placement = 'left' 54 @Input() placement = 'left'
52 55
@@ -74,6 +77,7 @@ export class VideoActionsDropdownComponent implements OnChanges {
74 private screenService: ScreenService, 77 private screenService: ScreenService,
75 private videoService: VideoService, 78 private videoService: VideoService,
76 private blocklistService: BlocklistService, 79 private blocklistService: BlocklistService,
80 private redundancyService: RedundancyService,
77 private i18n: I18n 81 private i18n: I18n
78 ) { } 82 ) { }
79 83
@@ -144,6 +148,10 @@ export class VideoActionsDropdownComponent implements OnChanges {
144 return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled 148 return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled
145 } 149 }
146 150
151 canVideoBeDuplicated () {
152 return this.video.canBeDuplicatedBy(this.user)
153 }
154
147 /* Action handlers */ 155 /* Action handlers */
148 156
149 async unblacklistVideo () { 157 async unblacklistVideo () {
@@ -186,6 +194,18 @@ export class VideoActionsDropdownComponent implements OnChanges {
186 ) 194 )
187 } 195 }
188 196
197 duplicateVideo () {
198 this.redundancyService.addVideoRedundancy(this.video)
199 .subscribe(
200 () => {
201 const message = this.i18n('This video will be duplicated by your instance.')
202 this.notifier.success(message)
203 },
204
205 err => this.notifier.error(err.message)
206 )
207 }
208
189 onVideoBlacklisted () { 209 onVideoBlacklisted () {
190 this.videoBlacklisted.emit() 210 this.videoBlacklisted.emit()
191 } 211 }
@@ -234,6 +254,12 @@ export class VideoActionsDropdownComponent implements OnChanges {
234 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoUnblacklistable() 254 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoUnblacklistable()
235 }, 255 },
236 { 256 {
257 label: this.i18n('Duplicate (redundancy)'),
258 handler: () => this.duplicateVideo(),
259 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.duplicate && this.canVideoBeDuplicated(),
260 iconName: 'cloud-download'
261 },
262 {
237 label: this.i18n('Delete'), 263 label: this.i18n('Delete'),
238 handler: () => this.removeVideo(), 264 handler: () => this.removeVideo(),
239 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.delete && this.isVideoRemovable(), 265 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.delete && this.isVideoRemovable(),
diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html
index 46c49c15b..819be6d48 100644
--- a/client/src/app/shared/video/video-miniature.component.html
+++ b/client/src/app/shared/video/video-miniature.component.html
@@ -50,7 +50,7 @@
50 </div> 50 </div>
51 51
52 <div class="video-actions"> 52 <div class="video-actions">
53 <!-- FIXME: remove bottom placement when overflow is fixed in bootstrap dropdown --> 53 <!-- FIXME: remove bottom placement when overflow is fixed in bootstrap dropdown: https://github.com/ng-bootstrap/ng-bootstrap/issues/3495 -->
54 <my-video-actions-dropdown 54 <my-video-actions-dropdown
55 *ngIf="showActions" [video]="video" [displayOptions]="videoActionsDisplayOptions" placement="bottom-left bottom-right left" 55 *ngIf="showActions" [video]="video" [displayOptions]="videoActionsDisplayOptions" placement="bottom-left bottom-right left"
56 (videoRemoved)="onVideoRemoved()" (videoBlacklisted)="onVideoBlacklisted()" (videoUnblacklisted)="onVideoUnblacklisted()" 56 (videoRemoved)="onVideoRemoved()" (videoBlacklisted)="onVideoBlacklisted()" (videoUnblacklisted)="onVideoUnblacklisted()"
diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts
index 598a7a983..9d22e13fd 100644
--- a/client/src/app/shared/video/video-miniature.component.ts
+++ b/client/src/app/shared/video/video-miniature.component.ts
@@ -64,7 +64,8 @@ export class VideoMiniatureComponent implements OnInit {
64 update: true, 64 update: true,
65 blacklist: true, 65 blacklist: true,
66 delete: true, 66 delete: true,
67 report: true 67 report: true,
68 duplicate: false
68 } 69 }
69 showActions = false 70 showActions = false
70 serverConfig: ServerConfig 71 serverConfig: ServerConfig
@@ -199,7 +200,7 @@ export class VideoMiniatureComponent implements OnInit {
199 } 200 }
200 201
201 isWatchLaterPlaylistDisplayed () { 202 isWatchLaterPlaylistDisplayed () {
202 return this.inWatchLaterPlaylist !== undefined 203 return this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined
203 } 204 }
204 205
205 private setUpBy () { 206 private setUpBy () {
diff --git a/client/src/app/shared/video/video-thumbnail.component.scss b/client/src/app/shared/video/video-thumbnail.component.scss
index 573a64987..c13105e94 100644
--- a/client/src/app/shared/video/video-thumbnail.component.scss
+++ b/client/src/app/shared/video/video-thumbnail.component.scss
@@ -25,7 +25,7 @@
25 border-radius: 3px; 25 border-radius: 3px;
26 font-size: 12px; 26 font-size: 12px;
27 font-weight: $font-bold; 27 font-weight: $font-bold;
28 z-index: 1; 28 z-index: z(miniature);
29 } 29 }
30 30
31 .video-thumbnail-duration-overlay { 31 .video-thumbnail-duration-overlay {
diff --git a/client/src/app/shared/video/video-thumbnail.component.ts b/client/src/app/shared/video/video-thumbnail.component.ts
index 2420ec715..111b4c8bb 100644
--- a/client/src/app/shared/video/video-thumbnail.component.ts
+++ b/client/src/app/shared/video/video-thumbnail.component.ts
@@ -12,7 +12,7 @@ export class VideoThumbnailComponent {
12 @Input() video: Video 12 @Input() video: Video
13 @Input() nsfw = false 13 @Input() nsfw = false
14 @Input() routerLink: any[] 14 @Input() routerLink: any[]
15 @Input() queryParams: any[] 15 @Input() queryParams: { [ p: string ]: any }
16 16
17 @Input() displayWatchLaterPlaylist: boolean 17 @Input() displayWatchLaterPlaylist: boolean
18 @Input() inWatchLaterPlaylist: boolean 18 @Input() inWatchLaterPlaylist: boolean
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index fb98d5382..546518cca 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -42,6 +42,9 @@ export class Video implements VideoServerModel {
42 dislikes: number 42 dislikes: number
43 nsfw: boolean 43 nsfw: boolean
44 44
45 originInstanceUrl: string
46 originInstanceHost: string
47
45 waitTranscoding?: boolean 48 waitTranscoding?: boolean
46 state?: VideoConstant<VideoState> 49 state?: VideoConstant<VideoState>
47 scheduledUpdate?: VideoScheduleUpdate 50 scheduledUpdate?: VideoScheduleUpdate
@@ -86,22 +89,31 @@ export class Video implements VideoServerModel {
86 this.waitTranscoding = hash.waitTranscoding 89 this.waitTranscoding = hash.waitTranscoding
87 this.state = hash.state 90 this.state = hash.state
88 this.description = hash.description 91 this.description = hash.description
92
89 this.duration = hash.duration 93 this.duration = hash.duration
90 this.durationLabel = durationToString(hash.duration) 94 this.durationLabel = durationToString(hash.duration)
95
91 this.id = hash.id 96 this.id = hash.id
92 this.uuid = hash.uuid 97 this.uuid = hash.uuid
98
93 this.isLocal = hash.isLocal 99 this.isLocal = hash.isLocal
94 this.name = hash.name 100 this.name = hash.name
101
95 this.thumbnailPath = hash.thumbnailPath 102 this.thumbnailPath = hash.thumbnailPath
96 this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath 103 this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath
104
97 this.previewPath = hash.previewPath 105 this.previewPath = hash.previewPath
98 this.previewUrl = absoluteAPIUrl + hash.previewPath 106 this.previewUrl = absoluteAPIUrl + hash.previewPath
107
99 this.embedPath = hash.embedPath 108 this.embedPath = hash.embedPath
100 this.embedUrl = absoluteAPIUrl + hash.embedPath 109 this.embedUrl = absoluteAPIUrl + hash.embedPath
110
101 this.views = hash.views 111 this.views = hash.views
102 this.likes = hash.likes 112 this.likes = hash.likes
103 this.dislikes = hash.dislikes 113 this.dislikes = hash.dislikes
114
104 this.nsfw = hash.nsfw 115 this.nsfw = hash.nsfw
116
105 this.account = hash.account 117 this.account = hash.account
106 this.channel = hash.channel 118 this.channel = hash.channel
107 119
@@ -124,6 +136,9 @@ export class Video implements VideoServerModel {
124 this.blacklistedReason = hash.blacklistedReason 136 this.blacklistedReason = hash.blacklistedReason
125 137
126 this.userHistory = hash.userHistory 138 this.userHistory = hash.userHistory
139
140 this.originInstanceHost = this.account.host
141 this.originInstanceUrl = 'https://' + this.originInstanceHost
127 } 142 }
128 143
129 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { 144 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
@@ -152,4 +167,8 @@ export class Video implements VideoServerModel {
152 isUpdatableBy (user: AuthUser) { 167 isUpdatableBy (user: AuthUser) {
153 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) 168 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO))
154 } 169 }
170
171 canBeDuplicatedBy (user: AuthUser) {
172 return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES)
173 }
155} 174}