diff options
Diffstat (limited to 'client/src/app/shared')
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 @@ | |||
1 | import { PipeTransform, Pipe } from '@angular/core' | ||
2 | import { SafeHtml } from '@angular/platform-browser' | ||
3 | |||
4 | // Thanks https://gist.github.com/adamrecsko/0f28f474eca63e0279455476cc11eca7#gistcomment-2917369 | ||
5 | @Pipe({ name: 'highlight' }) | ||
6 | export 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 | ||
9 | export class EditButtonComponent { | 9 | export 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 | ||
22 | export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | 22 | export 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 @@ | |||
1 | import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core' | 1 | import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core' |
2 | import { HooksService } from '@app/core/plugins/hooks.service' | 2 | import { HooksService } from '@app/core/plugins/hooks.service' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | 3 | ||
5 | const icons = { | 4 | const 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 @@ | |||
1 | import { map } from 'rxjs/operators' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
5 | import { ServerStats } from '@shared/models/server' | 2 | import { ServerStats } from '@shared/models/server' |
6 | import { environment } from '../../../environments/environment' | 3 | import { 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 | }) |
13 | export class InstanceStatisticsComponent implements OnInit { | 10 | export 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 | |||
16 | button { | ||
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 @@ | |||
1 | import { | ||
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' | ||
14 | import { NgbDropdown, NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
15 | import { lowerFirst, uniqueId } from 'lodash-es' | ||
16 | import { ScreenService } from './screen.service' | ||
17 | import { take } from 'rxjs/operators' | ||
18 | |||
19 | export 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 | }) | ||
30 | export 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 | }) |
16 | export class UserModerationDropdownComponent implements OnInit, OnChanges { | 16 | export 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 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { MarkdownIt } from 'markdown-it' | ||
3 | import { buildVideoLink } from '../../../assets/player/utils' | 2 | import { buildVideoLink } from '../../../assets/player/utils' |
4 | import { HtmlRendererService } from '@app/shared/renderer/html-renderer.service' | 3 | import { HtmlRendererService } from '@app/shared/renderer/html-renderer.service' |
4 | import * as MarkdownIt from 'markdown-it' | ||
5 | 5 | ||
6 | type MarkdownParsers = { | 6 | type 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 @@ | |||
1 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' | 1 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' |
2 | import { LazyLoadEvent } from 'primeng/components/common/lazyloadevent' | 2 | import { LazyLoadEvent, SortMeta } from 'primeng/api' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | ||
4 | import { RestPagination } from './rest-pagination' | 3 | import { RestPagination } from './rest-pagination' |
5 | import { Subject } from 'rxjs' | 4 | import { Subject } from 'rxjs' |
6 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 5 | import { 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 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { HttpParams } from '@angular/common/http' | 2 | import { HttpParams } from '@angular/common/http' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { SortMeta } from 'primeng/api' |
4 | import { ComponentPagination, ComponentPaginationLight } from './component-pagination.model' | 4 | import { ComponentPagination, ComponentPaginationLight } from './component-pagination.model' |
5 | 5 | ||
6 | import { RestPagination } from './rest-pagination' | 6 | import { 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' | |||
5 | import { RouterModule } from '@angular/router' | 5 | import { RouterModule } from '@angular/router' |
6 | import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' | 6 | import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' |
7 | import { HelpComponent } from '@app/shared/misc/help.component' | 7 | import { HelpComponent } from '@app/shared/misc/help.component' |
8 | import { ListOverflowComponent } from '@app/shared/misc/list-overflow.component' | ||
8 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' | 9 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' |
9 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' | 10 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' |
10 | import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' | 11 | import { SharedModule as PrimeSharedModule } from 'primeng/api' |
11 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' | 12 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' |
12 | import { ButtonComponent } from './buttons/button.component' | 13 | import { ButtonComponent } from './buttons/button.component' |
13 | import { DeleteButtonComponent } from './buttons/delete-button.component' | 14 | import { DeleteButtonComponent } from './buttons/delete-button.component' |
@@ -88,16 +89,18 @@ import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe' | |||
88 | import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe' | 89 | import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe' |
89 | import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe' | 90 | import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe' |
90 | import { FromNowPipe } from '@app/shared/angular/from-now.pipe' | 91 | import { FromNowPipe } from '@app/shared/angular/from-now.pipe' |
92 | import { HighlightPipe } from '@app/shared/angular/highlight.pipe' | ||
91 | import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' | 93 | import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' |
92 | import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component' | 94 | import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component' |
93 | import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component' | 95 | import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component' |
94 | import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' | 96 | import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' |
95 | import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' | 97 | import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' |
96 | import { ClipboardModule } from 'ngx-clipboard' | ||
97 | import { FollowService } from '@app/shared/instance/follow.service' | 98 | import { FollowService } from '@app/shared/instance/follow.service' |
98 | import { MultiSelectModule } from 'primeng/multiselect' | 99 | import { MultiSelectModule } from 'primeng/multiselect' |
99 | import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' | 100 | import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' |
100 | import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' | 101 | import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' |
102 | import { RedundancyService } from '@app/shared/video/redundancy.service' | ||
103 | import { 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 @@ | |||
1 | import { catchError, map } from 'rxjs/operators' | 1 | import { catchError, map } from 'rxjs/operators' |
2 | import { HttpClient, HttpParams } from '@angular/common/http' | 2 | import { HttpClient, HttpParams } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { SortMeta } from 'primeng/components/common/sortmeta' | 4 | import { SortMeta } from 'primeng/api' |
5 | import { Observable } from 'rxjs' | 5 | import { Observable } from 'rxjs' |
6 | import { ResultList, VideoAbuse, VideoAbuseUpdate } from '../../../../../shared' | 6 | import { ResultList, VideoAbuse, VideoAbuseUpdate } from '../../../../../shared' |
7 | import { environment } from '../../../environments/environment' | 7 | import { 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 @@ | |||
1 | import { catchError, map, concatMap, toArray } from 'rxjs/operators' | 1 | import { catchError, map, concatMap, toArray } from 'rxjs/operators' |
2 | import { HttpClient, HttpParams } from '@angular/common/http' | 2 | import { HttpClient, HttpParams } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { SortMeta } from 'primeng/components/common/sortmeta' | 4 | import { SortMeta } from 'primeng/api' |
5 | import { from as observableFrom, Observable } from 'rxjs' | 5 | import { from as observableFrom, Observable } from 'rxjs' |
6 | import { VideoBlacklist, VideoBlacklistType, ResultList } from '../../../../../shared' | 6 | import { VideoBlacklist, VideoBlacklistType, ResultList } from '../../../../../shared' |
7 | import { Video } from '../video/video.model' | 7 | import { 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 | |||
9 | import { objectToFormData } from '@app/shared/misc/utils' | 9 | import { objectToFormData } from '@app/shared/misc/utils' |
10 | import { ResultList } from '../../../../../shared/models/result-list.model' | 10 | import { ResultList } from '../../../../../shared/models/result-list.model' |
11 | import { UserService } from '@app/shared/users/user.service' | 11 | import { UserService } from '@app/shared/users/user.service' |
12 | import { SortMeta } from 'primeng/components/common/sortmeta' | 12 | import { SortMeta } from 'primeng/api' |
13 | import { RestPagination } from '@app/shared/rest' | 13 | import { RestPagination } from '@app/shared/rest' |
14 | import { ServerService } from '@app/core' | 14 | import { 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' | |||
5 | import { RestExtractor, RestService } from '../rest' | 5 | import { RestExtractor, RestService } from '../rest' |
6 | import { VideoChangeOwnershipCreate } from '../../../../../shared/models/videos' | 6 | import { VideoChangeOwnershipCreate } from '../../../../../shared/models/videos' |
7 | import { Observable } from 'rxjs/index' | 7 | import { Observable } from 'rxjs/index' |
8 | import { SortMeta } from 'primeng/components/common/sortmeta' | 8 | import { SortMeta } from 'primeng/api' |
9 | import { ResultList, VideoChangeOwnership } from '../../../../../shared' | 9 | import { ResultList, VideoChangeOwnership } from '../../../../../shared' |
10 | import { RestPagination } from '@app/shared/rest' | 10 | import { RestPagination } from '@app/shared/rest' |
11 | import { VideoChangeOwnershipAccept } from '../../../../../shared/models/videos/video-change-ownership-accept.model' | 11 | import { 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 | }) |
20 | export class VideoPlaylistElementMiniatureComponent implements OnInit { | 20 | export 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' | |||
14 | import { I18n } from '@ngx-translate/i18n-polyfill' | 14 | import { I18n } from '@ngx-translate/i18n-polyfill' |
15 | import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date' | 15 | import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date' |
16 | import { ServerConfig } from '@shared/models' | 16 | import { ServerConfig } from '@shared/models' |
17 | import { GlobalIconName } from '@app/shared/images/global-icon.component' | ||
17 | 18 | ||
18 | enum GroupDate { | 19 | enum 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 @@ | |||
1 | import { distinctUntilChanged, filter, map, share, startWith, tap, throttleTime } from 'rxjs/operators' | 1 | import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators' |
2 | import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' | 2 | import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' |
3 | import { fromEvent, Observable, Subscription } from 'rxjs' | 3 | import { 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 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Notifier, RedirectService } from '@app/core' | 2 | import { Notifier, RedirectService } from '@app/core' |
3 | import { VideoBlacklistService } from '../../../shared/video-blacklist' | 3 | import { VideoBlacklistService } from '../../../shared/video-blacklist' |
4 | import { VideoDetails } from '../../../shared/video/video-details.model' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 5 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
9 | import { FormReactive, VideoBlacklistValidatorsService } from '@app/shared/forms' | 8 | import { FormReactive, VideoBlacklistValidatorsService } from '@app/shared/forms' |
9 | import { 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 | }) |
16 | export class VideoBlacklistComponent extends FormReactive implements OnInit { | 16 | export 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 @@ | |||
1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { FormReactive } from '../../../shared/forms' | 3 | import { FormReactive } from '../../../shared/forms' |
4 | import { VideoDetails } from '../../../shared/video/video-details.model' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 5 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
7 | import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service' | 6 | import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service' |
8 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
9 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
10 | import { VideoAbuseService } from '@app/shared/video-abuse' | 9 | import { VideoAbuseService } from '@app/shared/video-abuse' |
10 | import { 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 | }) |
17 | export class VideoReportComponent extends FormReactive implements OnInit { | 17 | export 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 @@ | |||
1 | import { catchError, map, toArray } from 'rxjs/operators' | ||
2 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { RestExtractor, RestPagination, RestService } from '@app/shared/rest' | ||
5 | import { SortMeta } from 'primeng/api' | ||
6 | import { ResultList, Video, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' | ||
7 | import { concat, Observable } from 'rxjs' | ||
8 | import { environment } from '../../../environments/environment' | ||
9 | |||
10 | @Injectable() | ||
11 | export 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 | |||
14 | import { VideoBlacklistService } from '@app/shared/video-blacklist' | 14 | import { VideoBlacklistService } from '@app/shared/video-blacklist' |
15 | import { ScreenService } from '@app/shared/misc/screen.service' | 15 | import { ScreenService } from '@app/shared/misc/screen.service' |
16 | import { VideoCaption } from '@shared/models' | 16 | import { VideoCaption } from '@shared/models' |
17 | import { RedundancyService } from '@app/shared/video/redundancy.service' | ||
17 | 18 | ||
18 | export type VideoActionsDisplayType = { | 19 | export 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 | }) |
32 | export class VideoActionsDropdownComponent implements OnChanges { | 34 | export 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 | } |