diff options
Diffstat (limited to 'client/src/app')
69 files changed, 869 insertions, 532 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html index bbf946df0..9701e7f85 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html | |||
@@ -52,6 +52,20 @@ | |||
52 | 52 | ||
53 | <div *ngIf="formErrors.cache.torrents.size" class="form-error">{{ formErrors.cache.torrents.size }}</div> | 53 | <div *ngIf="formErrors.cache.torrents.size" class="form-error">{{ formErrors.cache.torrents.size }}</div> |
54 | </div> | 54 | </div> |
55 | |||
56 | <div class="form-group" formGroupName="torrents"> | ||
57 | <label i18n for="cacheTorrentsSize">Number of video storyboard images to keep in cache</label> | ||
58 | |||
59 | <div class="number-with-unit"> | ||
60 | <input | ||
61 | type="number" min="0" id="cacheStoryboardsSize" class="form-control" | ||
62 | formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.storyboards.size'] }" | ||
63 | > | ||
64 | <span i18n>{getCacheSize('storyboards'), plural, =1 {cached storyboard} other {cached storyboards}}</span> | ||
65 | </div> | ||
66 | |||
67 | <div *ngIf="formErrors.cache.storyboards.size" class="form-error">{{ formErrors.cache.storyboards.size }}</div> | ||
68 | </div> | ||
55 | </ng-container> | 69 | </ng-container> |
56 | 70 | ||
57 | </div> | 71 | </div> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts index 79a98f288..06c5e6221 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts | |||
@@ -10,7 +10,7 @@ export class EditAdvancedConfigurationComponent { | |||
10 | @Input() form: FormGroup | 10 | @Input() form: FormGroup |
11 | @Input() formErrors: any | 11 | @Input() formErrors: any |
12 | 12 | ||
13 | getCacheSize (type: 'captions' | 'previews' | 'torrents') { | 13 | getCacheSize (type: 'captions' | 'previews' | 'torrents' | 'storyboards') { |
14 | return this.form.value['cache'][type]['size'] | 14 | return this.form.value['cache'][type]['size'] |
15 | } | 15 | } |
16 | } | 16 | } |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts b/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts index 628c2d102..42c0e6dc2 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { FormGroup } from '@angular/forms' | 2 | import { FormGroup } from '@angular/forms' |
3 | import { prepareIcu } from '@app/helpers' | 3 | import { formatICU } from '@app/helpers' |
4 | 4 | ||
5 | export type ResolutionOption = { | 5 | export type ResolutionOption = { |
6 | id: string | 6 | id: string |
@@ -99,10 +99,7 @@ export class EditConfigurationService { | |||
99 | return { | 99 | return { |
100 | value, | 100 | value, |
101 | atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible | 101 | atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible |
102 | unit: prepareIcu($localize`{value, plural, =1 {thread} other {threads}}`)( | 102 | unit: formatICU($localize`{value, plural, =1 {thread} other {threads}}`, { value }) |
103 | { value }, | ||
104 | $localize`threads` | ||
105 | ) | ||
106 | } | 103 | } |
107 | } | 104 | } |
108 | } | 105 | } |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index 2c3b7560d..b381473d6 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts | |||
@@ -9,8 +9,7 @@ import { Notifier } from '@app/core' | |||
9 | import { ServerService } from '@app/core/server/server.service' | 9 | import { ServerService } from '@app/core/server/server.service' |
10 | import { | 10 | import { |
11 | ADMIN_EMAIL_VALIDATOR, | 11 | ADMIN_EMAIL_VALIDATOR, |
12 | CACHE_CAPTIONS_SIZE_VALIDATOR, | 12 | CACHE_SIZE_VALIDATOR, |
13 | CACHE_PREVIEWS_SIZE_VALIDATOR, | ||
14 | CONCURRENCY_VALIDATOR, | 13 | CONCURRENCY_VALIDATOR, |
15 | INDEX_URL_VALIDATOR, | 14 | INDEX_URL_VALIDATOR, |
16 | INSTANCE_NAME_VALIDATOR, | 15 | INSTANCE_NAME_VALIDATOR, |
@@ -120,13 +119,16 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
120 | }, | 119 | }, |
121 | cache: { | 120 | cache: { |
122 | previews: { | 121 | previews: { |
123 | size: CACHE_PREVIEWS_SIZE_VALIDATOR | 122 | size: CACHE_SIZE_VALIDATOR |
124 | }, | 123 | }, |
125 | captions: { | 124 | captions: { |
126 | size: CACHE_CAPTIONS_SIZE_VALIDATOR | 125 | size: CACHE_SIZE_VALIDATOR |
127 | }, | 126 | }, |
128 | torrents: { | 127 | torrents: { |
129 | size: CACHE_CAPTIONS_SIZE_VALIDATOR | 128 | size: CACHE_SIZE_VALIDATOR |
129 | }, | ||
130 | storyboards: { | ||
131 | size: CACHE_SIZE_VALIDATOR | ||
130 | } | 132 | } |
131 | }, | 133 | }, |
132 | signup: { | 134 | signup: { |
@@ -188,7 +190,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
188 | hls: { | 190 | hls: { |
189 | enabled: null | 191 | enabled: null |
190 | }, | 192 | }, |
191 | webtorrent: { | 193 | webVideos: { |
192 | enabled: null | 194 | enabled: null |
193 | }, | 195 | }, |
194 | remoteRunners: { | 196 | remoteRunners: { |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html index fb750aca6..accf2c28c 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html | |||
@@ -67,11 +67,11 @@ | |||
67 | <div class="callout callout-light pt-2 mt-2 pb-0"> | 67 | <div class="callout callout-light pt-2 mt-2 pb-0"> |
68 | <h3 class="callout-title" i18n>Output formats</h3> | 68 | <h3 class="callout-title" i18n>Output formats</h3> |
69 | 69 | ||
70 | <ng-container formGroupName="webtorrent"> | 70 | <ng-container formGroupName="webVideos"> |
71 | <div class="form-group" [ngClass]="getTranscodingDisabledClass()"> | 71 | <div class="form-group" [ngClass]="getTranscodingDisabledClass()"> |
72 | <my-peertube-checkbox | 72 | <my-peertube-checkbox |
73 | inputName="transcodingWebTorrentEnabled" formControlName="enabled" | 73 | inputName="transcodingWebVideosEnabled" formControlName="enabled" |
74 | i18n-labelText labelText="WebTorrent enabled" | 74 | i18n-labelText labelText="Web Videos enabled" |
75 | > | 75 | > |
76 | <ng-template ptTemplate="help"> | 76 | <ng-template ptTemplate="help"> |
77 | <ng-container> | 77 | <ng-container> |
@@ -93,14 +93,14 @@ | |||
93 | <ng-container i18n> | 93 | <ng-container i18n> |
94 | <strong>Requires ffmpeg >= 4.1</strong> | 94 | <strong>Requires ffmpeg >= 4.1</strong> |
95 | 95 | ||
96 | <p>Generate HLS playlists and fragmented MP4 files resulting in a better playback than with plain WebTorrent:</p> | 96 | <p>Generate HLS playlists and fragmented MP4 files resulting in a better playback than with Web Videos:</p> |
97 | <ul> | 97 | <ul> |
98 | <li>Resolution change is smoother</li> | 98 | <li>Resolution change is smoother</li> |
99 | <li>Faster playback especially with long videos</li> | 99 | <li>Faster playback especially with long videos</li> |
100 | <li>More stable playback (less bugs/infinite loading)</li> | 100 | <li>More stable playback (less bugs/infinite loading)</li> |
101 | </ul> | 101 | </ul> |
102 | 102 | ||
103 | <p>If you also enabled WebTorrent support, it will multiply videos storage by 2</p> | 103 | <p>If you also enabled Web Videos support, it will multiply videos storage by 2</p> |
104 | </ng-container> | 104 | </ng-container> |
105 | </ng-template> | 105 | </ng-template> |
106 | </my-peertube-checkbox> | 106 | </my-peertube-checkbox> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts index c5f4ecddb..6496e8753 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts | |||
@@ -90,9 +90,9 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges { | |||
90 | const transcodingControl = this.form.get('transcoding.enabled') | 90 | const transcodingControl = this.form.get('transcoding.enabled') |
91 | const videoStudioControl = this.form.get('videoStudio.enabled') | 91 | const videoStudioControl = this.form.get('videoStudio.enabled') |
92 | const hlsControl = this.form.get('transcoding.hls.enabled') | 92 | const hlsControl = this.form.get('transcoding.hls.enabled') |
93 | const webtorrentControl = this.form.get('transcoding.webtorrent.enabled') | 93 | const webVideosControl = this.form.get('transcoding.webVideos.enabled') |
94 | 94 | ||
95 | webtorrentControl.valueChanges | 95 | webVideosControl.valueChanges |
96 | .subscribe(newValue => { | 96 | .subscribe(newValue => { |
97 | if (newValue === false && !hlsControl.disabled) { | 97 | if (newValue === false && !hlsControl.disabled) { |
98 | hlsControl.disable() | 98 | hlsControl.disable() |
@@ -105,12 +105,12 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges { | |||
105 | 105 | ||
106 | hlsControl.valueChanges | 106 | hlsControl.valueChanges |
107 | .subscribe(newValue => { | 107 | .subscribe(newValue => { |
108 | if (newValue === false && !webtorrentControl.disabled) { | 108 | if (newValue === false && !webVideosControl.disabled) { |
109 | webtorrentControl.disable() | 109 | webVideosControl.disable() |
110 | } | 110 | } |
111 | 111 | ||
112 | if (newValue === true && !webtorrentControl.enabled) { | 112 | if (newValue === true && !webVideosControl.enabled) { |
113 | webtorrentControl.enable() | 113 | webVideosControl.enable() |
114 | } | 114 | } |
115 | }) | 115 | }) |
116 | 116 | ||
@@ -122,7 +122,7 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges { | |||
122 | }) | 122 | }) |
123 | 123 | ||
124 | transcodingControl.updateValueAndValidity() | 124 | transcodingControl.updateValueAndValidity() |
125 | webtorrentControl.updateValueAndValidity() | 125 | webVideosControl.updateValueAndValidity() |
126 | videoStudioControl.updateValueAndValidity() | 126 | videoStudioControl.updateValueAndValidity() |
127 | hlsControl.updateValueAndValidity() | 127 | hlsControl.updateValueAndValidity() |
128 | } | 128 | } |
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts index cebb2e1a2..618892242 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { Component, OnInit } from '@angular/core' | 2 | import { Component, OnInit } from '@angular/core' |
3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' | 3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' |
4 | import { prepareIcu } from '@app/helpers' | 4 | import { formatICU } from '@app/helpers' |
5 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 5 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
6 | import { InstanceFollowService } from '@app/shared/shared-instance' | 6 | import { InstanceFollowService } from '@app/shared/shared-instance' |
7 | import { DropdownAction } from '@app/shared/shared-main' | 7 | import { DropdownAction } from '@app/shared/shared-main' |
@@ -63,9 +63,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O | |||
63 | .subscribe({ | 63 | .subscribe({ |
64 | next: () => { | 64 | next: () => { |
65 | // eslint-disable-next-line max-len | 65 | // eslint-disable-next-line max-len |
66 | const message = prepareIcu($localize`Accepted {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( | 66 | const message = formatICU( |
67 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, | 67 | $localize`Accepted {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`, |
68 | $localize`Follow requests accepted` | 68 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) } |
69 | ) | 69 | ) |
70 | this.notifier.success(message) | 70 | this.notifier.success(message) |
71 | 71 | ||
@@ -78,9 +78,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O | |||
78 | 78 | ||
79 | async rejectFollower (follows: ActorFollow[]) { | 79 | async rejectFollower (follows: ActorFollow[]) { |
80 | // eslint-disable-next-line max-len | 80 | // eslint-disable-next-line max-len |
81 | const message = prepareIcu($localize`Do you really want to reject {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)( | 81 | const message = formatICU( |
82 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, | 82 | $localize`Do you really want to reject {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`, |
83 | $localize`Do you really want to reject these follow requests?` | 83 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) } |
84 | ) | 84 | ) |
85 | 85 | ||
86 | const res = await this.confirmService.confirm(message, $localize`Reject`) | 86 | const res = await this.confirmService.confirm(message, $localize`Reject`) |
@@ -90,9 +90,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O | |||
90 | .subscribe({ | 90 | .subscribe({ |
91 | next: () => { | 91 | next: () => { |
92 | // eslint-disable-next-line max-len | 92 | // eslint-disable-next-line max-len |
93 | const message = prepareIcu($localize`Rejected {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( | 93 | const message = formatICU( |
94 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, | 94 | $localize`Rejected {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`, |
95 | $localize`Follow requests rejected` | 95 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) } |
96 | ) | 96 | ) |
97 | this.notifier.success(message) | 97 | this.notifier.success(message) |
98 | 98 | ||
@@ -110,9 +110,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O | |||
110 | message += '<br /><br />' | 110 | message += '<br /><br />' |
111 | 111 | ||
112 | // eslint-disable-next-line max-len | 112 | // eslint-disable-next-line max-len |
113 | message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)( | 113 | message += formatICU( |
114 | icuParams, | 114 | $localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`, |
115 | $localize`Do you really want to delete these follow requests?` | 115 | icuParams |
116 | ) | 116 | ) |
117 | 117 | ||
118 | const res = await this.confirmService.confirm(message, $localize`Delete`) | 118 | const res = await this.confirmService.confirm(message, $localize`Delete`) |
@@ -122,9 +122,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O | |||
122 | .subscribe({ | 122 | .subscribe({ |
123 | next: () => { | 123 | next: () => { |
124 | // eslint-disable-next-line max-len | 124 | // eslint-disable-next-line max-len |
125 | const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( | 125 | const message = formatICU( |
126 | icuParams, | 126 | $localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`, |
127 | $localize`Follow requests removed` | 127 | icuParams |
128 | ) | 128 | ) |
129 | 129 | ||
130 | this.notifier.success(message) | 130 | this.notifier.success(message) |
diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.ts b/client/src/app/+admin/follows/following-list/follow-modal.component.ts index 8f74e82a6..54b3cebc5 100644 --- a/client/src/app/+admin/follows/following-list/follow-modal.component.ts +++ b/client/src/app/+admin/follows/following-list/follow-modal.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { prepareIcu } from '@app/helpers' | 3 | import { formatICU } from '@app/helpers' |
4 | import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' | 4 | import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' |
5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
6 | import { InstanceFollowService } from '@app/shared/shared-instance' | 6 | import { InstanceFollowService } from '@app/shared/shared-instance' |
@@ -62,9 +62,9 @@ export class FollowModalComponent extends FormReactive implements OnInit { | |||
62 | .subscribe({ | 62 | .subscribe({ |
63 | next: () => { | 63 | next: () => { |
64 | this.notifier.success( | 64 | this.notifier.success( |
65 | prepareIcu($localize`{count, plural, =1 {Follow request sent!} other {Follow requests sent!}}`)( | 65 | formatICU( |
66 | { count: hostsOrHandles.length }, | 66 | $localize`{count, plural, =1 {Follow request sent!} other {Follow requests sent!}}`, |
67 | $localize`Follow request(s) sent!` | 67 | { count: hostsOrHandles.length } |
68 | ) | 68 | ) |
69 | ) | 69 | ) |
70 | 70 | ||
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts index 71f2fbe66..6c8723c16 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.ts +++ b/client/src/app/+admin/follows/following-list/following-list.component.ts | |||
@@ -6,7 +6,7 @@ import { InstanceFollowService } from '@app/shared/shared-instance' | |||
6 | import { ActorFollow } from '@shared/models' | 6 | import { ActorFollow } from '@shared/models' |
7 | import { FollowModalComponent } from './follow-modal.component' | 7 | import { FollowModalComponent } from './follow-modal.component' |
8 | import { DropdownAction } from '@app/shared/shared-main' | 8 | import { DropdownAction } from '@app/shared/shared-main' |
9 | import { prepareIcu } from '@app/helpers' | 9 | import { formatICU } from '@app/helpers' |
10 | 10 | ||
11 | @Component({ | 11 | @Component({ |
12 | templateUrl: './following-list.component.html', | 12 | templateUrl: './following-list.component.html', |
@@ -64,9 +64,9 @@ export class FollowingListComponent extends RestTable <ActorFollow> implements O | |||
64 | async removeFollowing (follows: ActorFollow[]) { | 64 | async removeFollowing (follows: ActorFollow[]) { |
65 | const icuParams = { count: follows.length, entryName: this.buildFollowingName(follows[0]) } | 65 | const icuParams = { count: follows.length, entryName: this.buildFollowingName(follows[0]) } |
66 | 66 | ||
67 | const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)( | 67 | const message = formatICU( |
68 | icuParams, | 68 | $localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`, |
69 | $localize`Do you really want to unfollow these entries?` | 69 | icuParams |
70 | ) | 70 | ) |
71 | 71 | ||
72 | const res = await this.confirmService.confirm(message, $localize`Unfollow`) | 72 | const res = await this.confirmService.confirm(message, $localize`Unfollow`) |
@@ -76,9 +76,9 @@ export class FollowingListComponent extends RestTable <ActorFollow> implements O | |||
76 | .subscribe({ | 76 | .subscribe({ |
77 | next: () => { | 77 | next: () => { |
78 | // eslint-disable-next-line max-len | 78 | // eslint-disable-next-line max-len |
79 | const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)( | 79 | const message = formatICU( |
80 | icuParams, | 80 | $localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`, |
81 | $localize`You are not following them anymore.` | 81 | icuParams |
82 | ) | 82 | ) |
83 | 83 | ||
84 | this.notifier.success(message) | 84 | this.notifier.success(message) |
diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.ts b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts index 3ca1ceab8..35d9d13d7 100644 --- a/client/src/app/+admin/moderation/registration-list/registration-list.component.ts +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts | |||
@@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api' | |||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | 4 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' |
5 | import { prepareIcu } from '@app/helpers' | 5 | import { formatICU } from '@app/helpers' |
6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
7 | import { DropdownAction } from '@app/shared/shared-main' | 7 | import { DropdownAction } from '@app/shared/shared-main' |
8 | import { UserRegistration, UserRegistrationState } from '@shared/models' | 8 | import { UserRegistration, UserRegistrationState } from '@shared/models' |
@@ -121,9 +121,9 @@ export class RegistrationListComponent extends RestTable <UserRegistration> impl | |||
121 | const icuParams = { count: registrations.length, username: registrations[0].username } | 121 | const icuParams = { count: registrations.length, username: registrations[0].username } |
122 | 122 | ||
123 | // eslint-disable-next-line max-len | 123 | // eslint-disable-next-line max-len |
124 | const message = prepareIcu($localize`Do you really want to delete {count, plural, =1 {{username} registration request?} other {{count} registration requests?}}`)( | 124 | const message = formatICU( |
125 | icuParams, | 125 | $localize`Do you really want to delete {count, plural, =1 {{username} registration request?} other {{count} registration requests?}}`, |
126 | $localize`Do you really want to delete these registration requests?` | 126 | icuParams |
127 | ) | 127 | ) |
128 | 128 | ||
129 | const res = await this.confirmService.confirm(message, $localize`Delete`) | 129 | const res = await this.confirmService.confirm(message, $localize`Delete`) |
@@ -133,9 +133,9 @@ export class RegistrationListComponent extends RestTable <UserRegistration> impl | |||
133 | .subscribe({ | 133 | .subscribe({ |
134 | next: () => { | 134 | next: () => { |
135 | // eslint-disable-next-line max-len | 135 | // eslint-disable-next-line max-len |
136 | const message = prepareIcu($localize`Removed {count, plural, =1 {{username} registration request} other {{count} registration requests}}`)( | 136 | const message = formatICU( |
137 | icuParams, | 137 | $localize`Removed {count, plural, =1 {{username} registration request} other {{count} registration requests}}`, |
138 | $localize`Registration requests removed` | 138 | icuParams |
139 | ) | 139 | ) |
140 | 140 | ||
141 | this.notifier.success(message) | 141 | this.notifier.success(message) |
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.ts b/client/src/app/+admin/overview/comments/video-comment-list.component.ts index 28efdc076..b77072665 100644 --- a/client/src/app/+admin/overview/comments/video-comment-list.component.ts +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.ts | |||
@@ -7,7 +7,7 @@ import { DropdownAction } from '@app/shared/shared-main' | |||
7 | import { BulkService } from '@app/shared/shared-moderation' | 7 | import { BulkService } from '@app/shared/shared-moderation' |
8 | import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' | 8 | import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' |
9 | import { FeedFormat, UserRight } from '@shared/models' | 9 | import { FeedFormat, UserRight } from '@shared/models' |
10 | import { prepareIcu } from '@app/helpers' | 10 | import { formatICU } from '@app/helpers' |
11 | 11 | ||
12 | @Component({ | 12 | @Component({ |
13 | selector: 'my-video-comment-list', | 13 | selector: 'my-video-comment-list', |
@@ -146,9 +146,9 @@ export class VideoCommentListComponent extends RestTable <VideoCommentAdmin> imp | |||
146 | .subscribe({ | 146 | .subscribe({ |
147 | next: () => { | 147 | next: () => { |
148 | this.notifier.success( | 148 | this.notifier.success( |
149 | prepareIcu($localize`{count, plural, =1 {1 comment deleted.} other {{count} comments deleted.}}`)( | 149 | formatICU( |
150 | { count: commentArgs.length }, | 150 | $localize`{count, plural, =1 {1 comment deleted.} other {{count} comments deleted.}}`, |
151 | $localize`${commentArgs.length} comment(s) deleted.` | 151 | { count: commentArgs.length } |
152 | ) | 152 | ) |
153 | ) | 153 | ) |
154 | 154 | ||
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.ts b/client/src/app/+admin/overview/users/user-list/user-list.component.ts index 19420b748..5d5abf6f4 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.ts | |||
@@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api' | |||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | 4 | import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' |
5 | import { getAPIHost, prepareIcu } from '@app/helpers' | 5 | import { formatICU, getAPIHost } from '@app/helpers' |
6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
7 | import { Actor, DropdownAction } from '@app/shared/shared-main' | 7 | import { Actor, DropdownAction } from '@app/shared/shared-main' |
8 | import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation' | 8 | import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation' |
@@ -210,9 +210,9 @@ export class UserListComponent extends RestTable <User> implements OnInit { | |||
210 | 210 | ||
211 | async unbanUsers (users: User[]) { | 211 | async unbanUsers (users: User[]) { |
212 | const res = await this.confirmService.confirm( | 212 | const res = await this.confirmService.confirm( |
213 | prepareIcu($localize`Do you really want to unban {count, plural, =1 {1 user} other {{count} users}}?`)( | 213 | formatICU( |
214 | { count: users.length }, | 214 | $localize`Do you really want to unban {count, plural, =1 {1 user} other {{count} users}}?`, |
215 | $localize`Do you really want to unban ${users.length} users?` | 215 | { count: users.length } |
216 | ), | 216 | ), |
217 | $localize`Unban` | 217 | $localize`Unban` |
218 | ) | 218 | ) |
@@ -223,9 +223,9 @@ export class UserListComponent extends RestTable <User> implements OnInit { | |||
223 | .subscribe({ | 223 | .subscribe({ |
224 | next: () => { | 224 | next: () => { |
225 | this.notifier.success( | 225 | this.notifier.success( |
226 | prepareIcu($localize`{count, plural, =1 {1 user unbanned.} other {{count} users unbanned.}}`)( | 226 | formatICU( |
227 | { count: users.length }, | 227 | $localize`{count, plural, =1 {1 user unbanned.} other {{count} users unbanned.}}`, |
228 | $localize`${users.length} users unbanned.` | 228 | { count: users.length } |
229 | ) | 229 | ) |
230 | ) | 230 | ) |
231 | this.reloadData() | 231 | this.reloadData() |
@@ -252,9 +252,9 @@ export class UserListComponent extends RestTable <User> implements OnInit { | |||
252 | .subscribe({ | 252 | .subscribe({ |
253 | next: () => { | 253 | next: () => { |
254 | this.notifier.success( | 254 | this.notifier.success( |
255 | prepareIcu($localize`{count, plural, =1 {1 user deleted.} other {{count} users deleted.}}`)( | 255 | formatICU( |
256 | { count: users.length }, | 256 | $localize`{count, plural, =1 {1 user deleted.} other {{count} users deleted.}}`, |
257 | $localize`${users.length} users deleted.` | 257 | { count: users.length } |
258 | ) | 258 | ) |
259 | ) | 259 | ) |
260 | 260 | ||
@@ -270,9 +270,9 @@ export class UserListComponent extends RestTable <User> implements OnInit { | |||
270 | .subscribe({ | 270 | .subscribe({ |
271 | next: () => { | 271 | next: () => { |
272 | this.notifier.success( | 272 | this.notifier.success( |
273 | prepareIcu($localize`{count, plural, =1 {1 user email set as verified.} other {{count} user emails set as verified.}}`)( | 273 | formatICU( |
274 | { count: users.length }, | 274 | $localize`{count, plural, =1 {1 user email set as verified.} other {{count} user emails set as verified.}}`, |
275 | $localize`${users.length} users email set as verified.` | 275 | { count: users.length } |
276 | ) | 276 | ) |
277 | ) | 277 | ) |
278 | 278 | ||
diff --git a/client/src/app/+admin/overview/videos/video-admin.service.ts b/client/src/app/+admin/overview/videos/video-admin.service.ts index 4b9357fb7..722495706 100644 --- a/client/src/app/+admin/overview/videos/video-admin.service.ts +++ b/client/src/app/+admin/overview/videos/video-admin.service.ts | |||
@@ -59,12 +59,12 @@ export class VideoAdminService { | |||
59 | title: $localize`Video files`, | 59 | title: $localize`Video files`, |
60 | children: [ | 60 | children: [ |
61 | { | 61 | { |
62 | value: 'webtorrent:true isLocal:true', | 62 | value: 'webVideos:true isLocal:true', |
63 | label: $localize`With WebTorrent` | 63 | label: $localize`With Web Videos` |
64 | }, | 64 | }, |
65 | { | 65 | { |
66 | value: 'webtorrent:false isLocal:true', | 66 | value: 'webVideos:false isLocal:true', |
67 | label: $localize`Without WebTorrent` | 67 | label: $localize`Without Web Videos` |
68 | }, | 68 | }, |
69 | { | 69 | { |
70 | value: 'hls:true isLocal:true', | 70 | value: 'hls:true isLocal:true', |
@@ -126,8 +126,8 @@ export class VideoAdminService { | |||
126 | prefix: 'hls:', | 126 | prefix: 'hls:', |
127 | isBoolean: true | 127 | isBoolean: true |
128 | }, | 128 | }, |
129 | hasWebtorrentFiles: { | 129 | hasWebVideoFiles: { |
130 | prefix: 'webtorrent:', | 130 | prefix: 'webVideos:', |
131 | isBoolean: true | 131 | isBoolean: true |
132 | }, | 132 | }, |
133 | isLive: { | 133 | isLive: { |
@@ -151,7 +151,7 @@ export class VideoAdminService { | |||
151 | } | 151 | } |
152 | 152 | ||
153 | if (filters.excludePublic) { | 153 | if (filters.excludePublic) { |
154 | privacyOneOf = [ VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.INTERNAL ] | 154 | privacyOneOf = [ VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.INTERNAL, VideoPrivacy.PASSWORD_PROTECTED ] |
155 | 155 | ||
156 | filters.excludePublic = undefined | 156 | filters.excludePublic = undefined |
157 | } | 157 | } |
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html index c4f78cadc..3a4666435 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.html +++ b/client/src/app/+admin/overview/videos/video-list.component.html | |||
@@ -83,8 +83,8 @@ | |||
83 | </td> | 83 | </td> |
84 | 84 | ||
85 | <td> | 85 | <td> |
86 | <span *ngIf="isHLS(video)" class="pt-badge badge-blue">HLS</span> | 86 | <span *ngIf="hasHLS(video)" class="pt-badge badge-blue">HLS</span> |
87 | <span *ngIf="isWebTorrent(video)" class="pt-badge badge-blue">WebTorrent ({{ video.files.length }})</span> | 87 | <span *ngIf="hasWebVideos(video)" class="pt-badge badge-blue">Web Videos ({{ video.files.length }})</span> |
88 | <span i18n *ngIf="video.isLive" class="pt-badge badge-blue">Live</span> | 88 | <span i18n *ngIf="video.isLive" class="pt-badge badge-blue">Live</span> |
89 | <span i18n *ngIf="hasObjectStorage(video)" class="pt-badge badge-purple">Object storage</span> | 89 | <span i18n *ngIf="hasObjectStorage(video)" class="pt-badge badge-purple">Object storage</span> |
90 | 90 | ||
@@ -102,8 +102,8 @@ | |||
102 | <tr> | 102 | <tr> |
103 | <td class="video-info expand-cell" myAutoColspan> | 103 | <td class="video-info expand-cell" myAutoColspan> |
104 | <div> | 104 | <div> |
105 | <div *ngIf="isWebTorrent(video)"> | 105 | <div *ngIf="hasWebVideos(video)"> |
106 | WebTorrent: | 106 | Web Videos: |
107 | 107 | ||
108 | <ul> | 108 | <ul> |
109 | <li *ngFor="let file of video.files"> | 109 | <li *ngFor="let file of video.files"> |
@@ -112,13 +112,13 @@ | |||
112 | <my-global-icon | 112 | <my-global-icon |
113 | *ngIf="canRemoveOneFile(video)" | 113 | *ngIf="canRemoveOneFile(video)" |
114 | i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button" | 114 | i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button" |
115 | (click)="removeVideoFile(video, file, 'webtorrent')" | 115 | (click)="removeVideoFile(video, file, 'web-videos')" |
116 | ></my-global-icon> | 116 | ></my-global-icon> |
117 | </li> | 117 | </li> |
118 | </ul> | 118 | </ul> |
119 | </div> | 119 | </div> |
120 | 120 | ||
121 | <div *ngIf="isHLS(video)"> | 121 | <div *ngIf="hasHLS(video)"> |
122 | HLS: | 122 | HLS: |
123 | 123 | ||
124 | <ul> | 124 | <ul> |
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts index ebf82ce16..52f02d8d0 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.ts +++ b/client/src/app/+admin/overview/videos/video-list.component.ts | |||
@@ -3,7 +3,7 @@ import { finalize } from 'rxjs/operators' | |||
3 | import { Component, OnInit, ViewChild } from '@angular/core' | 3 | import { Component, OnInit, ViewChild } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' | 5 | import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' |
6 | import { prepareIcu } from '@app/helpers' | 6 | import { formatICU } from '@app/helpers' |
7 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 7 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
9 | import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' | 9 | import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' |
@@ -99,8 +99,8 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
99 | iconName: 'cog' | 99 | iconName: 'cog' |
100 | }, | 100 | }, |
101 | { | 101 | { |
102 | label: $localize`Run WebTorrent transcoding`, | 102 | label: $localize`Run Web Video transcoding`, |
103 | handler: videos => this.runTranscoding(videos, 'webtorrent'), | 103 | handler: videos => this.runTranscoding(videos, 'web-video'), |
104 | isDisplayed: videos => videos.every(v => v.canRunTranscoding(this.authUser)), | 104 | isDisplayed: videos => videos.every(v => v.canRunTranscoding(this.authUser)), |
105 | iconName: 'cog' | 105 | iconName: 'cog' |
106 | }, | 106 | }, |
@@ -111,8 +111,8 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
111 | iconName: 'delete' | 111 | iconName: 'delete' |
112 | }, | 112 | }, |
113 | { | 113 | { |
114 | label: $localize`Delete WebTorrent files`, | 114 | label: $localize`Delete Web Video files`, |
115 | handler: videos => this.removeVideoFiles(videos, 'webtorrent'), | 115 | handler: videos => this.removeVideoFiles(videos, 'web-videos'), |
116 | isDisplayed: videos => videos.every(v => v.canRemoveFiles(this.authUser)), | 116 | isDisplayed: videos => videos.every(v => v.canRemoveFiles(this.authUser)), |
117 | iconName: 'delete' | 117 | iconName: 'delete' |
118 | } | 118 | } |
@@ -150,14 +150,14 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
150 | return video.state.id === VideoState.TO_IMPORT | 150 | return video.state.id === VideoState.TO_IMPORT |
151 | } | 151 | } |
152 | 152 | ||
153 | isHLS (video: Video) { | 153 | hasHLS (video: Video) { |
154 | const p = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | 154 | const p = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) |
155 | if (!p) return false | 155 | if (!p) return false |
156 | 156 | ||
157 | return p.files.length !== 0 | 157 | return p.files.length !== 0 |
158 | } | 158 | } |
159 | 159 | ||
160 | isWebTorrent (video: Video) { | 160 | hasWebVideos (video: Video) { |
161 | return video.files.length !== 0 | 161 | return video.files.length !== 0 |
162 | } | 162 | } |
163 | 163 | ||
@@ -176,14 +176,14 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
176 | getFilesSize (video: Video) { | 176 | getFilesSize (video: Video) { |
177 | let files = video.files | 177 | let files = video.files |
178 | 178 | ||
179 | if (this.isHLS(video)) { | 179 | if (this.hasHLS(video)) { |
180 | files = files.concat(video.streamingPlaylists[0].files) | 180 | files = files.concat(video.streamingPlaylists[0].files) |
181 | } | 181 | } |
182 | 182 | ||
183 | return files.reduce((p, f) => p += f.size, 0) | 183 | return files.reduce((p, f) => p += f.size, 0) |
184 | } | 184 | } |
185 | 185 | ||
186 | async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'webtorrent') { | 186 | async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'web-videos') { |
187 | const message = $localize`Are you sure you want to delete this ${file.resolution.label} file?` | 187 | const message = $localize`Are you sure you want to delete this ${file.resolution.label} file?` |
188 | const res = await this.confirmService.confirm(message, $localize`Delete file`) | 188 | const res = await this.confirmService.confirm(message, $localize`Delete file`) |
189 | if (res === false) return | 189 | if (res === false) return |
@@ -219,9 +219,9 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
219 | } | 219 | } |
220 | 220 | ||
221 | private async removeVideos (videos: Video[]) { | 221 | private async removeVideos (videos: Video[]) { |
222 | const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)( | 222 | const message = formatICU( |
223 | { count: videos.length }, | 223 | $localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`, |
224 | $localize`Are you sure you want to delete these ${videos.length} videos?` | 224 | { count: videos.length } |
225 | ) | 225 | ) |
226 | 226 | ||
227 | const res = await this.confirmService.confirm(message, $localize`Delete`) | 227 | const res = await this.confirmService.confirm(message, $localize`Delete`) |
@@ -231,9 +231,9 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
231 | .subscribe({ | 231 | .subscribe({ |
232 | next: () => { | 232 | next: () => { |
233 | this.notifier.success( | 233 | this.notifier.success( |
234 | prepareIcu($localize`Deleted {count, plural, =1 {1 video} other {{count} videos}}.`)( | 234 | formatICU( |
235 | { count: videos.length }, | 235 | $localize`Deleted {count, plural, =1 {1 video} other {{count} videos}}.`, |
236 | $localize`Deleted ${videos.length} videos.` | 236 | { count: videos.length } |
237 | ) | 237 | ) |
238 | ) | 238 | ) |
239 | 239 | ||
@@ -249,9 +249,9 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
249 | .subscribe({ | 249 | .subscribe({ |
250 | next: () => { | 250 | next: () => { |
251 | this.notifier.success( | 251 | this.notifier.success( |
252 | prepareIcu($localize`Unblocked {count, plural, =1 {1 video} other {{count} videos}}.`)( | 252 | formatICU( |
253 | { count: videos.length }, | 253 | $localize`Unblocked {count, plural, =1 {1 video} other {{count} videos}}.`, |
254 | $localize`Unblocked ${videos.length} videos.` | 254 | { count: videos.length } |
255 | ) | 255 | ) |
256 | ) | 256 | ) |
257 | 257 | ||
@@ -262,20 +262,20 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
262 | }) | 262 | }) |
263 | } | 263 | } |
264 | 264 | ||
265 | private async removeVideoFiles (videos: Video[], type: 'hls' | 'webtorrent') { | 265 | private async removeVideoFiles (videos: Video[], type: 'hls' | 'web-videos') { |
266 | let message: string | 266 | let message: string |
267 | 267 | ||
268 | if (type === 'hls') { | 268 | if (type === 'hls') { |
269 | // eslint-disable-next-line max-len | 269 | // eslint-disable-next-line max-len |
270 | message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {1 HLS streaming playlist} other {{count} HLS streaming playlists}}?`)( | 270 | message = formatICU( |
271 | { count: videos.length }, | 271 | $localize`Are you sure you want to delete {count, plural, =1 {1 HLS streaming playlist} other {{count} HLS streaming playlists}}?`, |
272 | $localize`Are you sure you want to delete ${videos.length} HLS streaming playlists?` | 272 | { count: videos.length } |
273 | ) | 273 | ) |
274 | } else { | 274 | } else { |
275 | // eslint-disable-next-line max-len | 275 | // eslint-disable-next-line max-len |
276 | message = prepareIcu($localize`Are you sure you want to delete WebTorrent files of {count, plural, =1 {1 video} other {{count} videos}}?`)( | 276 | message = formatICU( |
277 | { count: videos.length }, | 277 | $localize`Are you sure you want to delete Web Video files of {count, plural, =1 {1 video} other {{count} videos}}?`, |
278 | $localize`Are you sure you want to delete WebTorrent files of ${videos.length} videos?` | 278 | { count: videos.length } |
279 | ) | 279 | ) |
280 | } | 280 | } |
281 | 281 | ||
@@ -293,7 +293,7 @@ export class VideoListComponent extends RestTable <Video> implements OnInit { | |||
293 | }) | 293 | }) |
294 | } | 294 | } |
295 | 295 | ||
296 | private runTranscoding (videos: Video[], type: 'hls' | 'webtorrent') { | 296 | private runTranscoding (videos: Video[], type: 'hls' | 'web-video') { |
297 | this.videoService.runTranscoding(videos.map(v => v.id), type) | 297 | this.videoService.runTranscoding(videos.map(v => v.id), type) |
298 | .subscribe({ | 298 | .subscribe({ |
299 | next: () => { | 299 | next: () => { |
diff --git a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts index 8ba956eb8..8994c1d00 100644 --- a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts +++ b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { Component, OnInit } from '@angular/core' | 2 | import { Component, OnInit } from '@angular/core' |
3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' | 3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' |
4 | import { prepareIcu } from '@app/helpers' | 4 | import { formatICU } from '@app/helpers' |
5 | import { DropdownAction } from '@app/shared/shared-main' | 5 | import { DropdownAction } from '@app/shared/shared-main' |
6 | import { RunnerJob, RunnerJobState } from '@shared/models' | 6 | import { RunnerJob, RunnerJobState } from '@shared/models' |
7 | import { RunnerJobFormatted, RunnerService } from '../runner.service' | 7 | import { RunnerJobFormatted, RunnerService } from '../runner.service' |
@@ -57,9 +57,10 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI | |||
57 | } | 57 | } |
58 | 58 | ||
59 | async cancelJobs (jobs: RunnerJob[]) { | 59 | async cancelJobs (jobs: RunnerJob[]) { |
60 | const message = prepareIcu( | 60 | const message = formatICU( |
61 | $localize`Do you really want to cancel {count, plural, =1 {this job} other {{count} jobs}}? Children jobs will also be cancelled.` | 61 | $localize`Do you really want to cancel {count, plural, =1 {this job} other {{count} jobs}}? Children jobs will also be cancelled.`, |
62 | )({ count: jobs.length }, $localize`Do you really want to cancel these jobs? Children jobs will also be cancelled.`) | 62 | { count: jobs.length } |
63 | ) | ||
63 | 64 | ||
64 | const res = await this.confirmService.confirm(message, $localize`Cancel`) | 65 | const res = await this.confirmService.confirm(message, $localize`Cancel`) |
65 | 66 | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts index 97ffb6013..393c3ad6b 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts | |||
@@ -30,7 +30,7 @@ export class MyAccountTwoFactorButtonComponent implements OnInit { | |||
30 | async disableTwoFactor () { | 30 | async disableTwoFactor () { |
31 | const message = $localize`Are you sure you want to disable two factor authentication of your account?` | 31 | const message = $localize`Are you sure you want to disable two factor authentication of your account?` |
32 | 32 | ||
33 | const { confirmed, password } = await this.confirmService.confirmWithPassword(message, $localize`Disable two factor`) | 33 | const { confirmed, password } = await this.confirmService.confirmWithPassword({ message, title: $localize`Disable two factor` }) |
34 | if (confirmed === false) return | 34 | if (confirmed === false) return |
35 | 35 | ||
36 | this.twoFactorService.disableTwoFactor({ userId: this.user.id, currentPassword: password }) | 36 | this.twoFactorService.disableTwoFactor({ userId: this.user.id, currentPassword: password }) |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts index 633720a6c..4d5dbbc2b 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts | |||
@@ -54,7 +54,7 @@ export class MyVideoChannelsComponent { | |||
54 | const res = await this.confirmService.confirmWithExpectedInput( | 54 | const res = await this.confirmService.confirmWithExpectedInput( |
55 | $localize`Do you really want to delete ${videoChannel.displayName}? | 55 | $localize`Do you really want to delete ${videoChannel.displayName}? |
56 | It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another | 56 | It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another |
57 | channel with the same name (${videoChannel.name})!`, | 57 | channel or account with the same name (${videoChannel.name})!`, |
58 | 58 | ||
59 | $localize`Please type the name of the video channel (${videoChannel.name}) to confirm`, | 59 | $localize`Please type the name of the video channel (${videoChannel.name}) to confirm`, |
60 | 60 | ||
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts index 57b8bdf7d..1827d6a0b 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.ts +++ b/client/src/app/+my-library/my-videos/my-videos.component.ts | |||
@@ -5,7 +5,7 @@ import { Component, OnInit, ViewChild } from '@angular/core' | |||
5 | import { ActivatedRoute, Router } from '@angular/router' | 5 | import { ActivatedRoute, Router } from '@angular/router' |
6 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' | 6 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' |
7 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | 7 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' |
8 | import { immutableAssign, prepareIcu } from '@app/helpers' | 8 | import { immutableAssign, formatICU } from '@app/helpers' |
9 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 9 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
10 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 10 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
11 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' | 11 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' |
@@ -184,9 +184,9 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
184 | .map(([ k, _v ]) => parseInt(k, 10)) | 184 | .map(([ k, _v ]) => parseInt(k, 10)) |
185 | 185 | ||
186 | const res = await this.confirmService.confirm( | 186 | const res = await this.confirmService.confirm( |
187 | prepareIcu($localize`Do you really want to delete {length, plural, =1 {this video} other {{length} videos}}?`)( | 187 | formatICU( |
188 | { length: toDeleteVideosIds.length }, | 188 | $localize`Do you really want to delete {length, plural, =1 {this video} other {{length} videos}}?`, |
189 | $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?` | 189 | { length: toDeleteVideosIds.length } |
190 | ), | 190 | ), |
191 | $localize`Delete` | 191 | $localize`Delete` |
192 | ) | 192 | ) |
@@ -205,9 +205,9 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
205 | .subscribe({ | 205 | .subscribe({ |
206 | next: () => { | 206 | next: () => { |
207 | this.notifier.success( | 207 | this.notifier.success( |
208 | prepareIcu($localize`{length, plural, =1 {Video has been deleted} other {{length} videos have been deleted}}`)( | 208 | formatICU( |
209 | { length: toDeleteVideosIds.length }, | 209 | $localize`{length, plural, =1 {Video has been deleted} other {{length} videos have been deleted}}`, |
210 | $localize`${toDeleteVideosIds.length} have been deleted.` | 210 | { length: toDeleteVideosIds.length } |
211 | ) | 211 | ) |
212 | ) | 212 | ) |
213 | 213 | ||
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html index b607dabe9..97b713874 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html | |||
@@ -120,7 +120,12 @@ | |||
120 | </div> | 120 | </div> |
121 | </div> | 121 | </div> |
122 | 122 | ||
123 | <div *ngIf="schedulePublicationEnabled" class="form-group"> | 123 | <div *ngIf="passwordProtectionSelected" class="form-group"> |
124 | <label i18n for="videoPassword">Password</label> | ||
125 | <my-input-text formControlName="videoPassword" inputId="videoPassword" [withCopy]="true" [formError]="formErrors['videoPassword']"></my-input-text> | ||
126 | </div> | ||
127 | |||
128 | <div *ngIf="schedulePublicationSelected" class="form-group"> | ||
124 | <label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label> | 129 | <label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label> |
125 | <p-calendar | 130 | <p-calendar |
126 | id="schedulePublicationAt" formControlName="schedulePublicationAt" [dateFormat]="calendarDateFormat" | 131 | id="schedulePublicationAt" formControlName="schedulePublicationAt" [dateFormat]="calendarDateFormat" |
@@ -287,7 +292,7 @@ | |||
287 | <div class="form-group mx-4" *ngIf="isSaveReplayEnabled()"> | 292 | <div class="form-group mx-4" *ngIf="isSaveReplayEnabled()"> |
288 | <label i18n for="replayPrivacy">Privacy of the new replay</label> | 293 | <label i18n for="replayPrivacy">Privacy of the new replay</label> |
289 | <my-select-options | 294 | <my-select-options |
290 | labelForId="replayPrivacy" [items]="videoPrivacies" [clearable]="false" formControlName="replayPrivacy" | 295 | labelForId="replayPrivacy" [items]="replayPrivacies" [clearable]="false" formControlName="replayPrivacy" |
291 | ></my-select-options> | 296 | ></my-select-options> |
292 | </div> | 297 | </div> |
293 | 298 | ||
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts index 8ed54ce6b..5e5df8db7 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts | |||
@@ -14,6 +14,7 @@ import { | |||
14 | VIDEO_LICENCE_VALIDATOR, | 14 | VIDEO_LICENCE_VALIDATOR, |
15 | VIDEO_NAME_VALIDATOR, | 15 | VIDEO_NAME_VALIDATOR, |
16 | VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR, | 16 | VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR, |
17 | VIDEO_PASSWORD_VALIDATOR, | ||
17 | VIDEO_PRIVACY_VALIDATOR, | 18 | VIDEO_PRIVACY_VALIDATOR, |
18 | VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR, | 19 | VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR, |
19 | VIDEO_SUPPORT_VALIDATOR, | 20 | VIDEO_SUPPORT_VALIDATOR, |
@@ -79,7 +80,8 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
79 | // So that it can be accessed in the template | 80 | // So that it can be accessed in the template |
80 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY | 81 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY |
81 | 82 | ||
82 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] | 83 | videoPrivacies: VideoConstant<VideoPrivacy | typeof VideoEdit.SPECIAL_SCHEDULED_PRIVACY > [] = [] |
84 | replayPrivacies: VideoConstant<VideoPrivacy> [] = [] | ||
83 | videoCategories: VideoConstant<number>[] = [] | 85 | videoCategories: VideoConstant<number>[] = [] |
84 | videoLicences: VideoConstant<number>[] = [] | 86 | videoLicences: VideoConstant<number>[] = [] |
85 | videoLanguages: VideoLanguages[] = [] | 87 | videoLanguages: VideoLanguages[] = [] |
@@ -103,7 +105,8 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
103 | 105 | ||
104 | pluginDataFormGroup: FormGroup | 106 | pluginDataFormGroup: FormGroup |
105 | 107 | ||
106 | schedulePublicationEnabled = false | 108 | schedulePublicationSelected = false |
109 | passwordProtectionSelected = false | ||
107 | 110 | ||
108 | calendarLocale: any = {} | 111 | calendarLocale: any = {} |
109 | minScheduledDate = new Date() | 112 | minScheduledDate = new Date() |
@@ -148,6 +151,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
148 | const obj: { [ id: string ]: BuildFormValidator } = { | 151 | const obj: { [ id: string ]: BuildFormValidator } = { |
149 | name: VIDEO_NAME_VALIDATOR, | 152 | name: VIDEO_NAME_VALIDATOR, |
150 | privacy: VIDEO_PRIVACY_VALIDATOR, | 153 | privacy: VIDEO_PRIVACY_VALIDATOR, |
154 | videoPassword: VIDEO_PASSWORD_VALIDATOR, | ||
151 | channelId: VIDEO_CHANNEL_VALIDATOR, | 155 | channelId: VIDEO_CHANNEL_VALIDATOR, |
152 | nsfw: null, | 156 | nsfw: null, |
153 | commentsEnabled: null, | 157 | commentsEnabled: null, |
@@ -222,7 +226,9 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
222 | 226 | ||
223 | this.serverService.getVideoPrivacies() | 227 | this.serverService.getVideoPrivacies() |
224 | .subscribe(privacies => { | 228 | .subscribe(privacies => { |
225 | this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies).videoPrivacies | 229 | const videoPrivacies = this.videoService.explainedPrivacyLabels(privacies).videoPrivacies |
230 | this.videoPrivacies = videoPrivacies | ||
231 | this.replayPrivacies = videoPrivacies.filter((privacy) => privacy.id !== VideoPrivacy.PASSWORD_PROTECTED) | ||
226 | 232 | ||
227 | // Can't schedule publication if private privacy is not available (could be deleted by a plugin) | 233 | // Can't schedule publication if private privacy is not available (could be deleted by a plugin) |
228 | const hasPrivatePrivacy = this.videoPrivacies.some(p => p.id === VideoPrivacy.PRIVATE) | 234 | const hasPrivatePrivacy = this.videoPrivacies.some(p => p.id === VideoPrivacy.PRIVATE) |
@@ -410,13 +416,13 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
410 | .subscribe( | 416 | .subscribe( |
411 | newPrivacyId => { | 417 | newPrivacyId => { |
412 | 418 | ||
413 | this.schedulePublicationEnabled = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY | 419 | this.schedulePublicationSelected = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY |
414 | 420 | ||
415 | // Value changed | 421 | // Value changed |
416 | const scheduleControl = this.form.get('schedulePublicationAt') | 422 | const scheduleControl = this.form.get('schedulePublicationAt') |
417 | const waitTranscodingControl = this.form.get('waitTranscoding') | 423 | const waitTranscodingControl = this.form.get('waitTranscoding') |
418 | 424 | ||
419 | if (this.schedulePublicationEnabled) { | 425 | if (this.schedulePublicationSelected) { |
420 | scheduleControl.setValidators([ Validators.required ]) | 426 | scheduleControl.setValidators([ Validators.required ]) |
421 | 427 | ||
422 | waitTranscodingControl.disable() | 428 | waitTranscodingControl.disable() |
@@ -437,6 +443,16 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
437 | 443 | ||
438 | this.firstPatchDone = true | 444 | this.firstPatchDone = true |
439 | 445 | ||
446 | this.passwordProtectionSelected = newPrivacyId === VideoPrivacy.PASSWORD_PROTECTED | ||
447 | const videoPasswordControl = this.form.get('videoPassword') | ||
448 | |||
449 | if (this.passwordProtectionSelected) { | ||
450 | videoPasswordControl.setValidators([ Validators.required ]) | ||
451 | } else { | ||
452 | videoPasswordControl.clearValidators() | ||
453 | } | ||
454 | videoPasswordControl.updateValueAndValidity() | ||
455 | |||
440 | } | 456 | } |
441 | ) | 457 | ) |
442 | } | 458 | } |
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts index ad71162b8..e51047e8c 100644 --- a/client/src/app/+videos/+video-edit/video-update.component.ts +++ b/client/src/app/+videos/+video-edit/video-update.component.ts | |||
@@ -10,7 +10,7 @@ import { LiveVideoService } from '@app/shared/shared-video-live' | |||
10 | import { LoadingBarService } from '@ngx-loading-bar/core' | 10 | import { LoadingBarService } from '@ngx-loading-bar/core' |
11 | import { logger } from '@root-helpers/logger' | 11 | import { logger } from '@root-helpers/logger' |
12 | import { pick, simpleObjectsDeepEqual } from '@shared/core-utils' | 12 | import { pick, simpleObjectsDeepEqual } from '@shared/core-utils' |
13 | import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models' | 13 | import { LiveVideo, LiveVideoUpdate, VideoPrivacy, VideoState } from '@shared/models' |
14 | import { VideoSource } from '@shared/models/videos/video-source' | 14 | import { VideoSource } from '@shared/models/videos/video-source' |
15 | import { hydrateFormFromVideo } from './shared/video-edit-utils' | 15 | import { hydrateFormFromVideo } from './shared/video-edit-utils' |
16 | 16 | ||
@@ -49,10 +49,10 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
49 | this.buildForm({}) | 49 | this.buildForm({}) |
50 | 50 | ||
51 | const { videoData } = this.route.snapshot.data | 51 | const { videoData } = this.route.snapshot.data |
52 | const { video, videoChannels, videoCaptions, videoSource, liveVideo } = videoData | 52 | const { video, videoChannels, videoCaptions, videoSource, liveVideo, videoPassword } = videoData |
53 | 53 | ||
54 | this.videoDetails = video | 54 | this.videoDetails = video |
55 | this.videoEdit = new VideoEdit(this.videoDetails) | 55 | this.videoEdit = new VideoEdit(this.videoDetails, videoPassword) |
56 | 56 | ||
57 | this.userVideoChannels = videoChannels | 57 | this.userVideoChannels = videoChannels |
58 | this.videoCaptions = videoCaptions | 58 | this.videoCaptions = videoCaptions |
@@ -98,11 +98,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
98 | } | 98 | } |
99 | 99 | ||
100 | isWaitTranscodingHidden () { | 100 | isWaitTranscodingHidden () { |
101 | if (this.videoDetails.getFiles().length > 1) { // Already transcoded | 101 | return this.videoDetails.state.id !== VideoState.TO_TRANSCODE |
102 | return true | ||
103 | } | ||
104 | |||
105 | return false | ||
106 | } | 102 | } |
107 | 103 | ||
108 | async update () { | 104 | async update () { |
diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts index 6612d22de..2c99b36a8 100644 --- a/client/src/app/+videos/+video-edit/video-update.resolver.ts +++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts | |||
@@ -4,8 +4,9 @@ import { Injectable } from '@angular/core' | |||
4 | import { ActivatedRouteSnapshot } from '@angular/router' | 4 | import { ActivatedRouteSnapshot } from '@angular/router' |
5 | import { AuthService } from '@app/core' | 5 | import { AuthService } from '@app/core' |
6 | import { listUserChannelsForSelect } from '@app/helpers' | 6 | import { listUserChannelsForSelect } from '@app/helpers' |
7 | import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' | 7 | import { VideoCaptionService, VideoDetails, VideoService, VideoPasswordService } from '@app/shared/shared-main' |
8 | import { LiveVideoService } from '@app/shared/shared-video-live' | 8 | import { LiveVideoService } from '@app/shared/shared-video-live' |
9 | import { VideoPrivacy } from '@shared/models/videos' | ||
9 | 10 | ||
10 | @Injectable() | 11 | @Injectable() |
11 | export class VideoUpdateResolver { | 12 | export class VideoUpdateResolver { |
@@ -13,7 +14,8 @@ export class VideoUpdateResolver { | |||
13 | private videoService: VideoService, | 14 | private videoService: VideoService, |
14 | private liveVideoService: LiveVideoService, | 15 | private liveVideoService: LiveVideoService, |
15 | private authService: AuthService, | 16 | private authService: AuthService, |
16 | private videoCaptionService: VideoCaptionService | 17 | private videoCaptionService: VideoCaptionService, |
18 | private videoPasswordService: VideoPasswordService | ||
17 | ) { | 19 | ) { |
18 | } | 20 | } |
19 | 21 | ||
@@ -21,11 +23,11 @@ export class VideoUpdateResolver { | |||
21 | const uuid: string = route.params['uuid'] | 23 | const uuid: string = route.params['uuid'] |
22 | 24 | ||
23 | return this.videoService.getVideo({ videoId: uuid }) | 25 | return this.videoService.getVideo({ videoId: uuid }) |
24 | .pipe( | 26 | .pipe( |
25 | switchMap(video => forkJoin(this.buildVideoObservables(video))), | 27 | switchMap(video => forkJoin(this.buildVideoObservables(video))), |
26 | map(([ video, videoSource, videoChannels, videoCaptions, liveVideo ]) => | 28 | map(([ video, videoSource, videoChannels, videoCaptions, liveVideo, videoPassword ]) => |
27 | ({ video, videoChannels, videoCaptions, videoSource, liveVideo })) | 29 | ({ video, videoChannels, videoCaptions, videoSource, liveVideo, videoPassword })) |
28 | ) | 30 | ) |
29 | } | 31 | } |
30 | 32 | ||
31 | private buildVideoObservables (video: VideoDetails) { | 33 | private buildVideoObservables (video: VideoDetails) { |
@@ -46,6 +48,10 @@ export class VideoUpdateResolver { | |||
46 | 48 | ||
47 | video.isLive | 49 | video.isLive |
48 | ? this.liveVideoService.getVideoLive(video.id) | 50 | ? this.liveVideoService.getVideoLive(video.id) |
51 | : of(undefined), | ||
52 | |||
53 | video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
54 | ? this.videoPasswordService.getVideoPasswords({ videoUUID: video.uuid }) | ||
49 | : of(undefined) | 55 | : of(undefined) |
50 | ] | 56 | ] |
51 | } | 57 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html index cf32e371a..140a391e9 100644 --- a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <div class="video-actions-rates"> | 1 | <div class="video-actions-rates"> |
2 | <div class="video-actions justify-content-end"> | 2 | <div class="video-actions justify-content-end"> |
3 | <my-video-rate | 3 | <my-video-rate |
4 | [video]="video" [isUserLoggedIn]="isUserLoggedIn" | 4 | [video]="video" [videoPassword]="videoPassword" [isUserLoggedIn]="isUserLoggedIn" |
5 | (rateUpdated)="onRateUpdated($event)" (userRatingLoaded)="onRateUpdated($event)" | 5 | (rateUpdated)="onRateUpdated($event)" (userRatingLoaded)="onRateUpdated($event)" |
6 | ></my-video-rate> | 6 | ></my-video-rate> |
7 | 7 | ||
@@ -20,7 +20,7 @@ | |||
20 | 20 | ||
21 | <div | 21 | <div |
22 | class="action-dropdown" ngbDropdown placement="top" role="button" autoClose="outside" | 22 | class="action-dropdown" ngbDropdown placement="top" role="button" autoClose="outside" |
23 | *ngIf="isUserLoggedIn" (openChange)="addContent.openChange($event)" | 23 | *ngIf="isVideoAddableToPlaylist()" (openChange)="addContent.openChange($event)" |
24 | [ngbTooltip]="tooltipSaveToPlaylist" | 24 | [ngbTooltip]="tooltipSaveToPlaylist" |
25 | placement="bottom auto" | 25 | placement="bottom auto" |
26 | > | 26 | > |
@@ -43,7 +43,7 @@ | |||
43 | <span class="icon-text d-none d-sm-inline" i18n>DOWNLOAD</span> | 43 | <span class="icon-text d-none d-sm-inline" i18n>DOWNLOAD</span> |
44 | </button> | 44 | </button> |
45 | 45 | ||
46 | <my-video-download #videoDownloadModal></my-video-download> | 46 | <my-video-download #videoDownloadModal [videoPassword]="videoPassword"></my-video-download> |
47 | </ng-container> | 47 | </ng-container> |
48 | 48 | ||
49 | <ng-container *ngIf="isUserLoggedIn"> | 49 | <ng-container *ngIf="isUserLoggedIn"> |
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts index 51718827d..e6c0d4de1 100644 --- a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.ts | |||
@@ -5,7 +5,7 @@ import { VideoShareComponent } from '@app/shared/shared-share-modal' | |||
5 | import { SupportModalComponent } from '@app/shared/shared-support-modal' | 5 | import { SupportModalComponent } from '@app/shared/shared-support-modal' |
6 | import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' | 6 | import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' |
7 | import { VideoPlaylist } from '@app/shared/shared-video-playlist' | 7 | import { VideoPlaylist } from '@app/shared/shared-video-playlist' |
8 | import { UserVideoRateType, VideoCaption } from '@shared/models/videos' | 8 | import { UserVideoRateType, VideoCaption, VideoPrivacy } from '@shared/models/videos' |
9 | 9 | ||
10 | @Component({ | 10 | @Component({ |
11 | selector: 'my-action-buttons', | 11 | selector: 'my-action-buttons', |
@@ -18,10 +18,12 @@ export class ActionButtonsComponent implements OnInit, OnChanges { | |||
18 | @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent | 18 | @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent |
19 | 19 | ||
20 | @Input() video: VideoDetails | 20 | @Input() video: VideoDetails |
21 | @Input() videoPassword: string | ||
21 | @Input() videoCaptions: VideoCaption[] | 22 | @Input() videoCaptions: VideoCaption[] |
22 | @Input() playlist: VideoPlaylist | 23 | @Input() playlist: VideoPlaylist |
23 | 24 | ||
24 | @Input() isUserLoggedIn: boolean | 25 | @Input() isUserLoggedIn: boolean |
26 | @Input() isUserOwner: boolean | ||
25 | 27 | ||
26 | @Input() currentTime: number | 28 | @Input() currentTime: number |
27 | @Input() currentPlaylistPosition: number | 29 | @Input() currentPlaylistPosition: number |
@@ -92,4 +94,14 @@ export class ActionButtonsComponent implements OnInit, OnChanges { | |||
92 | private setVideoLikesBarTooltipText () { | 94 | private setVideoLikesBarTooltipText () { |
93 | this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes` | 95 | this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes` |
94 | } | 96 | } |
97 | |||
98 | isVideoAddableToPlaylist () { | ||
99 | const isPasswordProtected = this.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
100 | |||
101 | if (!this.isUserLoggedIn) return false | ||
102 | |||
103 | if (isPasswordProtected) return this.isUserOwner | ||
104 | |||
105 | return true | ||
106 | } | ||
95 | } | 107 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts b/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts index d0c138834..11966ce34 100644 --- a/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts +++ b/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts | |||
@@ -12,6 +12,7 @@ import { UserVideoRateType } from '@shared/models' | |||
12 | }) | 12 | }) |
13 | export class VideoRateComponent implements OnInit, OnChanges, OnDestroy { | 13 | export class VideoRateComponent implements OnInit, OnChanges, OnDestroy { |
14 | @Input() video: VideoDetails | 14 | @Input() video: VideoDetails |
15 | @Input() videoPassword: string | ||
15 | @Input() isUserLoggedIn: boolean | 16 | @Input() isUserLoggedIn: boolean |
16 | 17 | ||
17 | @Output() userRatingLoaded = new EventEmitter<UserVideoRateType>() | 18 | @Output() userRatingLoaded = new EventEmitter<UserVideoRateType>() |
@@ -103,13 +104,13 @@ export class VideoRateComponent implements OnInit, OnChanges, OnDestroy { | |||
103 | } | 104 | } |
104 | 105 | ||
105 | private setRating (nextRating: UserVideoRateType) { | 106 | private setRating (nextRating: UserVideoRateType) { |
106 | const ratingMethods: { [id in UserVideoRateType]: (id: string) => Observable<any> } = { | 107 | const ratingMethods: { [id in UserVideoRateType]: (id: string, videoPassword: string) => Observable<any> } = { |
107 | like: this.videoService.setVideoLike, | 108 | like: this.videoService.setVideoLike, |
108 | dislike: this.videoService.setVideoDislike, | 109 | dislike: this.videoService.setVideoDislike, |
109 | none: this.videoService.unsetVideoLike | 110 | none: this.videoService.unsetVideoLike |
110 | } | 111 | } |
111 | 112 | ||
112 | ratingMethods[nextRating].call(this.videoService, this.video.uuid) | 113 | ratingMethods[nextRating].call(this.videoService, this.video.uuid, this.videoPassword) |
113 | .subscribe({ | 114 | .subscribe({ |
114 | next: () => { | 115 | next: () => { |
115 | // Update the video like attribute | 116 | // Update the video like attribute |
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts index 033097084..1d9e10d0a 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts | |||
@@ -29,6 +29,7 @@ import { VideoCommentCreate } from '@shared/models' | |||
29 | export class VideoCommentAddComponent extends FormReactive implements OnChanges, OnInit { | 29 | export class VideoCommentAddComponent extends FormReactive implements OnChanges, OnInit { |
30 | @Input() user: User | 30 | @Input() user: User |
31 | @Input() video: Video | 31 | @Input() video: Video |
32 | @Input() videoPassword: string | ||
32 | @Input() parentComment?: VideoComment | 33 | @Input() parentComment?: VideoComment |
33 | @Input() parentComments?: VideoComment[] | 34 | @Input() parentComments?: VideoComment[] |
34 | @Input() focusOnInit = false | 35 | @Input() focusOnInit = false |
@@ -176,12 +177,17 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges, | |||
176 | 177 | ||
177 | private addCommentReply (commentCreate: VideoCommentCreate) { | 178 | private addCommentReply (commentCreate: VideoCommentCreate) { |
178 | return this.videoCommentService | 179 | return this.videoCommentService |
179 | .addCommentReply(this.video.uuid, this.parentComment.id, commentCreate) | 180 | .addCommentReply({ |
181 | videoId: this.video.uuid, | ||
182 | inReplyToCommentId: this.parentComment.id, | ||
183 | comment: commentCreate, | ||
184 | videoPassword: this.videoPassword | ||
185 | }) | ||
180 | } | 186 | } |
181 | 187 | ||
182 | private addCommentThread (commentCreate: VideoCommentCreate) { | 188 | private addCommentThread (commentCreate: VideoCommentCreate) { |
183 | return this.videoCommentService | 189 | return this.videoCommentService |
184 | .addCommentThread(this.video.uuid, commentCreate) | 190 | .addCommentThread(this.video.uuid, commentCreate, this.videoPassword) |
185 | } | 191 | } |
186 | 192 | ||
187 | private initTextValue () { | 193 | private initTextValue () { |
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html index 91bd8309c..80ea22a20 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html | |||
@@ -62,6 +62,7 @@ | |||
62 | *ngIf="!comment.isDeleted && inReplyToCommentId === comment.id" | 62 | *ngIf="!comment.isDeleted && inReplyToCommentId === comment.id" |
63 | [user]="user" | 63 | [user]="user" |
64 | [video]="video" | 64 | [video]="video" |
65 | [videoPassword]="videoPassword" | ||
65 | [parentComment]="comment" | 66 | [parentComment]="comment" |
66 | [parentComments]="newParentComments" | 67 | [parentComments]="newParentComments" |
67 | [focusOnInit]="true" | 68 | [focusOnInit]="true" |
@@ -75,6 +76,7 @@ | |||
75 | <my-video-comment | 76 | <my-video-comment |
76 | [comment]="commentChild.comment" | 77 | [comment]="commentChild.comment" |
77 | [video]="video" | 78 | [video]="video" |
79 | [videoPassword]="videoPassword" | ||
78 | [inReplyToCommentId]="inReplyToCommentId" | 80 | [inReplyToCommentId]="inReplyToCommentId" |
79 | [commentTree]="commentChild" | 81 | [commentTree]="commentChild" |
80 | [parentComments]="newParentComments" | 82 | [parentComments]="newParentComments" |
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts index 191ec4a28..4c85df657 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts | |||
@@ -16,6 +16,7 @@ export class VideoCommentComponent implements OnInit, OnChanges { | |||
16 | @ViewChild('commentReportModal') commentReportModal: CommentReportComponent | 16 | @ViewChild('commentReportModal') commentReportModal: CommentReportComponent |
17 | 17 | ||
18 | @Input() video: Video | 18 | @Input() video: Video |
19 | @Input() videoPassword: string | ||
19 | @Input() comment: VideoComment | 20 | @Input() comment: VideoComment |
20 | @Input() parentComments: VideoComment[] = [] | 21 | @Input() parentComments: VideoComment[] = [] |
21 | @Input() commentTree: VideoCommentThreadTree | 22 | @Input() commentTree: VideoCommentThreadTree |
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html index a003a10eb..0932d2b7f 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html | |||
@@ -20,6 +20,7 @@ | |||
20 | <ng-template [ngIf]="video.commentsEnabled === true"> | 20 | <ng-template [ngIf]="video.commentsEnabled === true"> |
21 | <my-video-comment-add | 21 | <my-video-comment-add |
22 | [video]="video" | 22 | [video]="video" |
23 | [videoPassword]="videoPassword" | ||
23 | [user]="user" | 24 | [user]="user" |
24 | (commentCreated)="onCommentThreadCreated($event)" | 25 | (commentCreated)="onCommentThreadCreated($event)" |
25 | [textValue]="commentThreadRedraftValue" | 26 | [textValue]="commentThreadRedraftValue" |
@@ -34,6 +35,7 @@ | |||
34 | *ngIf="highlightedThread" | 35 | *ngIf="highlightedThread" |
35 | [comment]="highlightedThread" | 36 | [comment]="highlightedThread" |
36 | [video]="video" | 37 | [video]="video" |
38 | [videoPassword]="videoPassword" | ||
37 | [inReplyToCommentId]="inReplyToCommentId" | 39 | [inReplyToCommentId]="inReplyToCommentId" |
38 | [commentTree]="threadComments[highlightedThread.id]" | 40 | [commentTree]="threadComments[highlightedThread.id]" |
39 | [highlightedComment]="true" | 41 | [highlightedComment]="true" |
@@ -53,6 +55,7 @@ | |||
53 | *ngIf="!highlightedThread || comment.id !== highlightedThread.id" | 55 | *ngIf="!highlightedThread || comment.id !== highlightedThread.id" |
54 | [comment]="comment" | 56 | [comment]="comment" |
55 | [video]="video" | 57 | [video]="video" |
58 | [videoPassword]="videoPassword" | ||
56 | [inReplyToCommentId]="inReplyToCommentId" | 59 | [inReplyToCommentId]="inReplyToCommentId" |
57 | [commentTree]="threadComments[comment.id]" | 60 | [commentTree]="threadComments[comment.id]" |
58 | [firstInThread]="i + 1 !== comments.length" | 61 | [firstInThread]="i + 1 !== comments.length" |
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts index 96bdb28c9..848936f91 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts | |||
@@ -15,6 +15,7 @@ import { PeerTubeProblemDocument, ServerErrorCode } from '@shared/models' | |||
15 | export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | 15 | export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { |
16 | @ViewChild('commentHighlightBlock') commentHighlightBlock: ElementRef | 16 | @ViewChild('commentHighlightBlock') commentHighlightBlock: ElementRef |
17 | @Input() video: VideoDetails | 17 | @Input() video: VideoDetails |
18 | @Input() videoPassword: string | ||
18 | @Input() user: User | 19 | @Input() user: User |
19 | 20 | ||
20 | @Output() timestampClicked = new EventEmitter<number>() | 21 | @Output() timestampClicked = new EventEmitter<number>() |
@@ -80,7 +81,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | |||
80 | 81 | ||
81 | const params = { | 82 | const params = { |
82 | videoId: this.video.uuid, | 83 | videoId: this.video.uuid, |
83 | threadId: commentId | 84 | threadId: commentId, |
85 | videoPassword: this.videoPassword | ||
84 | } | 86 | } |
85 | 87 | ||
86 | const obs = this.hooks.wrapObsFun( | 88 | const obs = this.hooks.wrapObsFun( |
@@ -119,6 +121,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | |||
119 | loadMoreThreads () { | 121 | loadMoreThreads () { |
120 | const params = { | 122 | const params = { |
121 | videoId: this.video.uuid, | 123 | videoId: this.video.uuid, |
124 | videoPassword: this.videoPassword, | ||
122 | componentPagination: this.componentPagination, | 125 | componentPagination: this.componentPagination, |
123 | sort: this.sort | 126 | sort: this.sort |
124 | } | 127 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html index 79b83811d..45e222743 100644 --- a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html +++ b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html | |||
@@ -42,3 +42,7 @@ | |||
42 | <div class="blocked-label" i18n>This video is blocked.</div> | 42 | <div class="blocked-label" i18n>This video is blocked.</div> |
43 | {{ video.blacklistedReason }} | 43 | {{ video.blacklistedReason }} |
44 | </div> | 44 | </div> |
45 | |||
46 | <div i18n class="alert alert-warning" *ngIf="video?.canAccessPasswordProtectedVideoWithoutPassword(user)"> | ||
47 | This video is password protected. | ||
48 | </div> | ||
diff --git a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.ts b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.ts index ba79fabc8..8781ead7e 100644 --- a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.ts +++ b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { AuthUser } from '@app/core' | ||
2 | import { VideoDetails } from '@app/shared/shared-main' | 3 | import { VideoDetails } from '@app/shared/shared-main' |
3 | import { VideoState } from '@shared/models' | 4 | import { VideoPrivacy, VideoState } from '@shared/models' |
4 | 5 | ||
5 | @Component({ | 6 | @Component({ |
6 | selector: 'my-video-alert', | 7 | selector: 'my-video-alert', |
@@ -8,6 +9,7 @@ import { VideoState } from '@shared/models' | |||
8 | styleUrls: [ './video-alert.component.scss' ] | 9 | styleUrls: [ './video-alert.component.scss' ] |
9 | }) | 10 | }) |
10 | export class VideoAlertComponent { | 11 | export class VideoAlertComponent { |
12 | @Input() user: AuthUser | ||
11 | @Input() video: VideoDetails | 13 | @Input() video: VideoDetails |
12 | @Input() noPlaylistVideoFound: boolean | 14 | @Input() noPlaylistVideoFound: boolean |
13 | 15 | ||
@@ -46,4 +48,8 @@ export class VideoAlertComponent { | |||
46 | isLiveEnded () { | 48 | isLiveEnded () { |
47 | return this.video?.state.id === VideoState.LIVE_ENDED | 49 | return this.video?.state.id === VideoState.LIVE_ENDED |
48 | } | 50 | } |
51 | |||
52 | isVideoPasswordProtected () { | ||
53 | return this.video?.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
54 | } | ||
49 | } | 55 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts index ec85db0ff..97d71a510 100644 --- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts +++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts | |||
@@ -152,12 +152,24 @@ export class VideoWatchPlaylistComponent { | |||
152 | this.onPlaylistVideosNearOfBottom(position) | 152 | this.onPlaylistVideosNearOfBottom(position) |
153 | } | 153 | } |
154 | 154 | ||
155 | // --------------------------------------------------------------------------- | ||
156 | |||
155 | hasPreviousVideo () { | 157 | hasPreviousVideo () { |
156 | return !!this.findPlaylistVideo(this.currentPlaylistPosition - 1, 'previous') | 158 | return !!this.getPreviousVideo() |
159 | } | ||
160 | |||
161 | getPreviousVideo () { | ||
162 | return this.findPlaylistVideo(this.currentPlaylistPosition - 1, 'previous') | ||
157 | } | 163 | } |
158 | 164 | ||
165 | // --------------------------------------------------------------------------- | ||
166 | |||
159 | hasNextVideo () { | 167 | hasNextVideo () { |
160 | return !!this.findPlaylistVideo(this.currentPlaylistPosition + 1, 'next') | 168 | return !!this.getNextVideo() |
169 | } | ||
170 | |||
171 | getNextVideo () { | ||
172 | return this.findPlaylistVideo(this.currentPlaylistPosition + 1, 'next') | ||
161 | } | 173 | } |
162 | 174 | ||
163 | navigateToPreviousPlaylistVideo () { | 175 | navigateToPreviousPlaylistVideo () { |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html index 461891779..294ff4b3a 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.html +++ b/client/src/app/+videos/+video-watch/video-watch.component.html | |||
@@ -8,7 +8,7 @@ | |||
8 | </div> | 8 | </div> |
9 | 9 | ||
10 | <div id="videojs-wrapper"> | 10 | <div id="videojs-wrapper"> |
11 | <img class="placeholder-image" *ngIf="playerPlaceholderImgSrc" [src]="playerPlaceholderImgSrc" alt="Placeholder image" i18n-alt> | 11 | <video #playerElement class="video-js vjs-peertube-skin" playsinline="true"></video> |
12 | </div> | 12 | </div> |
13 | 13 | ||
14 | <my-video-watch-playlist | 14 | <my-video-watch-playlist |
@@ -19,7 +19,7 @@ | |||
19 | <my-plugin-placeholder pluginId="player-next"></my-plugin-placeholder> | 19 | <my-plugin-placeholder pluginId="player-next"></my-plugin-placeholder> |
20 | </div> | 20 | </div> |
21 | 21 | ||
22 | <my-video-alert [video]="video" [noPlaylistVideoFound]="noPlaylistVideoFound"></my-video-alert> | 22 | <my-video-alert [video]="video" [user]="user" [noPlaylistVideoFound]="noPlaylistVideoFound"></my-video-alert> |
23 | 23 | ||
24 | <!-- Video information --> | 24 | <!-- Video information --> |
25 | <div *ngIf="video" class="margin-content video-bottom"> | 25 | <div *ngIf="video" class="margin-content video-bottom"> |
@@ -51,8 +51,8 @@ | |||
51 | </div> | 51 | </div> |
52 | 52 | ||
53 | <my-action-buttons | 53 | <my-action-buttons |
54 | [video]="video" [isUserLoggedIn]="isUserLoggedIn()" [videoCaptions]="videoCaptions" [playlist]="playlist" | 54 | [video]="video" [videoPassword]="videoPassword" [isUserLoggedIn]="isUserLoggedIn()" [isUserOwner]="isUserOwner()" [videoCaptions]="videoCaptions" |
55 | [currentTime]="getCurrentTime()" [currentPlaylistPosition]="getCurrentPlaylistPosition()" | 55 | [playlist]="playlist" [currentTime]="getCurrentTime()" [currentPlaylistPosition]="getCurrentPlaylistPosition()" |
56 | ></my-action-buttons> | 56 | ></my-action-buttons> |
57 | </div> | 57 | </div> |
58 | </div> | 58 | </div> |
@@ -92,6 +92,7 @@ | |||
92 | <my-video-comments | 92 | <my-video-comments |
93 | class="border-top" | 93 | class="border-top" |
94 | [video]="video" | 94 | [video]="video" |
95 | [videoPassword]="videoPassword" | ||
95 | [user]="user" | 96 | [user]="user" |
96 | (timestampClicked)="handleTimestampClicked($event)" | 97 | (timestampClicked)="handleTimestampClicked($event)" |
97 | ></my-video-comments> | 98 | ></my-video-comments> |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 19ad97d42..aebec52fb 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
2 | import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs' | 2 | import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs' |
3 | import { VideoJsPlayer } from 'video.js' | ||
4 | import { PlatformLocation } from '@angular/common' | 3 | import { PlatformLocation } from '@angular/common' |
5 | import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' | 4 | import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' |
6 | import { ActivatedRoute, Router } from '@angular/router' | 5 | import { ActivatedRoute, Router } from '@angular/router' |
@@ -19,13 +18,13 @@ import { | |||
19 | UserService | 18 | UserService |
20 | } from '@app/core' | 19 | } from '@app/core' |
21 | import { HooksService } from '@app/core/plugins/hooks.service' | 20 | import { HooksService } from '@app/core/plugins/hooks.service' |
22 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' | 21 | import { isXPercentInViewport, scrollToTop, toBoolean } from '@app/helpers' |
23 | import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' | 22 | import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' |
24 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 23 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
25 | import { LiveVideoService } from '@app/shared/shared-video-live' | 24 | import { LiveVideoService } from '@app/shared/shared-video-live' |
26 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 25 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
27 | import { logger } from '@root-helpers/logger' | 26 | import { logger } from '@root-helpers/logger' |
28 | import { isP2PEnabled, videoRequiresAuth } from '@root-helpers/video' | 27 | import { isP2PEnabled, videoRequiresFileToken, videoRequiresUserAuth } from '@root-helpers/video' |
29 | import { timeToInt } from '@shared/core-utils' | 28 | import { timeToInt } from '@shared/core-utils' |
30 | import { | 29 | import { |
31 | HTMLServerConfig, | 30 | HTMLServerConfig, |
@@ -33,15 +32,16 @@ import { | |||
33 | LiveVideo, | 32 | LiveVideo, |
34 | PeerTubeProblemDocument, | 33 | PeerTubeProblemDocument, |
35 | ServerErrorCode, | 34 | ServerErrorCode, |
35 | Storyboard, | ||
36 | VideoCaption, | 36 | VideoCaption, |
37 | VideoPrivacy, | 37 | VideoPrivacy, |
38 | VideoState | 38 | VideoState |
39 | } from '@shared/models' | 39 | } from '@shared/models' |
40 | import { | 40 | import { |
41 | CustomizationOptions, | 41 | HLSOptions, |
42 | P2PMediaLoaderOptions, | 42 | PeerTubePlayer, |
43 | PeertubePlayerManager, | 43 | PeerTubePlayerContructorOptions, |
44 | PeertubePlayerManagerOptions, | 44 | PeerTubePlayerLoadOptions, |
45 | PlayerMode, | 45 | PlayerMode, |
46 | videojs | 46 | videojs |
47 | } from '../../../assets/player' | 47 | } from '../../../assets/player' |
@@ -49,7 +49,24 @@ import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from | |||
49 | import { environment } from '../../../environments/environment' | 49 | import { environment } from '../../../environments/environment' |
50 | import { VideoWatchPlaylistComponent } from './shared' | 50 | import { VideoWatchPlaylistComponent } from './shared' |
51 | 51 | ||
52 | type URLOptions = CustomizationOptions & { playerMode: PlayerMode } | 52 | type URLOptions = { |
53 | playerMode: PlayerMode | ||
54 | |||
55 | startTime: number | string | ||
56 | stopTime: number | string | ||
57 | |||
58 | controls?: boolean | ||
59 | controlBar?: boolean | ||
60 | |||
61 | muted?: boolean | ||
62 | loop?: boolean | ||
63 | subtitle?: string | ||
64 | resume?: string | ||
65 | |||
66 | peertubeLink: boolean | ||
67 | |||
68 | playbackRate?: number | string | ||
69 | } | ||
53 | 70 | ||
54 | @Component({ | 71 | @Component({ |
55 | selector: 'my-video-watch', | 72 | selector: 'my-video-watch', |
@@ -59,15 +76,16 @@ type URLOptions = CustomizationOptions & { playerMode: PlayerMode } | |||
59 | export class VideoWatchComponent implements OnInit, OnDestroy { | 76 | export class VideoWatchComponent implements OnInit, OnDestroy { |
60 | @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent | 77 | @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent |
61 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent | 78 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent |
79 | @ViewChild('playerElement') playerElement: ElementRef<HTMLVideoElement> | ||
62 | 80 | ||
63 | player: VideoJsPlayer | 81 | peertubePlayer: PeerTubePlayer |
64 | playerElement: HTMLVideoElement | ||
65 | playerPlaceholderImgSrc: string | ||
66 | theaterEnabled = false | 82 | theaterEnabled = false |
67 | 83 | ||
68 | video: VideoDetails = null | 84 | video: VideoDetails = null |
69 | videoCaptions: VideoCaption[] = [] | 85 | videoCaptions: VideoCaption[] = [] |
70 | liveVideo: LiveVideo | 86 | liveVideo: LiveVideo |
87 | videoPassword: string | ||
88 | storyboards: Storyboard[] = [] | ||
71 | 89 | ||
72 | playlistPosition: number | 90 | playlistPosition: number |
73 | playlist: VideoPlaylist = null | 91 | playlist: VideoPlaylist = null |
@@ -75,8 +93,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
75 | remoteServerDown = false | 93 | remoteServerDown = false |
76 | noPlaylistVideoFound = false | 94 | noPlaylistVideoFound = false |
77 | 95 | ||
78 | private nextVideoUUID = '' | 96 | private nextRecommendedVideoUUID = '' |
79 | private nextVideoTitle = '' | 97 | private nextRecommendedVideoTitle = '' |
80 | 98 | ||
81 | private videoFileToken: string | 99 | private videoFileToken: string |
82 | 100 | ||
@@ -127,11 +145,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
127 | return this.userService.getAnonymousUser() | 145 | return this.userService.getAnonymousUser() |
128 | } | 146 | } |
129 | 147 | ||
130 | ngOnInit () { | 148 | async ngOnInit () { |
131 | this.serverConfig = this.serverService.getHTMLConfig() | 149 | this.serverConfig = this.serverService.getHTMLConfig() |
132 | 150 | ||
133 | PeertubePlayerManager.initState() | ||
134 | |||
135 | this.loadRouteParams() | 151 | this.loadRouteParams() |
136 | this.loadRouteQuery() | 152 | this.loadRouteQuery() |
137 | 153 | ||
@@ -140,10 +156,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
140 | this.hooks.runAction('action:video-watch.init', 'video-watch') | 156 | this.hooks.runAction('action:video-watch.init', 'video-watch') |
141 | 157 | ||
142 | setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI | 158 | setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI |
159 | |||
160 | const constructorOptions = await this.hooks.wrapFun( | ||
161 | this.buildPeerTubePlayerConstructorOptions.bind(this), | ||
162 | { urlOptions: this.getUrlOptions() }, | ||
163 | 'video-watch', | ||
164 | 'filter:internal.video-watch.player.build-options.params', | ||
165 | 'filter:internal.video-watch.player.build-options.result' | ||
166 | ) | ||
167 | |||
168 | this.peertubePlayer = new PeerTubePlayer(constructorOptions) | ||
143 | } | 169 | } |
144 | 170 | ||
145 | ngOnDestroy () { | 171 | ngOnDestroy () { |
146 | this.flushPlayer() | 172 | if (this.peertubePlayer) this.peertubePlayer.destroy() |
147 | 173 | ||
148 | // Unsubscribe subscriptions | 174 | // Unsubscribe subscriptions |
149 | if (this.paramsSub) this.paramsSub.unsubscribe() | 175 | if (this.paramsSub) this.paramsSub.unsubscribe() |
@@ -168,14 +194,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
168 | 194 | ||
169 | // The recommended videos's first element should be the next video | 195 | // The recommended videos's first element should be the next video |
170 | const video = videos[0] | 196 | const video = videos[0] |
171 | this.nextVideoUUID = video.uuid | 197 | this.nextRecommendedVideoUUID = video.uuid |
172 | this.nextVideoTitle = video.name | 198 | this.nextRecommendedVideoTitle = video.name |
173 | } | 199 | } |
174 | 200 | ||
175 | handleTimestampClicked (timestamp: number) { | 201 | handleTimestampClicked (timestamp: number) { |
176 | if (!this.player || this.video.isLive) return | 202 | if (!this.peertubePlayer || this.video.isLive) return |
177 | 203 | ||
178 | this.player.currentTime(timestamp) | 204 | this.peertubePlayer.getPlayer().currentTime(timestamp) |
179 | scrollToTop() | 205 | scrollToTop() |
180 | } | 206 | } |
181 | 207 | ||
@@ -191,6 +217,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
191 | return this.authService.isLoggedIn() | 217 | return this.authService.isLoggedIn() |
192 | } | 218 | } |
193 | 219 | ||
220 | isUserOwner () { | ||
221 | return this.video.isLocal === true && this.video.account.name === this.user?.username | ||
222 | } | ||
223 | |||
194 | isVideoBlur (video: Video) { | 224 | isVideoBlur (video: Video) { |
195 | return video.isVideoNSFWForUser(this.user, this.serverConfig) | 225 | return video.isVideoNSFWForUser(this.user, this.serverConfig) |
196 | } | 226 | } |
@@ -236,25 +266,24 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
236 | this.videoWatchPlaylist.updatePlaylistIndex(this.playlistPosition) | 266 | this.videoWatchPlaylist.updatePlaylistIndex(this.playlistPosition) |
237 | 267 | ||
238 | const start = queryParams['start'] | 268 | const start = queryParams['start'] |
239 | if (this.player && start) this.player.currentTime(parseInt(start, 10)) | 269 | if (this.peertubePlayer && start) this.peertubePlayer.getPlayer().currentTime(parseInt(start, 10)) |
240 | }) | 270 | }) |
241 | } | 271 | } |
242 | 272 | ||
243 | private loadVideo (options: { | 273 | private loadVideo (options: { |
244 | videoId: string | 274 | videoId: string |
245 | forceAutoplay: boolean | 275 | forceAutoplay: boolean |
276 | videoPassword?: string | ||
246 | }) { | 277 | }) { |
247 | const { videoId, forceAutoplay } = options | 278 | const { videoId, forceAutoplay, videoPassword } = options |
248 | 279 | ||
249 | if (this.isSameElement(this.video, videoId)) return | 280 | if (this.isSameElement(this.video, videoId)) return |
250 | 281 | ||
251 | if (this.player) this.player.pause() | ||
252 | |||
253 | this.video = undefined | 282 | this.video = undefined |
254 | 283 | ||
255 | const videoObs = this.hooks.wrapObsFun( | 284 | const videoObs = this.hooks.wrapObsFun( |
256 | this.videoService.getVideo.bind(this.videoService), | 285 | this.videoService.getVideo.bind(this.videoService), |
257 | { videoId }, | 286 | { videoId, videoPassword }, |
258 | 'video-watch', | 287 | 'video-watch', |
259 | 'filter:api.video-watch.video.get.params', | 288 | 'filter:api.video-watch.video.get.params', |
260 | 'filter:api.video-watch.video.get.result' | 289 | 'filter:api.video-watch.video.get.result' |
@@ -269,48 +298,44 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
269 | }), | 298 | }), |
270 | 299 | ||
271 | switchMap(({ video, live }) => { | 300 | switchMap(({ video, live }) => { |
272 | if (!videoRequiresAuth(video)) return of({ video, live, videoFileToken: undefined }) | 301 | if (!videoRequiresFileToken(video)) return of({ video, live, videoFileToken: undefined }) |
273 | 302 | ||
274 | return this.videoFileTokenService.getVideoFileToken(video.uuid) | 303 | return this.videoFileTokenService.getVideoFileToken({ videoUUID: video.uuid, videoPassword }) |
275 | .pipe(map(({ token }) => ({ video, live, videoFileToken: token }))) | 304 | .pipe(map(({ token }) => ({ video, live, videoFileToken: token }))) |
276 | }) | 305 | }) |
277 | ) | 306 | ) |
278 | 307 | ||
279 | forkJoin([ | 308 | forkJoin([ |
280 | videoAndLiveObs, | 309 | videoAndLiveObs, |
281 | this.videoCaptionService.listCaptions(videoId), | 310 | this.videoCaptionService.listCaptions(videoId, videoPassword), |
311 | this.videoService.getStoryboards(videoId, videoPassword), | ||
282 | this.userService.getAnonymousOrLoggedUser() | 312 | this.userService.getAnonymousOrLoggedUser() |
283 | ]).subscribe({ | 313 | ]).subscribe({ |
284 | next: ([ { video, live, videoFileToken }, captionsResult, loggedInOrAnonymousUser ]) => { | 314 | next: ([ { video, live, videoFileToken }, captionsResult, storyboards, loggedInOrAnonymousUser ]) => { |
285 | const queryParams = this.route.snapshot.queryParams | ||
286 | |||
287 | const urlOptions = { | ||
288 | resume: queryParams.resume, | ||
289 | |||
290 | startTime: queryParams.start, | ||
291 | stopTime: queryParams.stop, | ||
292 | |||
293 | muted: queryParams.muted, | ||
294 | loop: queryParams.loop, | ||
295 | subtitle: queryParams.subtitle, | ||
296 | |||
297 | playerMode: queryParams.mode, | ||
298 | playbackRate: queryParams.playbackRate, | ||
299 | peertubeLink: false | ||
300 | } | ||
301 | |||
302 | this.onVideoFetched({ | 315 | this.onVideoFetched({ |
303 | video, | 316 | video, |
304 | live, | 317 | live, |
305 | videoCaptions: captionsResult.data, | 318 | videoCaptions: captionsResult.data, |
319 | storyboards, | ||
306 | videoFileToken, | 320 | videoFileToken, |
321 | videoPassword, | ||
307 | loggedInOrAnonymousUser, | 322 | loggedInOrAnonymousUser, |
308 | urlOptions, | ||
309 | forceAutoplay | 323 | forceAutoplay |
310 | }).catch(err => this.handleGlobalError(err)) | 324 | }).catch(err => { |
325 | this.handleGlobalError(err) | ||
326 | }) | ||
311 | }, | 327 | }, |
328 | error: async err => { | ||
329 | if (err.body.code === ServerErrorCode.VIDEO_REQUIRES_PASSWORD || err.body.code === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) { | ||
330 | const { confirmed, password } = await this.handleVideoPasswordError(err) | ||
312 | 331 | ||
313 | error: err => this.handleRequestError(err) | 332 | if (confirmed === false) return this.location.back() |
333 | |||
334 | this.loadVideo({ ...options, videoPassword: password }) | ||
335 | } else { | ||
336 | this.handleRequestError(err) | ||
337 | } | ||
338 | } | ||
314 | }) | 339 | }) |
315 | } | 340 | } |
316 | 341 | ||
@@ -364,28 +389,47 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
364 | const errorMessage: string = typeof err === 'string' ? err : err.message | 389 | const errorMessage: string = typeof err === 'string' ? err : err.message |
365 | if (!errorMessage) return | 390 | if (!errorMessage) return |
366 | 391 | ||
367 | // Display a message in the video player instead of a notification | 392 | this.notifier.error(errorMessage) |
368 | if (errorMessage.includes('from xs param')) { | 393 | } |
369 | this.flushPlayer() | ||
370 | this.remoteServerDown = true | ||
371 | 394 | ||
372 | return | 395 | private handleVideoPasswordError (err: any) { |
396 | let isIncorrectPassword: boolean | ||
397 | |||
398 | if (err.body.code === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) { | ||
399 | isIncorrectPassword = false | ||
400 | } else if (err.body.code === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) { | ||
401 | this.videoPassword = undefined | ||
402 | isIncorrectPassword = true | ||
373 | } | 403 | } |
374 | 404 | ||
375 | this.notifier.error(errorMessage) | 405 | return this.confirmService.confirmWithPassword({ |
406 | message: $localize`You need a password to watch this video`, | ||
407 | title: $localize`This video is password protected`, | ||
408 | errorMessage: isIncorrectPassword ? $localize`Incorrect password, please enter a correct password` : '' | ||
409 | }) | ||
376 | } | 410 | } |
377 | 411 | ||
378 | private async onVideoFetched (options: { | 412 | private async onVideoFetched (options: { |
379 | video: VideoDetails | 413 | video: VideoDetails |
380 | live: LiveVideo | 414 | live: LiveVideo |
381 | videoCaptions: VideoCaption[] | 415 | videoCaptions: VideoCaption[] |
416 | storyboards: Storyboard[] | ||
382 | videoFileToken: string | 417 | videoFileToken: string |
418 | videoPassword: string | ||
383 | 419 | ||
384 | urlOptions: URLOptions | ||
385 | loggedInOrAnonymousUser: User | 420 | loggedInOrAnonymousUser: User |
386 | forceAutoplay: boolean | 421 | forceAutoplay: boolean |
387 | }) { | 422 | }) { |
388 | const { video, live, videoCaptions, urlOptions, videoFileToken, loggedInOrAnonymousUser, forceAutoplay } = options | 423 | const { |
424 | video, | ||
425 | live, | ||
426 | videoCaptions, | ||
427 | storyboards, | ||
428 | videoFileToken, | ||
429 | videoPassword, | ||
430 | loggedInOrAnonymousUser, | ||
431 | forceAutoplay | ||
432 | } = options | ||
389 | 433 | ||
390 | this.subscribeToLiveEventsIfNeeded(this.video, video) | 434 | this.subscribeToLiveEventsIfNeeded(this.video, video) |
391 | 435 | ||
@@ -393,9 +437,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
393 | this.videoCaptions = videoCaptions | 437 | this.videoCaptions = videoCaptions |
394 | this.liveVideo = live | 438 | this.liveVideo = live |
395 | this.videoFileToken = videoFileToken | 439 | this.videoFileToken = videoFileToken |
440 | this.videoPassword = videoPassword | ||
441 | this.storyboards = storyboards | ||
396 | 442 | ||
397 | // Re init attributes | 443 | // Re init attributes |
398 | this.playerPlaceholderImgSrc = undefined | ||
399 | this.remoteServerDown = false | 444 | this.remoteServerDown = false |
400 | this.currentTime = undefined | 445 | this.currentTime = undefined |
401 | 446 | ||
@@ -409,7 +454,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
409 | 454 | ||
410 | this.buildHotkeysHelp(video) | 455 | this.buildHotkeysHelp(video) |
411 | 456 | ||
412 | this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) | 457 | this.loadPlayer({ loggedInOrAnonymousUser, forceAutoplay }) |
413 | .catch(err => logger.error('Cannot build the player', err)) | 458 | .catch(err => logger.error('Cannot build the player', err)) |
414 | 459 | ||
415 | this.setOpenGraphTags() | 460 | this.setOpenGraphTags() |
@@ -422,114 +467,70 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
422 | this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions) | 467 | this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions) |
423 | } | 468 | } |
424 | 469 | ||
425 | private async buildPlayer (options: { | 470 | private async loadPlayer (options: { |
426 | urlOptions: URLOptions | ||
427 | loggedInOrAnonymousUser: User | 471 | loggedInOrAnonymousUser: User |
428 | forceAutoplay: boolean | 472 | forceAutoplay: boolean |
429 | }) { | 473 | }) { |
430 | const { urlOptions, loggedInOrAnonymousUser, forceAutoplay } = options | 474 | const { loggedInOrAnonymousUser, forceAutoplay } = options |
431 | |||
432 | // Flush old player if needed | ||
433 | this.flushPlayer() | ||
434 | 475 | ||
435 | const videoState = this.video.state.id | 476 | const videoState = this.video.state.id |
436 | if (videoState === VideoState.LIVE_ENDED || videoState === VideoState.WAITING_FOR_LIVE) { | 477 | if (videoState === VideoState.LIVE_ENDED || videoState === VideoState.WAITING_FOR_LIVE) { |
437 | this.playerPlaceholderImgSrc = this.video.previewPath | 478 | this.updatePlayerOnNoLive() |
438 | return | 479 | return |
439 | } | 480 | } |
440 | 481 | ||
441 | // Build video element, because videojs removes it on dispose | 482 | this.peertubePlayer?.enable() |
442 | const playerElementWrapper = this.elementRef.nativeElement.querySelector('#videojs-wrapper') | ||
443 | this.playerElement = document.createElement('video') | ||
444 | this.playerElement.className = 'video-js vjs-peertube-skin' | ||
445 | this.playerElement.setAttribute('playsinline', 'true') | ||
446 | playerElementWrapper.appendChild(this.playerElement) | ||
447 | 483 | ||
448 | const params = { | 484 | const params = { |
449 | video: this.video, | 485 | video: this.video, |
450 | videoCaptions: this.videoCaptions, | 486 | videoCaptions: this.videoCaptions, |
487 | storyboards: this.storyboards, | ||
451 | liveVideo: this.liveVideo, | 488 | liveVideo: this.liveVideo, |
452 | videoFileToken: this.videoFileToken, | 489 | videoFileToken: this.videoFileToken, |
453 | urlOptions, | 490 | videoPassword: this.videoPassword, |
491 | urlOptions: this.getUrlOptions(), | ||
454 | loggedInOrAnonymousUser, | 492 | loggedInOrAnonymousUser, |
455 | forceAutoplay, | 493 | forceAutoplay, |
456 | user: this.user | 494 | user: this.user |
457 | } | 495 | } |
458 | const { playerMode, playerOptions } = await this.hooks.wrapFun( | 496 | |
459 | this.buildPlayerManagerOptions.bind(this), | 497 | const loadOptions = await this.hooks.wrapFun( |
498 | this.buildPeerTubePlayerLoadOptions.bind(this), | ||
460 | params, | 499 | params, |
461 | 'video-watch', | 500 | 'video-watch', |
462 | 'filter:internal.video-watch.player.build-options.params', | 501 | 'filter:internal.video-watch.player.load-options.params', |
463 | 'filter:internal.video-watch.player.build-options.result' | 502 | 'filter:internal.video-watch.player.load-options.result' |
464 | ) | 503 | ) |
465 | 504 | ||
466 | this.zone.runOutsideAngular(async () => { | 505 | this.zone.runOutsideAngular(async () => { |
467 | this.player = await PeertubePlayerManager.initialize(playerMode, playerOptions, player => this.player = player) | 506 | await this.peertubePlayer.load(loadOptions) |
468 | 507 | ||
469 | this.player.on('customError', (_e, data: any) => { | 508 | const player = this.peertubePlayer.getPlayer() |
470 | this.zone.run(() => this.handleGlobalError(data.err)) | ||
471 | }) | ||
472 | 509 | ||
473 | this.player.on('timeupdate', () => { | 510 | player.on('timeupdate', () => { |
474 | // Don't need to trigger angular change for this variable, that is sent to children components on click | 511 | // Don't need to trigger angular change for this variable, that is sent to children components on click |
475 | this.currentTime = Math.floor(this.player.currentTime()) | 512 | this.currentTime = Math.floor(player.currentTime()) |
476 | }) | 513 | }) |
477 | 514 | ||
478 | /** | 515 | if (this.video.isLive) { |
479 | * condition: true to make the upnext functionality trigger, false to disable the upnext functionality | 516 | player.one('ended', () => { |
480 | * go to the next video in 'condition()' if you don't want of the timer. | 517 | this.zone.run(() => { |
481 | * next: function triggered at the end of the timer. | 518 | // We changed the video, it's not a live anymore |
482 | * suspended: function used at each click of the timer checking if we need to reset progress | 519 | if (!this.video.isLive) return |
483 | * and wait until suspended becomes truthy again. | ||
484 | */ | ||
485 | this.player.upnext({ | ||
486 | timeout: 5000, // 5s | ||
487 | |||
488 | headText: $localize`Up Next`, | ||
489 | cancelText: $localize`Cancel`, | ||
490 | suspendedText: $localize`Autoplay is suspended`, | ||
491 | |||
492 | getTitle: () => this.nextVideoTitle, | ||
493 | 520 | ||
494 | next: () => this.zone.run(() => this.playNextVideoInAngularZone()), | 521 | this.video.state.id = VideoState.LIVE_ENDED |
495 | condition: () => { | ||
496 | if (!this.playlist) return this.isAutoPlayNext() | ||
497 | 522 | ||
498 | // Don't wait timeout to play the next playlist video | 523 | this.updatePlayerOnNoLive() |
499 | if (this.isPlaylistAutoPlayNext()) { | 524 | }) |
500 | this.playNextVideoInAngularZone() | 525 | }) |
501 | return undefined | 526 | } |
502 | } | ||
503 | |||
504 | return false | ||
505 | }, | ||
506 | |||
507 | suspended: () => { | ||
508 | return ( | ||
509 | !isXPercentInViewport(this.player.el() as HTMLElement, 80) || | ||
510 | !document.getElementById('content').contains(document.activeElement) | ||
511 | ) | ||
512 | } | ||
513 | }) | ||
514 | |||
515 | this.player.one('stopped', () => { | ||
516 | if (this.playlist && this.isPlaylistAutoPlayNext()) { | ||
517 | this.playNextVideoInAngularZone() | ||
518 | } | ||
519 | }) | ||
520 | |||
521 | this.player.one('ended', () => { | ||
522 | if (this.video.isLive) { | ||
523 | this.zone.run(() => this.video.state.id = VideoState.LIVE_ENDED) | ||
524 | } | ||
525 | }) | ||
526 | 527 | ||
527 | this.player.on('theaterChange', (_: any, enabled: boolean) => { | 528 | player.on('theater-change', (_: any, enabled: boolean) => { |
528 | this.zone.run(() => this.theaterEnabled = enabled) | 529 | this.zone.run(() => this.theaterEnabled = enabled) |
529 | }) | 530 | }) |
530 | 531 | ||
531 | this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', { | 532 | this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', { |
532 | player: this.player, | 533 | player, |
533 | playlist: this.playlist, | 534 | playlist: this.playlist, |
534 | playlistPosition: this.playlistPosition, | 535 | playlistPosition: this.playlistPosition, |
535 | videojs, | 536 | videojs, |
@@ -546,15 +547,25 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
546 | return true | 547 | return true |
547 | } | 548 | } |
548 | 549 | ||
549 | private playNextVideoInAngularZone () { | 550 | private getNextVideoTitle () { |
550 | if (this.playlist) { | 551 | if (this.playlist) { |
551 | this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo()) | 552 | return this.videoWatchPlaylist.getNextVideo()?.video?.name || '' |
552 | return | ||
553 | } | 553 | } |
554 | 554 | ||
555 | if (this.nextVideoUUID) { | 555 | return this.nextRecommendedVideoTitle |
556 | this.router.navigate([ '/w', this.nextVideoUUID ]) | 556 | } |
557 | } | 557 | |
558 | private playNextVideoInAngularZone () { | ||
559 | this.zone.run(() => { | ||
560 | if (this.playlist) { | ||
561 | this.videoWatchPlaylist.navigateToNextPlaylistVideo() | ||
562 | return | ||
563 | } | ||
564 | |||
565 | if (this.nextRecommendedVideoUUID) { | ||
566 | this.router.navigate([ '/w', this.nextRecommendedVideoUUID ]) | ||
567 | } | ||
568 | }) | ||
558 | } | 569 | } |
559 | 570 | ||
560 | private isAutoplay () { | 571 | private isAutoplay () { |
@@ -582,32 +593,93 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
582 | ) | 593 | ) |
583 | } | 594 | } |
584 | 595 | ||
585 | private flushPlayer () { | 596 | private buildPeerTubePlayerConstructorOptions (options: { |
586 | // Remove player if it exists | 597 | urlOptions: URLOptions |
587 | if (!this.player) return | 598 | }): PeerTubePlayerContructorOptions { |
599 | const { urlOptions } = options | ||
600 | |||
601 | return { | ||
602 | playerElement: () => this.playerElement.nativeElement, | ||
603 | |||
604 | enableHotkeys: true, | ||
605 | inactivityTimeout: 2500, | ||
606 | |||
607 | theaterButton: true, | ||
608 | |||
609 | controls: urlOptions.controls, | ||
610 | controlBar: urlOptions.controlBar, | ||
611 | |||
612 | muted: urlOptions.muted, | ||
613 | loop: urlOptions.loop, | ||
614 | |||
615 | playbackRate: urlOptions.playbackRate, | ||
616 | |||
617 | instanceName: this.serverConfig.instance.name, | ||
618 | language: this.localeId, | ||
619 | metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', | ||
620 | |||
621 | videoViewIntervalMs: VideoWatchComponent.VIEW_VIDEO_INTERVAL_MS, | ||
622 | authorizationHeader: () => this.authService.getRequestHeaderValue(), | ||
588 | 623 | ||
589 | try { | 624 | serverUrl: environment.originServerUrl || window.location.origin, |
590 | this.player.dispose() | 625 | |
591 | this.player = undefined | 626 | errorNotifier: (message: string) => this.notifier.error(message), |
592 | } catch (err) { | 627 | |
593 | logger.error('Cannot dispose player.', err) | 628 | peertubeLink: () => false, |
629 | |||
630 | pluginsManager: this.pluginService.getPluginsManager() | ||
594 | } | 631 | } |
595 | } | 632 | } |
596 | 633 | ||
597 | private buildPlayerManagerOptions (params: { | 634 | private buildPeerTubePlayerLoadOptions (options: { |
598 | video: VideoDetails | 635 | video: VideoDetails |
599 | liveVideo: LiveVideo | 636 | liveVideo: LiveVideo |
600 | videoCaptions: VideoCaption[] | 637 | videoCaptions: VideoCaption[] |
638 | storyboards: Storyboard[] | ||
601 | 639 | ||
602 | videoFileToken: string | 640 | videoFileToken: string |
641 | videoPassword: string | ||
603 | 642 | ||
604 | urlOptions: CustomizationOptions & { playerMode: PlayerMode } | 643 | urlOptions: URLOptions |
605 | 644 | ||
606 | loggedInOrAnonymousUser: User | 645 | loggedInOrAnonymousUser: User |
607 | forceAutoplay: boolean | 646 | forceAutoplay: boolean |
608 | user?: AuthUser // Keep for plugins | 647 | user?: AuthUser // Keep for plugins |
609 | }) { | 648 | }): PeerTubePlayerLoadOptions { |
610 | const { video, liveVideo, videoCaptions, videoFileToken, urlOptions, loggedInOrAnonymousUser, forceAutoplay } = params | 649 | const { |
650 | video, | ||
651 | liveVideo, | ||
652 | videoCaptions, | ||
653 | storyboards, | ||
654 | videoFileToken, | ||
655 | videoPassword, | ||
656 | urlOptions, | ||
657 | loggedInOrAnonymousUser, | ||
658 | forceAutoplay | ||
659 | } = options | ||
660 | |||
661 | let mode: PlayerMode | ||
662 | |||
663 | if (urlOptions.playerMode) { | ||
664 | if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader' | ||
665 | else mode = 'web-video' | ||
666 | } else { | ||
667 | if (video.hasHlsPlaylist()) mode = 'p2p-media-loader' | ||
668 | else mode = 'web-video' | ||
669 | } | ||
670 | |||
671 | let hlsOptions: HLSOptions | ||
672 | if (video.hasHlsPlaylist()) { | ||
673 | const hlsPlaylist = video.getHlsPlaylist() | ||
674 | |||
675 | hlsOptions = { | ||
676 | playlistUrl: hlsPlaylist.playlistUrl, | ||
677 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
678 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
679 | trackerAnnounce: video.trackerUrls, | ||
680 | videoFiles: hlsPlaylist.files | ||
681 | } | ||
682 | } | ||
611 | 683 | ||
612 | const getStartTime = () => { | 684 | const getStartTime = () => { |
613 | const byUrl = urlOptions.startTime !== undefined | 685 | const byUrl = urlOptions.startTime !== undefined |
@@ -634,117 +706,93 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
634 | src: environment.apiUrl + c.captionPath | 706 | src: environment.apiUrl + c.captionPath |
635 | })) | 707 | })) |
636 | 708 | ||
709 | const storyboard = storyboards.length !== 0 | ||
710 | ? { | ||
711 | url: environment.apiUrl + storyboards[0].storyboardPath, | ||
712 | height: storyboards[0].spriteHeight, | ||
713 | width: storyboards[0].spriteWidth, | ||
714 | interval: storyboards[0].spriteDuration | ||
715 | } | ||
716 | : undefined | ||
717 | |||
637 | const liveOptions = video.isLive | 718 | const liveOptions = video.isLive |
638 | ? { latencyMode: liveVideo.latencyMode } | 719 | ? { latencyMode: liveVideo.latencyMode } |
639 | : undefined | 720 | : undefined |
640 | 721 | ||
641 | const options: PeertubePlayerManagerOptions = { | 722 | return { |
642 | common: { | 723 | mode, |
643 | autoplay: this.isAutoplay(), | ||
644 | forceAutoplay, | ||
645 | p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled), | ||
646 | |||
647 | hasNextVideo: () => this.hasNextVideo(), | ||
648 | nextVideo: () => this.playNextVideoInAngularZone(), | ||
649 | |||
650 | playerElement: this.playerElement, | ||
651 | onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element, | ||
652 | |||
653 | videoDuration: video.duration, | ||
654 | enableHotkeys: true, | ||
655 | inactivityTimeout: 2500, | ||
656 | poster: video.previewUrl, | ||
657 | |||
658 | startTime, | ||
659 | stopTime: urlOptions.stopTime, | ||
660 | controlBar: urlOptions.controlBar, | ||
661 | controls: urlOptions.controls, | ||
662 | muted: urlOptions.muted, | ||
663 | loop: urlOptions.loop, | ||
664 | subtitle: urlOptions.subtitle, | ||
665 | playbackRate: urlOptions.playbackRate, | ||
666 | 724 | ||
667 | peertubeLink: urlOptions.peertubeLink, | 725 | autoplay: this.isAutoplay(), |
726 | forceAutoplay, | ||
668 | 727 | ||
669 | theaterButton: true, | 728 | duration: this.video.duration, |
670 | captions: videoCaptions.length !== 0, | 729 | poster: video.previewUrl, |
730 | p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled), | ||
671 | 731 | ||
672 | embedUrl: video.embedUrl, | 732 | startTime, |
673 | embedTitle: video.name, | 733 | stopTime: urlOptions.stopTime, |
674 | instanceName: this.serverConfig.instance.name, | ||
675 | 734 | ||
676 | isLive: video.isLive, | 735 | embedUrl: video.embedUrl, |
677 | liveOptions, | 736 | embedTitle: video.name, |
678 | 737 | ||
679 | language: this.localeId, | 738 | isLive: video.isLive, |
739 | liveOptions, | ||
680 | 740 | ||
681 | metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', | 741 | videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE |
742 | ? this.videoService.getVideoViewUrl(video.uuid) | ||
743 | : null, | ||
682 | 744 | ||
683 | videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE | 745 | videoFileToken: () => videoFileToken, |
684 | ? this.videoService.getVideoViewUrl(video.uuid) | 746 | requiresUserAuth: videoRequiresUserAuth(video, videoPassword), |
685 | : null, | 747 | requiresPassword: video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED && |
686 | videoViewIntervalMs: VideoWatchComponent.VIEW_VIDEO_INTERVAL_MS, | 748 | !video.canAccessPasswordProtectedVideoWithoutPassword(this.user), |
687 | authorizationHeader: () => this.authService.getRequestHeaderValue(), | 749 | videoPassword: () => videoPassword, |
688 | 750 | ||
689 | serverUrl: environment.originServerUrl || window.location.origin, | 751 | videoCaptions: playerCaptions, |
752 | storyboard, | ||
690 | 753 | ||
691 | videoFileToken: () => videoFileToken, | 754 | videoShortUUID: video.shortUUID, |
692 | requiresAuth: videoRequiresAuth(video), | 755 | videoUUID: video.uuid, |
693 | 756 | ||
694 | videoCaptions: playerCaptions, | 757 | previousVideo: { |
758 | enabled: this.playlist && this.videoWatchPlaylist.hasPreviousVideo(), | ||
695 | 759 | ||
696 | videoShortUUID: video.shortUUID, | 760 | handler: this.playlist |
697 | videoUUID: video.uuid, | 761 | ? () => this.zone.run(() => this.videoWatchPlaylist.navigateToPreviousPlaylistVideo()) |
762 | : undefined, | ||
698 | 763 | ||
699 | errorNotifier: (message: string) => this.notifier.error(message) | 764 | displayControlBarButton: !!this.playlist |
700 | }, | 765 | }, |
701 | 766 | ||
702 | webtorrent: { | 767 | nextVideo: { |
703 | videoFiles: video.files | 768 | enabled: this.hasNextVideo(), |
769 | handler: () => this.playNextVideoInAngularZone(), | ||
770 | getVideoTitle: () => this.getNextVideoTitle(), | ||
771 | displayControlBarButton: this.hasNextVideo() | ||
704 | }, | 772 | }, |
705 | 773 | ||
706 | pluginsManager: this.pluginService.getPluginsManager() | 774 | upnext: { |
707 | } | 775 | isEnabled: () => { |
776 | if (this.playlist) return this.isPlaylistAutoPlayNext() | ||
708 | 777 | ||
709 | // Only set this if we're in a playlist | 778 | return this.isAutoPlayNext() |
710 | if (this.playlist) { | 779 | }, |
711 | options.common.hasPreviousVideo = () => this.videoWatchPlaylist.hasPreviousVideo() | ||
712 | |||
713 | options.common.previousVideo = () => { | ||
714 | this.zone.run(() => this.videoWatchPlaylist.navigateToPreviousPlaylistVideo()) | ||
715 | } | ||
716 | } | ||
717 | |||
718 | let mode: PlayerMode | ||
719 | |||
720 | if (urlOptions.playerMode) { | ||
721 | if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader' | ||
722 | else mode = 'webtorrent' | ||
723 | } else { | ||
724 | if (video.hasHlsPlaylist()) mode = 'p2p-media-loader' | ||
725 | else mode = 'webtorrent' | ||
726 | } | ||
727 | 780 | ||
728 | // p2p-media-loader needs TextEncoder, fallback on WebTorrent if not available | 781 | isSuspended: (player: videojs.Player) => { |
729 | if (typeof TextEncoder === 'undefined') { | 782 | return !isXPercentInViewport(player.el() as HTMLElement, 80) |
730 | mode = 'webtorrent' | 783 | }, |
731 | } | ||
732 | 784 | ||
733 | if (mode === 'p2p-media-loader') { | 785 | timeout: this.playlist |
734 | const hlsPlaylist = video.getHlsPlaylist() | 786 | ? 0 // Don't wait to play next video in playlist |
787 | : 5000 // 5 seconds for a recommended video | ||
788 | }, | ||
735 | 789 | ||
736 | const p2pMediaLoader = { | 790 | hls: hlsOptions, |
737 | playlistUrl: hlsPlaylist.playlistUrl, | ||
738 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
739 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
740 | trackerAnnounce: video.trackerUrls, | ||
741 | videoFiles: hlsPlaylist.files | ||
742 | } as P2PMediaLoaderOptions | ||
743 | 791 | ||
744 | Object.assign(options, { p2pMediaLoader }) | 792 | webVideo: { |
793 | videoFiles: video.files | ||
794 | } | ||
745 | } | 795 | } |
746 | |||
747 | return { playerMode: mode, playerOptions: options } | ||
748 | } | 796 | } |
749 | 797 | ||
750 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { | 798 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { |
@@ -792,6 +840,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
792 | this.video.viewers = newViewers | 840 | this.video.viewers = newViewers |
793 | } | 841 | } |
794 | 842 | ||
843 | private updatePlayerOnNoLive () { | ||
844 | this.peertubePlayer.unload() | ||
845 | this.peertubePlayer.disable() | ||
846 | this.peertubePlayer.setPoster(this.video.previewPath) | ||
847 | } | ||
848 | |||
795 | private buildHotkeysHelp (video: Video) { | 849 | private buildHotkeysHelp (video: Video) { |
796 | if (this.hotkeys.length !== 0) { | 850 | if (this.hotkeys.length !== 0) { |
797 | this.hotkeysService.remove(this.hotkeys) | 851 | this.hotkeysService.remove(this.hotkeys) |
@@ -863,4 +917,26 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
863 | this.metaService.setTag('og:url', window.location.href) | 917 | this.metaService.setTag('og:url', window.location.href) |
864 | this.metaService.setTag('url', window.location.href) | 918 | this.metaService.setTag('url', window.location.href) |
865 | } | 919 | } |
920 | |||
921 | private getUrlOptions (): URLOptions { | ||
922 | const queryParams = this.route.snapshot.queryParams | ||
923 | |||
924 | return { | ||
925 | resume: queryParams.resume, | ||
926 | |||
927 | startTime: queryParams.start, | ||
928 | stopTime: queryParams.stop, | ||
929 | |||
930 | muted: toBoolean(queryParams.muted), | ||
931 | loop: toBoolean(queryParams.loop), | ||
932 | subtitle: queryParams.subtitle, | ||
933 | |||
934 | playerMode: queryParams.mode, | ||
935 | playbackRate: queryParams.playbackRate, | ||
936 | |||
937 | controlBar: toBoolean(queryParams.controlBar), | ||
938 | |||
939 | peertubeLink: false | ||
940 | } | ||
941 | } | ||
866 | } | 942 | } |
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 7e4fac730..9339865f1 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -12,13 +12,14 @@ import { CoreModule, PluginService, RedirectService, ServerService } from './cor | |||
12 | import { EmptyComponent } from './empty.component' | 12 | import { EmptyComponent } from './empty.component' |
13 | import { HeaderComponent, SearchTypeaheadComponent, SuggestionComponent } from './header' | 13 | import { HeaderComponent, SearchTypeaheadComponent, SuggestionComponent } from './header' |
14 | import { HighlightPipe } from './header/highlight.pipe' | 14 | import { HighlightPipe } from './header/highlight.pipe' |
15 | import { polyfillICU } from './helpers' | ||
15 | import { LanguageChooserComponent, MenuComponent, NotificationComponent } from './menu' | 16 | import { LanguageChooserComponent, MenuComponent, NotificationComponent } from './menu' |
17 | import { AccountSetupWarningModalComponent } from './modal/account-setup-warning-modal.component' | ||
18 | import { AdminWelcomeModalComponent } from './modal/admin-welcome-modal.component' | ||
16 | import { ConfirmComponent } from './modal/confirm.component' | 19 | import { ConfirmComponent } from './modal/confirm.component' |
17 | import { CustomModalComponent } from './modal/custom-modal.component' | 20 | import { CustomModalComponent } from './modal/custom-modal.component' |
18 | import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' | 21 | import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' |
19 | import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component' | 22 | import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component' |
20 | import { AdminWelcomeModalComponent } from './modal/admin-welcome-modal.component' | ||
21 | import { AccountSetupWarningModalComponent } from './modal/account-setup-warning-modal.component' | ||
22 | import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module' | 23 | import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module' |
23 | import { SharedFormModule } from './shared/shared-forms' | 24 | import { SharedFormModule } from './shared/shared-forms' |
24 | import { SharedGlobalIconModule } from './shared/shared-icons' | 25 | import { SharedGlobalIconModule } from './shared/shared-icons' |
@@ -90,6 +91,11 @@ export function loadConfigFactory (server: ServerService, pluginService: PluginS | |||
90 | useFactory: loadConfigFactory, | 91 | useFactory: loadConfigFactory, |
91 | deps: [ ServerService, PluginService, RedirectService ], | 92 | deps: [ ServerService, PluginService, RedirectService ], |
92 | multi: true | 93 | multi: true |
94 | }, | ||
95 | { | ||
96 | provide: APP_INITIALIZER, | ||
97 | useFactory: () => polyfillICU, | ||
98 | multi: true | ||
93 | } | 99 | } |
94 | ] | 100 | ] |
95 | }) | 101 | }) |
diff --git a/client/src/app/core/confirm/confirm.service.ts b/client/src/app/core/confirm/confirm.service.ts index 89a25f0a5..abe163aae 100644 --- a/client/src/app/core/confirm/confirm.service.ts +++ b/client/src/app/core/confirm/confirm.service.ts | |||
@@ -4,6 +4,7 @@ import { Injectable } from '@angular/core' | |||
4 | type ConfirmOptions = { | 4 | type ConfirmOptions = { |
5 | title: string | 5 | title: string |
6 | message: string | 6 | message: string |
7 | errorMessage?: string | ||
7 | } & ( | 8 | } & ( |
8 | { | 9 | { |
9 | type: 'confirm' | 10 | type: 'confirm' |
@@ -12,6 +13,7 @@ type ConfirmOptions = { | |||
12 | { | 13 | { |
13 | type: 'confirm-password' | 14 | type: 'confirm-password' |
14 | confirmButtonText?: string | 15 | confirmButtonText?: string |
16 | isIncorrectPassword?: boolean | ||
15 | } | | 17 | } | |
16 | { | 18 | { |
17 | type: 'confirm-expected-input' | 19 | type: 'confirm-expected-input' |
@@ -32,8 +34,14 @@ export class ConfirmService { | |||
32 | return firstValueFrom(this.extractConfirmed(this.confirmResponse.asObservable())) | 34 | return firstValueFrom(this.extractConfirmed(this.confirmResponse.asObservable())) |
33 | } | 35 | } |
34 | 36 | ||
35 | confirmWithPassword (message: string, title = '', confirmButtonText?: string) { | 37 | confirmWithPassword (options: { |
36 | this.showConfirm.next({ type: 'confirm-password', title, message, confirmButtonText }) | 38 | message: string |
39 | title?: string | ||
40 | confirmButtonText?: string | ||
41 | errorMessage?: string | ||
42 | }) { | ||
43 | const { message, title = '', confirmButtonText, errorMessage } = options | ||
44 | this.showConfirm.next({ type: 'confirm-password', title, message, confirmButtonText, errorMessage }) | ||
37 | 45 | ||
38 | const obs = this.confirmResponse.asObservable() | 46 | const obs = this.confirmResponse.asObservable() |
39 | .pipe(map(({ confirmed, value }) => ({ confirmed, password: value }))) | 47 | .pipe(map(({ confirmed, value }) => ({ confirmed, password: value }))) |
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts index d57608f1c..5aa02e472 100644 --- a/client/src/app/core/users/user.model.ts +++ b/client/src/app/core/users/user.model.ts | |||
@@ -30,8 +30,6 @@ export class User implements UserServerModel { | |||
30 | autoPlayNextVideoPlaylist: boolean | 30 | autoPlayNextVideoPlaylist: boolean |
31 | 31 | ||
32 | p2pEnabled: boolean | 32 | p2pEnabled: boolean |
33 | // FIXME: deprecated in 4.1 | ||
34 | webTorrentEnabled: never | ||
35 | 33 | ||
36 | videosHistoryEnabled: boolean | 34 | videosHistoryEnabled: boolean |
37 | videoLanguages: string[] | 35 | videoLanguages: string[] |
diff --git a/client/src/app/helpers/i18n-utils.ts b/client/src/app/helpers/i18n-utils.ts index b7d73d16b..9e22bb4c1 100644 --- a/client/src/app/helpers/i18n-utils.ts +++ b/client/src/app/helpers/i18n-utils.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import IntlMessageFormat from 'intl-messageformat' | 1 | import IntlMessageFormat from 'intl-messageformat' |
2 | import { shouldPolyfill as shouldPolyfillLocale } from '@formatjs/intl-locale/should-polyfill' | ||
3 | import { shouldPolyfill as shouldPolyfillPlural } from '@formatjs/intl-pluralrules/should-polyfill' | ||
2 | import { logger } from '@root-helpers/logger' | 4 | import { logger } from '@root-helpers/logger' |
3 | import { environment } from '../../environments/environment' | 5 | import { environment } from '../../environments/environment' |
4 | 6 | ||
@@ -10,31 +12,68 @@ function getDevLocale () { | |||
10 | return 'fr-FR' | 12 | return 'fr-FR' |
11 | } | 13 | } |
12 | 14 | ||
13 | function prepareIcu (icu: string) { | 15 | async function polyfillICU () { |
14 | let alreadyWarned = false | 16 | // Important to be in this order, Plural needs Locale (https://formatjs.io/docs/polyfills/intl-pluralrules) |
17 | await polyfillICULocale() | ||
18 | await polyfillICUPlural() | ||
19 | } | ||
15 | 20 | ||
16 | try { | 21 | async function polyfillICULocale () { |
17 | const msg = new IntlMessageFormat(icu, $localize.locale) | 22 | // This locale is supported |
23 | if (shouldPolyfillLocale()) { | ||
24 | // TODO: remove, it's only needed to support Plural polyfill and so iOS 12 | ||
25 | console.log('Loading Intl Locale polyfill for ' + $localize.locale) | ||
26 | |||
27 | await import('@formatjs/intl-locale/polyfill') | ||
28 | } | ||
29 | } | ||
30 | |||
31 | async function polyfillICUPlural () { | ||
32 | const unsupportedLocale = shouldPolyfillPlural($localize.locale) | ||
33 | |||
34 | // This locale is supported | ||
35 | if (!unsupportedLocale) { | ||
36 | return | ||
37 | } | ||
18 | 38 | ||
19 | return (context: { [id: string]: number | string }, fallback: string) => { | 39 | // TODO: remove, it's only needed to support iOS 12 |
20 | try { | 40 | console.log('Loading Intl Plural rules polyfill for ' + $localize.locale) |
21 | return msg.format(context) as string | ||
22 | } catch (err) { | ||
23 | if (!alreadyWarned) logger.warn(`Cannot format ICU ${icu}.`, err) | ||
24 | 41 | ||
25 | alreadyWarned = true | 42 | // Load the polyfill 1st BEFORE loading data |
26 | return fallback | 43 | await import('@formatjs/intl-pluralrules/polyfill-force') |
27 | } | 44 | // Degraded mode, so only load the en local data |
45 | await import(`@formatjs/intl-pluralrules/locale-data/en.js`) | ||
46 | } | ||
47 | |||
48 | // --------------------------------------------------------------------------- | ||
49 | |||
50 | const icuCache = new Map<string, IntlMessageFormat>() | ||
51 | const icuWarnings = new Set<string>() | ||
52 | const fallback = 'String translation error' | ||
53 | |||
54 | function formatICU (icu: string, context: { [id: string]: number | string }) { | ||
55 | try { | ||
56 | let msg = icuCache.get(icu) | ||
57 | |||
58 | if (!msg) { | ||
59 | msg = new IntlMessageFormat(icu, $localize.locale) | ||
60 | icuCache.set(icu, msg) | ||
28 | } | 61 | } |
62 | |||
63 | return msg.format(context) as string | ||
29 | } catch (err) { | 64 | } catch (err) { |
30 | logger.warn(`Cannot build intl message ${icu}.`, err) | 65 | if (!icuWarnings.has(icu)) { |
66 | logger.warn(`Cannot format ICU ${icu}.`, err) | ||
67 | } | ||
31 | 68 | ||
32 | return (_context: unknown, fallback: string) => fallback | 69 | icuWarnings.add(icu) |
70 | return fallback | ||
33 | } | 71 | } |
34 | } | 72 | } |
35 | 73 | ||
36 | export { | 74 | export { |
37 | getDevLocale, | 75 | getDevLocale, |
38 | prepareIcu, | 76 | polyfillICU, |
77 | formatICU, | ||
39 | isOnDevLocale | 78 | isOnDevLocale |
40 | } | 79 | } |
diff --git a/client/src/app/helpers/utils/object.ts b/client/src/app/helpers/utils/object.ts index 69b2b18c0..b69e31edf 100644 --- a/client/src/app/helpers/utils/object.ts +++ b/client/src/app/helpers/utils/object.ts | |||
@@ -34,6 +34,8 @@ function toBoolean (value: any) { | |||
34 | 34 | ||
35 | if (value === 'true') return true | 35 | if (value === 'true') return true |
36 | if (value === 'false') return false | 36 | if (value === 'false') return false |
37 | if (value === '1') return true | ||
38 | if (value === '0') return false | ||
37 | 39 | ||
38 | return undefined | 40 | return undefined |
39 | } | 41 | } |
diff --git a/client/src/app/modal/confirm.component.html b/client/src/app/modal/confirm.component.html index 6584db3e6..33696d0a5 100644 --- a/client/src/app/modal/confirm.component.html +++ b/client/src/app/modal/confirm.component.html | |||
@@ -12,10 +12,12 @@ | |||
12 | <div *ngIf="inputLabel" class="form-group mt-3"> | 12 | <div *ngIf="inputLabel" class="form-group mt-3"> |
13 | <label for="confirmInput">{{ inputLabel }}</label> | 13 | <label for="confirmInput">{{ inputLabel }}</label> |
14 | 14 | ||
15 | <input *ngIf="!isPasswordInput" type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" /> | 15 | <input *ngIf="!isPasswordInput" type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" (keyup.enter)="confirm()" /> |
16 | 16 | ||
17 | <my-input-text *ngIf="isPasswordInput" inputId="confirmInput" [(ngModel)]="inputValue"></my-input-text> | 17 | <my-input-text *ngIf="isPasswordInput" inputId="confirmInput" [(ngModel)]="inputValue" (keyup.enter)="confirm()"></my-input-text> |
18 | </div> | 18 | </div> |
19 | |||
20 | <div *ngIf="hasError()" class="text-danger">{{ errorMessage }}</div> | ||
19 | </div> | 21 | </div> |
20 | 22 | ||
21 | <div class="modal-footer inputs"> | 23 | <div class="modal-footer inputs"> |
diff --git a/client/src/app/modal/confirm.component.ts b/client/src/app/modal/confirm.component.ts index 3bb8b9b21..43369befa 100644 --- a/client/src/app/modal/confirm.component.ts +++ b/client/src/app/modal/confirm.component.ts | |||
@@ -21,6 +21,8 @@ export class ConfirmComponent implements OnInit { | |||
21 | inputValue = '' | 21 | inputValue = '' |
22 | confirmButtonText = '' | 22 | confirmButtonText = '' |
23 | 23 | ||
24 | errorMessage = '' | ||
25 | |||
24 | isPasswordInput = false | 26 | isPasswordInput = false |
25 | 27 | ||
26 | private openedModal: NgbModalRef | 28 | private openedModal: NgbModalRef |
@@ -42,8 +44,9 @@ export class ConfirmComponent implements OnInit { | |||
42 | this.inputValue = '' | 44 | this.inputValue = '' |
43 | this.confirmButtonText = '' | 45 | this.confirmButtonText = '' |
44 | this.isPasswordInput = false | 46 | this.isPasswordInput = false |
47 | this.errorMessage = '' | ||
45 | 48 | ||
46 | const { type, title, message, confirmButtonText } = payload | 49 | const { type, title, message, confirmButtonText, errorMessage } = payload |
47 | 50 | ||
48 | this.title = title | 51 | this.title = title |
49 | 52 | ||
@@ -53,6 +56,7 @@ export class ConfirmComponent implements OnInit { | |||
53 | } else if (type === 'confirm-password') { | 56 | } else if (type === 'confirm-password') { |
54 | this.inputLabel = $localize`Confirm your password` | 57 | this.inputLabel = $localize`Confirm your password` |
55 | this.isPasswordInput = true | 58 | this.isPasswordInput = true |
59 | this.errorMessage = errorMessage | ||
56 | } | 60 | } |
57 | 61 | ||
58 | this.confirmButtonText = confirmButtonText || $localize`Confirm` | 62 | this.confirmButtonText = confirmButtonText || $localize`Confirm` |
@@ -78,6 +82,9 @@ export class ConfirmComponent implements OnInit { | |||
78 | return this.expectedInputValue !== this.inputValue | 82 | return this.expectedInputValue !== this.inputValue |
79 | } | 83 | } |
80 | 84 | ||
85 | hasError () { | ||
86 | return this.errorMessage | ||
87 | } | ||
81 | showModal () { | 88 | showModal () { |
82 | this.inputValue = '' | 89 | this.inputValue = '' |
83 | 90 | ||
diff --git a/client/src/app/shared/form-validators/custom-config-validators.ts b/client/src/app/shared/form-validators/custom-config-validators.ts index ff0813f7d..3672e5610 100644 --- a/client/src/app/shared/form-validators/custom-config-validators.ts +++ b/client/src/app/shared/form-validators/custom-config-validators.ts | |||
@@ -22,21 +22,12 @@ export const SERVICES_TWITTER_USERNAME_VALIDATOR: BuildFormValidator = { | |||
22 | } | 22 | } |
23 | } | 23 | } |
24 | 24 | ||
25 | export const CACHE_PREVIEWS_SIZE_VALIDATOR: BuildFormValidator = { | 25 | export const CACHE_SIZE_VALIDATOR: BuildFormValidator = { |
26 | VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], | 26 | VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], |
27 | MESSAGES: { | 27 | MESSAGES: { |
28 | required: $localize`Previews cache size is required.`, | 28 | required: $localize`Cache size is required.`, |
29 | min: $localize`Previews cache size must be greater than 1.`, | 29 | min: $localize`Cache size must be greater than 1.`, |
30 | pattern: $localize`Previews cache size must be a number.` | 30 | pattern: $localize`Cache size must be a number.` |
31 | } | ||
32 | } | ||
33 | |||
34 | export const CACHE_CAPTIONS_SIZE_VALIDATOR: BuildFormValidator = { | ||
35 | VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], | ||
36 | MESSAGES: { | ||
37 | required: $localize`Captions cache size is required.`, | ||
38 | min: $localize`Captions cache size must be greater than 1.`, | ||
39 | pattern: $localize`Captions cache size must be a number.` | ||
40 | } | 31 | } |
41 | } | 32 | } |
42 | 33 | ||
diff --git a/client/src/app/shared/form-validators/video-validators.ts b/client/src/app/shared/form-validators/video-validators.ts index a4bda8f16..090a76e43 100644 --- a/client/src/app/shared/form-validators/video-validators.ts +++ b/client/src/app/shared/form-validators/video-validators.ts | |||
@@ -26,6 +26,15 @@ export const VIDEO_PRIVACY_VALIDATOR: BuildFormValidator = { | |||
26 | } | 26 | } |
27 | } | 27 | } |
28 | 28 | ||
29 | export const VIDEO_PASSWORD_VALIDATOR: BuildFormValidator = { | ||
30 | VALIDATORS: [ Validators.minLength(2), Validators.maxLength(100) ], // Required is set dynamically | ||
31 | MESSAGES: { | ||
32 | minLength: $localize`A password should be at least 2 characters long.`, | ||
33 | maxLength: $localize`A password should be shorter than 100 characters long.`, | ||
34 | required: $localize`A password is required for password protected video.` | ||
35 | } | ||
36 | } | ||
37 | |||
29 | export const VIDEO_CATEGORY_VALIDATOR: BuildFormValidator = { | 38 | export const VIDEO_CATEGORY_VALIDATOR: BuildFormValidator = { |
30 | VALIDATORS: [ ], | 39 | VALIDATORS: [ ], |
31 | MESSAGES: {} | 40 | MESSAGES: {} |
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts b/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts index 2c3226f68..8b6cd091a 100644 --- a/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts +++ b/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, forwardRef, Input } from '@angular/core' | 1 | import { Component, forwardRef, Input } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { prepareIcu } from '@app/helpers' | 4 | import { formatICU } from '@app/helpers' |
5 | import { SelectOptionsItem } from '../../../../types/select-options-item.model' | 5 | import { SelectOptionsItem } from '../../../../types/select-options-item.model' |
6 | import { ItemSelectCheckboxValue } from './select-checkbox.component' | 6 | import { ItemSelectCheckboxValue } from './select-checkbox.component' |
7 | 7 | ||
@@ -80,9 +80,9 @@ export class SelectCheckboxAllComponent implements ControlValueAccessor { | |||
80 | 80 | ||
81 | if (outputItems.length >= this.maxItems) { | 81 | if (outputItems.length >= this.maxItems) { |
82 | this.notifier.error( | 82 | this.notifier.error( |
83 | prepareIcu($localize`You can't select more than {maxItems, plural, =1 {1 item} other {{maxItems} items}}`)( | 83 | formatICU( |
84 | { maxItems: this.maxItems }, | 84 | $localize`You can't select more than {maxItems, plural, =1 {1 item} other {{maxItems} items}}`, |
85 | $localize`You can't select more than ${this.maxItems} items` | 85 | { maxItems: this.maxItems } |
86 | ) | 86 | ) |
87 | ) | 87 | ) |
88 | 88 | ||
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts index 2e63f6c17..ab1b1458a 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.ts +++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ServerService } from '@app/core' | 2 | import { ServerService } from '@app/core' |
3 | import { prepareIcu } from '@app/helpers' | 3 | import { formatICU } from '@app/helpers' |
4 | import { ServerConfig } from '@shared/models' | 4 | import { ServerConfig } from '@shared/models' |
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
@@ -71,17 +71,17 @@ export class InstanceFeaturesTableComponent implements OnInit { | |||
71 | const hours = Math.floor(seconds / 3600) | 71 | const hours = Math.floor(seconds / 3600) |
72 | 72 | ||
73 | if (hours !== 0) { | 73 | if (hours !== 0) { |
74 | return prepareIcu($localize`~ {hours, plural, =1 {1 hour} other {{hours} hours}}`)( | 74 | return formatICU( |
75 | { hours }, | 75 | $localize`~ {hours, plural, =1 {1 hour} other {{hours} hours}}`, |
76 | $localize`~ ${hours} hours` | 76 | { hours } |
77 | ) | 77 | ) |
78 | } | 78 | } |
79 | 79 | ||
80 | const minutes = Math.floor(seconds % 3600 / 60) | 80 | const minutes = Math.floor(seconds % 3600 / 60) |
81 | 81 | ||
82 | return prepareIcu($localize`~ {minutes, plural, =1 {1 minute} other {{minutes} minutes}}`)( | 82 | return formatICU( |
83 | { minutes }, | 83 | $localize`~ {minutes, plural, =1 {1 minute} other {{minutes} minutes}}`, |
84 | $localize`~ ${minutes} minutes` | 84 | { minutes } |
85 | ) | 85 | ) |
86 | } | 86 | } |
87 | 87 | ||
diff --git a/client/src/app/shared/shared-main/angular/from-now.pipe.ts b/client/src/app/shared/shared-main/angular/from-now.pipe.ts index dc6a25e83..4ff244bbb 100644 --- a/client/src/app/shared/shared-main/angular/from-now.pipe.ts +++ b/client/src/app/shared/shared-main/angular/from-now.pipe.ts | |||
@@ -1,14 +1,9 @@ | |||
1 | import { Pipe, PipeTransform } from '@angular/core' | 1 | import { Pipe, PipeTransform } from '@angular/core' |
2 | import { prepareIcu } from '@app/helpers' | 2 | import { formatICU } from '@app/helpers' |
3 | 3 | ||
4 | // Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site | 4 | // Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site |
5 | @Pipe({ name: 'myFromNow' }) | 5 | @Pipe({ name: 'myFromNow' }) |
6 | export class FromNowPipe implements PipeTransform { | 6 | export class FromNowPipe implements PipeTransform { |
7 | private yearICU = prepareIcu($localize`{interval, plural, =1 {1 year ago} other {{interval} years ago}}`) | ||
8 | private monthICU = prepareIcu($localize`{interval, plural, =1 {1 month ago} other {{interval} months ago}}`) | ||
9 | private weekICU = prepareIcu($localize`{interval, plural, =1 {1 week ago} other {{interval} weeks ago}}`) | ||
10 | private dayICU = prepareIcu($localize`{interval, plural, =1 {1 day ago} other {{interval} days ago}}`) | ||
11 | private hourICU = prepareIcu($localize`{interval, plural, =1 {1 hour ago} other {{interval} hours ago}}`) | ||
12 | 7 | ||
13 | transform (arg: number | Date | string) { | 8 | transform (arg: number | Date | string) { |
14 | const argDate = new Date(arg) | 9 | const argDate = new Date(arg) |
@@ -16,7 +11,7 @@ export class FromNowPipe implements PipeTransform { | |||
16 | 11 | ||
17 | let interval = Math.floor(seconds / 31536000) | 12 | let interval = Math.floor(seconds / 31536000) |
18 | if (interval >= 1) { | 13 | if (interval >= 1) { |
19 | return this.yearICU({ interval }, $localize`${interval} year(s) ago`) | 14 | return formatICU($localize`{interval, plural, =1 {1 year ago} other {{interval} years ago}}`, { interval }) |
20 | } | 15 | } |
21 | 16 | ||
22 | interval = Math.floor(seconds / 2419200) | 17 | interval = Math.floor(seconds / 2419200) |
@@ -25,7 +20,7 @@ export class FromNowPipe implements PipeTransform { | |||
25 | if (interval >= 12) return $localize`1 year ago` | 20 | if (interval >= 12) return $localize`1 year ago` |
26 | 21 | ||
27 | if (interval >= 1) { | 22 | if (interval >= 1) { |
28 | return this.monthICU({ interval }, $localize`${interval} month(s) ago`) | 23 | return formatICU($localize`{interval, plural, =1 {1 month ago} other {{interval} months ago}}`, { interval }) |
29 | } | 24 | } |
30 | 25 | ||
31 | interval = Math.floor(seconds / 604800) | 26 | interval = Math.floor(seconds / 604800) |
@@ -34,17 +29,17 @@ export class FromNowPipe implements PipeTransform { | |||
34 | if (interval >= 4) return $localize`1 month ago` | 29 | if (interval >= 4) return $localize`1 month ago` |
35 | 30 | ||
36 | if (interval >= 1) { | 31 | if (interval >= 1) { |
37 | return this.weekICU({ interval }, $localize`${interval} week(s) ago`) | 32 | return formatICU($localize`{interval, plural, =1 {1 week ago} other {{interval} weeks ago}}`, { interval }) |
38 | } | 33 | } |
39 | 34 | ||
40 | interval = Math.floor(seconds / 86400) | 35 | interval = Math.floor(seconds / 86400) |
41 | if (interval >= 1) { | 36 | if (interval >= 1) { |
42 | return this.dayICU({ interval }, $localize`${interval} day(s) ago`) | 37 | return formatICU($localize`{interval, plural, =1 {1 day ago} other {{interval} days ago}}`, { interval }) |
43 | } | 38 | } |
44 | 39 | ||
45 | interval = Math.floor(seconds / 3600) | 40 | interval = Math.floor(seconds / 3600) |
46 | if (interval >= 1) { | 41 | if (interval >= 1) { |
47 | return this.hourICU({ interval }, $localize`${interval} hour(s) ago`) | 42 | return formatICU($localize`{interval, plural, =1 {1 hour ago} other {{interval} hours ago}}`, { interval }) |
48 | } | 43 | } |
49 | 44 | ||
50 | interval = Math.floor(seconds / 60) | 45 | interval = Math.floor(seconds / 60) |
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index d3ec31d6e..480277450 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -52,6 +52,7 @@ import { | |||
52 | VideoFileTokenService, | 52 | VideoFileTokenService, |
53 | VideoImportService, | 53 | VideoImportService, |
54 | VideoOwnershipService, | 54 | VideoOwnershipService, |
55 | VideoPasswordService, | ||
55 | VideoResolver, | 56 | VideoResolver, |
56 | VideoService | 57 | VideoService |
57 | } from './video' | 58 | } from './video' |
@@ -210,6 +211,8 @@ import { VideoChannelService } from './video-channel' | |||
210 | 211 | ||
211 | VideoChannelService, | 212 | VideoChannelService, |
212 | 213 | ||
214 | VideoPasswordService, | ||
215 | |||
213 | CustomPageService, | 216 | CustomPageService, |
214 | 217 | ||
215 | ActorRedirectGuard | 218 | ActorRedirectGuard |
diff --git a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts index 0f3afd116..21f31a717 100644 --- a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts +++ b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts | |||
@@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http' | |||
4 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
5 | import { RestExtractor, ServerService } from '@app/core' | 5 | import { RestExtractor, ServerService } from '@app/core' |
6 | import { objectToFormData, sortBy } from '@app/helpers' | 6 | import { objectToFormData, sortBy } from '@app/helpers' |
7 | import { VideoService } from '@app/shared/shared-main/video' | 7 | import { VideoPasswordService, VideoService } from '@app/shared/shared-main/video' |
8 | import { peertubeTranslate } from '@shared/core-utils/i18n' | 8 | import { peertubeTranslate } from '@shared/core-utils/i18n' |
9 | import { ResultList, VideoCaption } from '@shared/models' | 9 | import { ResultList, VideoCaption } from '@shared/models' |
10 | import { environment } from '../../../../environments/environment' | 10 | import { environment } from '../../../../environments/environment' |
@@ -18,8 +18,10 @@ export class VideoCaptionService { | |||
18 | private restExtractor: RestExtractor | 18 | private restExtractor: RestExtractor |
19 | ) {} | 19 | ) {} |
20 | 20 | ||
21 | listCaptions (videoId: string): Observable<ResultList<VideoCaption>> { | 21 | listCaptions (videoId: string, videoPassword?: string): Observable<ResultList<VideoCaption>> { |
22 | return this.authHttp.get<ResultList<VideoCaption>>(`${VideoService.BASE_VIDEO_URL}/${videoId}/captions`) | 22 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) |
23 | |||
24 | return this.authHttp.get<ResultList<VideoCaption>>(`${VideoService.BASE_VIDEO_URL}/${videoId}/captions`, { headers }) | ||
23 | .pipe( | 25 | .pipe( |
24 | switchMap(captionsResult => { | 26 | switchMap(captionsResult => { |
25 | return this.serverService.getServerLocale() | 27 | return this.serverService.getServerLocale() |
diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts index a2e47883e..07d40b117 100644 --- a/client/src/app/shared/shared-main/video/index.ts +++ b/client/src/app/shared/shared-main/video/index.ts | |||
@@ -5,6 +5,7 @@ export * from './video-edit.model' | |||
5 | export * from './video-file-token.service' | 5 | export * from './video-file-token.service' |
6 | export * from './video-import.service' | 6 | export * from './video-import.service' |
7 | export * from './video-ownership.service' | 7 | export * from './video-ownership.service' |
8 | export * from './video-password.service' | ||
8 | export * from './video.model' | 9 | export * from './video.model' |
9 | export * from './video.resolver' | 10 | export * from './video.resolver' |
10 | export * from './video.service' | 11 | export * from './video.service' |
diff --git a/client/src/app/shared/shared-main/video/video-edit.model.ts b/client/src/app/shared/shared-main/video/video-edit.model.ts index 47eee80d8..1b8b67ee2 100644 --- a/client/src/app/shared/shared-main/video/video-edit.model.ts +++ b/client/src/app/shared/shared-main/video/video-edit.model.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { getAbsoluteAPIUrl } from '@app/helpers' | 1 | import { getAbsoluteAPIUrl } from '@app/helpers' |
2 | import { VideoPrivacy, VideoScheduleUpdate, VideoUpdate } from '@shared/models' | 2 | import { VideoPassword, VideoPrivacy, VideoScheduleUpdate, VideoUpdate } from '@shared/models' |
3 | import { VideoDetails } from './video-details.model' | 3 | import { VideoDetails } from './video-details.model' |
4 | import { objectKeysTyped } from '@shared/core-utils' | 4 | import { objectKeysTyped } from '@shared/core-utils' |
5 | 5 | ||
@@ -18,6 +18,7 @@ export class VideoEdit implements VideoUpdate { | |||
18 | waitTranscoding: boolean | 18 | waitTranscoding: boolean |
19 | channelId: number | 19 | channelId: number |
20 | privacy: VideoPrivacy | 20 | privacy: VideoPrivacy |
21 | videoPassword?: string | ||
21 | support: string | 22 | support: string |
22 | thumbnailfile?: any | 23 | thumbnailfile?: any |
23 | previewfile?: any | 24 | previewfile?: any |
@@ -32,7 +33,7 @@ export class VideoEdit implements VideoUpdate { | |||
32 | 33 | ||
33 | pluginData?: any | 34 | pluginData?: any |
34 | 35 | ||
35 | constructor (video?: VideoDetails) { | 36 | constructor (video?: VideoDetails, videoPassword?: VideoPassword) { |
36 | if (!video) return | 37 | if (!video) return |
37 | 38 | ||
38 | this.id = video.id | 39 | this.id = video.id |
@@ -63,6 +64,8 @@ export class VideoEdit implements VideoUpdate { | |||
63 | : null | 64 | : null |
64 | 65 | ||
65 | this.pluginData = video.pluginData | 66 | this.pluginData = video.pluginData |
67 | |||
68 | if (videoPassword) this.videoPassword = videoPassword.password | ||
66 | } | 69 | } |
67 | 70 | ||
68 | patch (values: { [ id: string ]: any }) { | 71 | patch (values: { [ id: string ]: any }) { |
@@ -112,6 +115,7 @@ export class VideoEdit implements VideoUpdate { | |||
112 | waitTranscoding: this.waitTranscoding, | 115 | waitTranscoding: this.waitTranscoding, |
113 | channelId: this.channelId, | 116 | channelId: this.channelId, |
114 | privacy: this.privacy, | 117 | privacy: this.privacy, |
118 | videoPassword: this.videoPassword, | ||
115 | originallyPublishedAt: this.originallyPublishedAt | 119 | originallyPublishedAt: this.originallyPublishedAt |
116 | } | 120 | } |
117 | 121 | ||
diff --git a/client/src/app/shared/shared-main/video/video-file-token.service.ts b/client/src/app/shared/shared-main/video/video-file-token.service.ts index 791607249..9bca5b9ec 100644 --- a/client/src/app/shared/shared-main/video/video-file-token.service.ts +++ b/client/src/app/shared/shared-main/video/video-file-token.service.ts | |||
@@ -4,6 +4,7 @@ import { Injectable } from '@angular/core' | |||
4 | import { RestExtractor } from '@app/core' | 4 | import { RestExtractor } from '@app/core' |
5 | import { VideoToken } from '@shared/models' | 5 | import { VideoToken } from '@shared/models' |
6 | import { VideoService } from './video.service' | 6 | import { VideoService } from './video.service' |
7 | import { VideoPasswordService } from './video-password.service' | ||
7 | 8 | ||
8 | @Injectable() | 9 | @Injectable() |
9 | export class VideoFileTokenService { | 10 | export class VideoFileTokenService { |
@@ -15,16 +16,18 @@ export class VideoFileTokenService { | |||
15 | private restExtractor: RestExtractor | 16 | private restExtractor: RestExtractor |
16 | ) {} | 17 | ) {} |
17 | 18 | ||
18 | getVideoFileToken (videoUUID: string) { | 19 | getVideoFileToken ({ videoUUID, videoPassword }: { videoUUID: string, videoPassword?: string }) { |
19 | const existing = this.store.get(videoUUID) | 20 | const existing = this.store.get(videoUUID) |
20 | if (existing) return of(existing) | 21 | if (existing) return of(existing) |
21 | 22 | ||
22 | return this.createVideoFileToken(videoUUID) | 23 | return this.createVideoFileToken(videoUUID, videoPassword) |
23 | .pipe(tap(result => this.store.set(videoUUID, { token: result.token, expires: new Date(result.expires) }))) | 24 | .pipe(tap(result => this.store.set(videoUUID, { token: result.token, expires: new Date(result.expires) }))) |
24 | } | 25 | } |
25 | 26 | ||
26 | private createVideoFileToken (videoUUID: string) { | 27 | private createVideoFileToken (videoUUID: string, videoPassword?: string) { |
27 | return this.authHttp.post<VideoToken>(`${VideoService.BASE_VIDEO_URL}/${videoUUID}/token`, {}) | 28 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) |
29 | |||
30 | return this.authHttp.post<VideoToken>(`${VideoService.BASE_VIDEO_URL}/${videoUUID}/token`, {}, { headers }) | ||
28 | .pipe( | 31 | .pipe( |
29 | map(({ files }) => files), | 32 | map(({ files }) => files), |
30 | catchError(err => this.restExtractor.handleError(err)) | 33 | catchError(err => this.restExtractor.handleError(err)) |
diff --git a/client/src/app/shared/shared-main/video/video-password.service.ts b/client/src/app/shared/shared-main/video/video-password.service.ts new file mode 100644 index 000000000..d5b0406f8 --- /dev/null +++ b/client/src/app/shared/shared-main/video/video-password.service.ts | |||
@@ -0,0 +1,29 @@ | |||
1 | import { ResultList, VideoPassword } from '@shared/models' | ||
2 | import { Injectable } from '@angular/core' | ||
3 | import { catchError, switchMap } from 'rxjs' | ||
4 | import { HttpClient, HttpHeaders } from '@angular/common/http' | ||
5 | import { RestExtractor } from '@app/core' | ||
6 | import { VideoService } from './video.service' | ||
7 | |||
8 | @Injectable() | ||
9 | export class VideoPasswordService { | ||
10 | |||
11 | constructor ( | ||
12 | private authHttp: HttpClient, | ||
13 | private restExtractor: RestExtractor | ||
14 | ) {} | ||
15 | |||
16 | static buildVideoPasswordHeader (videoPassword: string) { | ||
17 | return videoPassword | ||
18 | ? new HttpHeaders().set('x-peertube-video-password', videoPassword) | ||
19 | : undefined | ||
20 | } | ||
21 | |||
22 | getVideoPasswords (options: { videoUUID: string }) { | ||
23 | return this.authHttp.get<ResultList<VideoPassword>>(`${VideoService.BASE_VIDEO_URL}/${options.videoUUID}/passwords`) | ||
24 | .pipe( | ||
25 | switchMap(res => res.data), | ||
26 | catchError(err => this.restExtractor.handleError(err)) | ||
27 | ) | ||
28 | } | ||
29 | } | ||
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts index 6fdffb394..1ffc40411 100644 --- a/client/src/app/shared/shared-main/video/video.model.ts +++ b/client/src/app/shared/shared-main/video/video.model.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { AuthUser } from '@app/core' | 1 | import { AuthUser } from '@app/core' |
2 | import { User } from '@app/core/users/user.model' | 2 | import { User } from '@app/core/users/user.model' |
3 | import { durationToString, getAbsoluteAPIUrl, getAbsoluteEmbedUrl, prepareIcu } from '@app/helpers' | 3 | import { durationToString, formatICU, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers' |
4 | import { Actor } from '@app/shared/shared-main/account/actor.model' | 4 | import { Actor } from '@app/shared/shared-main/account/actor.model' |
5 | import { buildVideoWatchPath, getAllFiles } from '@shared/core-utils' | 5 | import { buildVideoWatchPath, getAllFiles } from '@shared/core-utils' |
6 | import { peertubeTranslate } from '@shared/core-utils/i18n' | 6 | import { peertubeTranslate } from '@shared/core-utils/i18n' |
@@ -19,9 +19,6 @@ import { | |||
19 | } from '@shared/models' | 19 | } from '@shared/models' |
20 | 20 | ||
21 | export class Video implements VideoServerModel { | 21 | export class Video implements VideoServerModel { |
22 | private static readonly viewsICU = prepareIcu($localize`{views, plural, =0 {No view} =1 {1 view} other {{views} views}}`) | ||
23 | private static readonly viewersICU = prepareIcu($localize`{viewers, plural, =0 {No viewers} =1 {1 viewer} other {{viewers} viewers}}`) | ||
24 | |||
25 | byVideoChannel: string | 22 | byVideoChannel: string |
26 | byAccount: string | 23 | byAccount: string |
27 | 24 | ||
@@ -255,7 +252,7 @@ export class Video implements VideoServerModel { | |||
255 | user && user.hasRight(UserRight.MANAGE_VIDEO_FILES) && | 252 | user && user.hasRight(UserRight.MANAGE_VIDEO_FILES) && |
256 | this.state.id !== VideoState.TO_TRANSCODE && | 253 | this.state.id !== VideoState.TO_TRANSCODE && |
257 | this.hasHLS() && | 254 | this.hasHLS() && |
258 | this.hasWebTorrent() | 255 | this.hasWebVideos() |
259 | } | 256 | } |
260 | 257 | ||
261 | canRunTranscoding (user: AuthUser) { | 258 | canRunTranscoding (user: AuthUser) { |
@@ -268,7 +265,7 @@ export class Video implements VideoServerModel { | |||
268 | return this.streamingPlaylists?.some(p => p.type === VideoStreamingPlaylistType.HLS) | 265 | return this.streamingPlaylists?.some(p => p.type === VideoStreamingPlaylistType.HLS) |
269 | } | 266 | } |
270 | 267 | ||
271 | hasWebTorrent () { | 268 | hasWebVideos () { |
272 | return this.files && this.files.length !== 0 | 269 | return this.files && this.files.length !== 0 |
273 | } | 270 | } |
274 | 271 | ||
@@ -281,11 +278,18 @@ export class Video implements VideoServerModel { | |||
281 | return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES) | 278 | return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES) |
282 | } | 279 | } |
283 | 280 | ||
281 | canAccessPasswordProtectedVideoWithoutPassword (user: AuthUser) { | ||
282 | return this.privacy.id === VideoPrivacy.PASSWORD_PROTECTED && | ||
283 | user && | ||
284 | this.isLocal === true && | ||
285 | (this.account.name === user.username || user.hasRight(UserRight.SEE_ALL_VIDEOS)) | ||
286 | } | ||
287 | |||
284 | getExactNumberOfViews () { | 288 | getExactNumberOfViews () { |
285 | if (this.isLive) { | 289 | if (this.isLive) { |
286 | return Video.viewersICU({ viewers: this.viewers }, $localize`${this.viewers} viewer(s)`) | 290 | return formatICU($localize`{viewers, plural, =0 {No viewers} =1 {1 viewer} other {{viewers} viewers}}`, { viewers: this.viewers }) |
287 | } | 291 | } |
288 | 292 | ||
289 | return Video.viewsICU({ views: this.views }, $localize`{${this.views} view(s)}`) | 293 | return formatICU($localize`{views, plural, =0 {No view} =1 {1 view} other {{views} views}}`, { views: this.views }) |
290 | } | 294 | } |
291 | } | 295 | } |
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index 78a49567f..20145b9c5 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts | |||
@@ -11,6 +11,7 @@ import { | |||
11 | FeedFormat, | 11 | FeedFormat, |
12 | NSFWPolicyType, | 12 | NSFWPolicyType, |
13 | ResultList, | 13 | ResultList, |
14 | Storyboard, | ||
14 | UserVideoRate, | 15 | UserVideoRate, |
15 | UserVideoRateType, | 16 | UserVideoRateType, |
16 | UserVideoRateUpdate, | 17 | UserVideoRateUpdate, |
@@ -33,6 +34,7 @@ import { VideoChannel, VideoChannelService } from '../video-channel' | |||
33 | import { VideoDetails } from './video-details.model' | 34 | import { VideoDetails } from './video-details.model' |
34 | import { VideoEdit } from './video-edit.model' | 35 | import { VideoEdit } from './video-edit.model' |
35 | import { Video } from './video.model' | 36 | import { Video } from './video.model' |
37 | import { VideoPasswordService } from './video-password.service' | ||
36 | 38 | ||
37 | export type CommonVideoParams = { | 39 | export type CommonVideoParams = { |
38 | videoPagination?: ComponentPaginationLight | 40 | videoPagination?: ComponentPaginationLight |
@@ -69,16 +71,17 @@ export class VideoService { | |||
69 | return `${VideoService.BASE_VIDEO_URL}/${uuid}/views` | 71 | return `${VideoService.BASE_VIDEO_URL}/${uuid}/views` |
70 | } | 72 | } |
71 | 73 | ||
72 | getVideo (options: { videoId: string }): Observable<VideoDetails> { | 74 | getVideo (options: { videoId: string, videoPassword?: string }): Observable<VideoDetails> { |
73 | return this.serverService.getServerLocale() | 75 | const headers = VideoPasswordService.buildVideoPasswordHeader(options.videoPassword) |
74 | .pipe( | 76 | |
75 | switchMap(translations => { | 77 | return this.serverService.getServerLocale().pipe( |
76 | return this.authHttp.get<VideoDetailsServerModel>(`${VideoService.BASE_VIDEO_URL}/${options.videoId}`) | 78 | switchMap(translations => { |
77 | .pipe(map(videoHash => ({ videoHash, translations }))) | 79 | return this.authHttp.get<VideoDetailsServerModel>(`${VideoService.BASE_VIDEO_URL}/${options.videoId}`, { headers }) |
78 | }), | 80 | .pipe(map(videoHash => ({ videoHash, translations }))) |
79 | map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), | 81 | }), |
80 | catchError(err => this.restExtractor.handleError(err)) | 82 | map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), |
81 | ) | 83 | catchError(err => this.restExtractor.handleError(err)) |
84 | ) | ||
82 | } | 85 | } |
83 | 86 | ||
84 | updateVideo (video: VideoEdit) { | 87 | updateVideo (video: VideoEdit) { |
@@ -99,6 +102,9 @@ export class VideoService { | |||
99 | description, | 102 | description, |
100 | channelId: video.channelId, | 103 | channelId: video.channelId, |
101 | privacy: video.privacy, | 104 | privacy: video.privacy, |
105 | videoPasswords: video.privacy === VideoPrivacy.PASSWORD_PROTECTED | ||
106 | ? [ video.videoPassword ] | ||
107 | : undefined, | ||
102 | tags: video.tags, | 108 | tags: video.tags, |
103 | nsfw: video.nsfw, | 109 | nsfw: video.nsfw, |
104 | waitTranscoding: video.waitTranscoding, | 110 | waitTranscoding: video.waitTranscoding, |
@@ -305,7 +311,7 @@ export class VideoService { | |||
305 | ) | 311 | ) |
306 | } | 312 | } |
307 | 313 | ||
308 | removeVideoFiles (videoIds: (number | string)[], type: 'hls' | 'webtorrent') { | 314 | removeVideoFiles (videoIds: (number | string)[], type: 'hls' | 'web-videos') { |
309 | return from(videoIds) | 315 | return from(videoIds) |
310 | .pipe( | 316 | .pipe( |
311 | concatMap(id => this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + id + '/' + type)), | 317 | concatMap(id => this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + id + '/' + type)), |
@@ -314,12 +320,12 @@ export class VideoService { | |||
314 | ) | 320 | ) |
315 | } | 321 | } |
316 | 322 | ||
317 | removeFile (videoId: number | string, fileId: number, type: 'hls' | 'webtorrent') { | 323 | removeFile (videoId: number | string, fileId: number, type: 'hls' | 'web-videos') { |
318 | return this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + videoId + '/' + type + '/' + fileId) | 324 | return this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + videoId + '/' + type + '/' + fileId) |
319 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 325 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
320 | } | 326 | } |
321 | 327 | ||
322 | runTranscoding (videoIds: (number | string)[], type: 'hls' | 'webtorrent') { | 328 | runTranscoding (videoIds: (number | string)[], type: 'hls' | 'web-video') { |
323 | const body: VideoTranscodingCreate = { transcodingType: type } | 329 | const body: VideoTranscodingCreate = { transcodingType: type } |
324 | 330 | ||
325 | return from(videoIds) | 331 | return from(videoIds) |
@@ -339,6 +345,27 @@ export class VideoService { | |||
339 | ) | 345 | ) |
340 | } | 346 | } |
341 | 347 | ||
348 | // --------------------------------------------------------------------------- | ||
349 | |||
350 | getStoryboards (videoId: string | number, videoPassword: string) { | ||
351 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
352 | |||
353 | return this.authHttp | ||
354 | .get<{ storyboards: Storyboard[] }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/storyboards', { headers }) | ||
355 | .pipe( | ||
356 | map(({ storyboards }) => storyboards), | ||
357 | catchError(err => { | ||
358 | if (err.status === 404) { | ||
359 | return of([]) | ||
360 | } | ||
361 | |||
362 | this.restExtractor.handleError(err) | ||
363 | }) | ||
364 | ) | ||
365 | } | ||
366 | |||
367 | // --------------------------------------------------------------------------- | ||
368 | |||
342 | getSource (videoId: number) { | 369 | getSource (videoId: number) { |
343 | return this.authHttp | 370 | return this.authHttp |
344 | .get<{ source: VideoSource }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source') | 371 | .get<{ source: VideoSource }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source') |
@@ -353,18 +380,22 @@ export class VideoService { | |||
353 | ) | 380 | ) |
354 | } | 381 | } |
355 | 382 | ||
356 | setVideoLike (id: string) { | 383 | // --------------------------------------------------------------------------- |
357 | return this.setVideoRate(id, 'like') | 384 | |
385 | setVideoLike (id: string, videoPassword: string) { | ||
386 | return this.setVideoRate(id, 'like', videoPassword) | ||
358 | } | 387 | } |
359 | 388 | ||
360 | setVideoDislike (id: string) { | 389 | setVideoDislike (id: string, videoPassword: string) { |
361 | return this.setVideoRate(id, 'dislike') | 390 | return this.setVideoRate(id, 'dislike', videoPassword) |
362 | } | 391 | } |
363 | 392 | ||
364 | unsetVideoLike (id: string) { | 393 | unsetVideoLike (id: string, videoPassword: string) { |
365 | return this.setVideoRate(id, 'none') | 394 | return this.setVideoRate(id, 'none', videoPassword) |
366 | } | 395 | } |
367 | 396 | ||
397 | // --------------------------------------------------------------------------- | ||
398 | |||
368 | getUserVideoRating (id: string) { | 399 | getUserVideoRating (id: string) { |
369 | const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating' | 400 | const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating' |
370 | 401 | ||
@@ -394,7 +425,8 @@ export class VideoService { | |||
394 | [VideoPrivacy.PRIVATE]: $localize`Only I can see this video`, | 425 | [VideoPrivacy.PRIVATE]: $localize`Only I can see this video`, |
395 | [VideoPrivacy.UNLISTED]: $localize`Only shareable via a private link`, | 426 | [VideoPrivacy.UNLISTED]: $localize`Only shareable via a private link`, |
396 | [VideoPrivacy.PUBLIC]: $localize`Anyone can see this video`, | 427 | [VideoPrivacy.PUBLIC]: $localize`Anyone can see this video`, |
397 | [VideoPrivacy.INTERNAL]: $localize`Only users of this instance can see this video` | 428 | [VideoPrivacy.INTERNAL]: $localize`Only users of this instance can see this video`, |
429 | [VideoPrivacy.PASSWORD_PROTECTED]: $localize`Only users with the appropriate password can see this video` | ||
398 | } | 430 | } |
399 | 431 | ||
400 | const videoPrivacies = serverPrivacies.map(p => { | 432 | const videoPrivacies = serverPrivacies.map(p => { |
@@ -412,7 +444,13 @@ export class VideoService { | |||
412 | } | 444 | } |
413 | 445 | ||
414 | getHighestAvailablePrivacy (serverPrivacies: VideoConstant<VideoPrivacy>[]) { | 446 | getHighestAvailablePrivacy (serverPrivacies: VideoConstant<VideoPrivacy>[]) { |
415 | const order = [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC ] | 447 | // We do not add a password as this requires additional configuration. |
448 | const order = [ | ||
449 | VideoPrivacy.PRIVATE, | ||
450 | VideoPrivacy.INTERNAL, | ||
451 | VideoPrivacy.UNLISTED, | ||
452 | VideoPrivacy.PUBLIC | ||
453 | ] | ||
416 | 454 | ||
417 | for (const privacy of order) { | 455 | for (const privacy of order) { |
418 | if (serverPrivacies.find(p => p.id === privacy)) { | 456 | if (serverPrivacies.find(p => p.id === privacy)) { |
@@ -499,14 +537,15 @@ export class VideoService { | |||
499 | } | 537 | } |
500 | } | 538 | } |
501 | 539 | ||
502 | private setVideoRate (id: string, rateType: UserVideoRateType) { | 540 | private setVideoRate (id: string, rateType: UserVideoRateType, videoPassword?: string) { |
503 | const url = `${VideoService.BASE_VIDEO_URL}/${id}/rate` | 541 | const url = `${VideoService.BASE_VIDEO_URL}/${id}/rate` |
504 | const body: UserVideoRateUpdate = { | 542 | const body: UserVideoRateUpdate = { |
505 | rating: rateType | 543 | rating: rateType |
506 | } | 544 | } |
545 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
507 | 546 | ||
508 | return this.authHttp | 547 | return this.authHttp |
509 | .put(url, body) | 548 | .put(url, body, { headers }) |
510 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 549 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
511 | } | 550 | } |
512 | } | 551 | } |
diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts index 27dcf043a..34295c34a 100644 --- a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts +++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { forkJoin } from 'rxjs' | 1 | import { forkJoin } from 'rxjs' |
2 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 2 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { prepareIcu } from '@app/helpers' | 4 | import { formatICU } from '@app/helpers' |
5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
@@ -67,9 +67,9 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
67 | let message: string | 67 | let message: string |
68 | 68 | ||
69 | if (Array.isArray(this.usersToBan)) { | 69 | if (Array.isArray(this.usersToBan)) { |
70 | message = prepareIcu($localize`{count, plural, =1 {1 user banned.} other {{count} users banned.}}`)( | 70 | message = formatICU( |
71 | { count: this.usersToBan.length }, | 71 | $localize`{count, plural, =1 {1 user banned.} other {{count} users banned.}}`, |
72 | $localize`${this.usersToBan.length} users banned.` | 72 | { count: this.usersToBan.length } |
73 | ) | 73 | ) |
74 | } else { | 74 | } else { |
75 | message = $localize`User ${this.usersToBan.username} banned.` | 75 | message = $localize`User ${this.usersToBan.username} banned.` |
@@ -88,9 +88,9 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
88 | 88 | ||
89 | getModalTitle () { | 89 | getModalTitle () { |
90 | if (Array.isArray(this.usersToBan)) { | 90 | if (Array.isArray(this.usersToBan)) { |
91 | return prepareIcu($localize`Ban {count, plural, =1 {1 user} other {{count} users}}`)( | 91 | return formatICU( |
92 | { count: this.usersToBan.length }, | 92 | $localize`Ban {count, plural, =1 {1 user} other {{count} users}}`, |
93 | $localize`Ban ${this.usersToBan.length} users` | 93 | { count: this.usersToBan.length } |
94 | ) | 94 | ) |
95 | } | 95 | } |
96 | 96 | ||
diff --git a/client/src/app/shared/shared-moderation/video-block.component.ts b/client/src/app/shared/shared-moderation/video-block.component.ts index 3ff53443a..0137def89 100644 --- a/client/src/app/shared/shared-moderation/video-block.component.ts +++ b/client/src/app/shared/shared-moderation/video-block.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { prepareIcu } from '@app/helpers' | 3 | import { formatICU } from '@app/helpers' |
4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | import { Video } from '@app/shared/shared-main' | 5 | import { Video } from '@app/shared/shared-main' |
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
@@ -81,9 +81,9 @@ export class VideoBlockComponent extends FormReactive implements OnInit { | |||
81 | this.videoBlocklistService.blockVideo(options) | 81 | this.videoBlocklistService.blockVideo(options) |
82 | .subscribe({ | 82 | .subscribe({ |
83 | next: () => { | 83 | next: () => { |
84 | const message = prepareIcu($localize`{count, plural, =1 {Blocked {videoName}.} other {Blocked {count} videos.}}`)( | 84 | const message = formatICU( |
85 | { count: this.videos.length, videoName: this.getSingleVideo().name }, | 85 | $localize`{count, plural, =1 {Blocked {videoName}.} other {Blocked {count} videos.}}`, |
86 | $localize`Blocked ${this.videos.length} videos.` | 86 | { count: this.videos.length, videoName: this.getSingleVideo().name } |
87 | ) | 87 | ) |
88 | 88 | ||
89 | this.notifier.success(message) | 89 | this.notifier.success(message) |
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.html b/client/src/app/shared/shared-share-modal/video-share.component.html index 5650fa948..9f1455561 100644 --- a/client/src/app/shared/shared-share-modal/video-share.component.html +++ b/client/src/app/shared/shared-share-modal/video-share.component.html | |||
@@ -107,6 +107,10 @@ | |||
107 | </a> | 107 | </a> |
108 | </div> | 108 | </div> |
109 | 109 | ||
110 | <div i18n *ngIf="isPasswordProtectedVideo()" class="alert-private alert alert-warning"> | ||
111 | This video is password protected, please note that recipients will require the corresponding password to access the content. | ||
112 | </div> | ||
113 | |||
110 | <div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activeVideoId"> | 114 | <div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activeVideoId"> |
111 | 115 | ||
112 | <ng-container ngbNavItem="url"> | 116 | <ng-container ngbNavItem="url"> |
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.ts b/client/src/app/shared/shared-share-modal/video-share.component.ts index 32f900f15..da4f2a4b4 100644 --- a/client/src/app/shared/shared-share-modal/video-share.component.ts +++ b/client/src/app/shared/shared-share-modal/video-share.component.ts | |||
@@ -243,6 +243,10 @@ export class VideoShareComponent { | |||
243 | return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE | 243 | return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE |
244 | } | 244 | } |
245 | 245 | ||
246 | isPasswordProtectedVideo () { | ||
247 | return this.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
248 | } | ||
249 | |||
246 | private getPlaylistOptions (baseUrl?: string) { | 250 | private getPlaylistOptions (baseUrl?: string) { |
247 | return { | 251 | return { |
248 | baseUrl, | 252 | baseUrl, |
diff --git a/client/src/app/shared/shared-video-comment/video-comment.service.ts b/client/src/app/shared/shared-video-comment/video-comment.service.ts index 8d2deedf7..3906652be 100644 --- a/client/src/app/shared/shared-video-comment/video-comment.service.ts +++ b/client/src/app/shared/shared-video-comment/video-comment.service.ts | |||
@@ -18,6 +18,7 @@ import { | |||
18 | import { environment } from '../../../environments/environment' | 18 | import { environment } from '../../../environments/environment' |
19 | import { VideoCommentThreadTree } from './video-comment-thread-tree.model' | 19 | import { VideoCommentThreadTree } from './video-comment-thread-tree.model' |
20 | import { VideoComment } from './video-comment.model' | 20 | import { VideoComment } from './video-comment.model' |
21 | import { VideoPasswordService } from '../shared-main' | ||
21 | 22 | ||
22 | @Injectable() | 23 | @Injectable() |
23 | export class VideoCommentService { | 24 | export class VideoCommentService { |
@@ -31,22 +32,25 @@ export class VideoCommentService { | |||
31 | private restService: RestService | 32 | private restService: RestService |
32 | ) {} | 33 | ) {} |
33 | 34 | ||
34 | addCommentThread (videoId: string, comment: VideoCommentCreate) { | 35 | addCommentThread (videoId: string, comment: VideoCommentCreate, videoPassword?: string) { |
36 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
35 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' | 37 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' |
36 | const normalizedComment = objectLineFeedToHtml(comment, 'text') | 38 | const normalizedComment = objectLineFeedToHtml(comment, 'text') |
37 | 39 | ||
38 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) | 40 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment, { headers }) |
39 | .pipe( | 41 | .pipe( |
40 | map(data => this.extractVideoComment(data.comment)), | 42 | map(data => this.extractVideoComment(data.comment)), |
41 | catchError(err => this.restExtractor.handleError(err)) | 43 | catchError(err => this.restExtractor.handleError(err)) |
42 | ) | 44 | ) |
43 | } | 45 | } |
44 | 46 | ||
45 | addCommentReply (videoId: string, inReplyToCommentId: number, comment: VideoCommentCreate) { | 47 | addCommentReply (options: { videoId: string, inReplyToCommentId: number, comment: VideoCommentCreate, videoPassword?: string }) { |
48 | const { videoId, inReplyToCommentId, comment, videoPassword } = options | ||
49 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
46 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId | 50 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId |
47 | const normalizedComment = objectLineFeedToHtml(comment, 'text') | 51 | const normalizedComment = objectLineFeedToHtml(comment, 'text') |
48 | 52 | ||
49 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) | 53 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment, { headers }) |
50 | .pipe( | 54 | .pipe( |
51 | map(data => this.extractVideoComment(data.comment)), | 55 | map(data => this.extractVideoComment(data.comment)), |
52 | catchError(err => this.restExtractor.handleError(err)) | 56 | catchError(err => this.restExtractor.handleError(err)) |
@@ -76,10 +80,13 @@ export class VideoCommentService { | |||
76 | 80 | ||
77 | getVideoCommentThreads (parameters: { | 81 | getVideoCommentThreads (parameters: { |
78 | videoId: string | 82 | videoId: string |
83 | videoPassword: string | ||
79 | componentPagination: ComponentPaginationLight | 84 | componentPagination: ComponentPaginationLight |
80 | sort: string | 85 | sort: string |
81 | }): Observable<ThreadsResultList<VideoComment>> { | 86 | }): Observable<ThreadsResultList<VideoComment>> { |
82 | const { videoId, componentPagination, sort } = parameters | 87 | const { videoId, videoPassword, componentPagination, sort } = parameters |
88 | |||
89 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
83 | 90 | ||
84 | const pagination = this.restService.componentToRestPagination(componentPagination) | 91 | const pagination = this.restService.componentToRestPagination(componentPagination) |
85 | 92 | ||
@@ -87,7 +94,7 @@ export class VideoCommentService { | |||
87 | params = this.restService.addRestGetParams(params, pagination, sort) | 94 | params = this.restService.addRestGetParams(params, pagination, sort) |
88 | 95 | ||
89 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' | 96 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' |
90 | return this.authHttp.get<ThreadsResultList<VideoComment>>(url, { params }) | 97 | return this.authHttp.get<ThreadsResultList<VideoComment>>(url, { params, headers }) |
91 | .pipe( | 98 | .pipe( |
92 | map(result => this.extractVideoComments(result)), | 99 | map(result => this.extractVideoComments(result)), |
93 | catchError(err => this.restExtractor.handleError(err)) | 100 | catchError(err => this.restExtractor.handleError(err)) |
@@ -97,12 +104,14 @@ export class VideoCommentService { | |||
97 | getVideoThreadComments (parameters: { | 104 | getVideoThreadComments (parameters: { |
98 | videoId: string | 105 | videoId: string |
99 | threadId: number | 106 | threadId: number |
107 | videoPassword?: string | ||
100 | }): Observable<VideoCommentThreadTree> { | 108 | }): Observable<VideoCommentThreadTree> { |
101 | const { videoId, threadId } = parameters | 109 | const { videoId, threadId, videoPassword } = parameters |
102 | const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` | 110 | const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` |
111 | const headers = VideoPasswordService.buildVideoPasswordHeader(videoPassword) | ||
103 | 112 | ||
104 | return this.authHttp | 113 | return this.authHttp |
105 | .get<VideoCommentThreadTreeServerModel>(url) | 114 | .get<VideoCommentThreadTreeServerModel>(url, { headers }) |
106 | .pipe( | 115 | .pipe( |
107 | map(tree => this.extractVideoCommentTree(tree)), | 116 | map(tree => this.extractVideoCommentTree(tree)), |
108 | catchError(err => this.restExtractor.handleError(err)) | 117 | catchError(err => this.restExtractor.handleError(err)) |
diff --git a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts index 56527ddfa..0a3ada711 100644 --- a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts | |||
@@ -273,7 +273,7 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
273 | }) | 273 | }) |
274 | } | 274 | } |
275 | 275 | ||
276 | async removeVideoFiles (video: Video, type: 'hls' | 'webtorrent') { | 276 | async removeVideoFiles (video: Video, type: 'hls' | 'web-videos') { |
277 | const confirmMessage = $localize`Do you really want to remove "${this.video.name}" files?` | 277 | const confirmMessage = $localize`Do you really want to remove "${this.video.name}" files?` |
278 | 278 | ||
279 | const res = await this.confirmService.confirm(confirmMessage, $localize`Remove "${this.video.name}" files`) | 279 | const res = await this.confirmService.confirm(confirmMessage, $localize`Remove "${this.video.name}" files`) |
@@ -290,7 +290,7 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
290 | }) | 290 | }) |
291 | } | 291 | } |
292 | 292 | ||
293 | runTranscoding (video: Video, type: 'hls' | 'webtorrent') { | 293 | runTranscoding (video: Video, type: 'hls' | 'web-video') { |
294 | this.videoService.runTranscoding([ video.id ], type) | 294 | this.videoService.runTranscoding([ video.id ], type) |
295 | .subscribe({ | 295 | .subscribe({ |
296 | next: () => { | 296 | next: () => { |
@@ -394,8 +394,8 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
394 | iconName: 'cog' | 394 | iconName: 'cog' |
395 | }, | 395 | }, |
396 | { | 396 | { |
397 | label: $localize`Run WebTorrent transcoding`, | 397 | label: $localize`Run Web Video transcoding`, |
398 | handler: ({ video }) => this.runTranscoding(video, 'webtorrent'), | 398 | handler: ({ video }) => this.runTranscoding(video, 'web-video'), |
399 | isDisplayed: () => this.displayOptions.transcoding && this.canRunTranscoding(), | 399 | isDisplayed: () => this.displayOptions.transcoding && this.canRunTranscoding(), |
400 | iconName: 'cog' | 400 | iconName: 'cog' |
401 | }, | 401 | }, |
@@ -406,8 +406,8 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
406 | iconName: 'delete' | 406 | iconName: 'delete' |
407 | }, | 407 | }, |
408 | { | 408 | { |
409 | label: $localize`Delete WebTorrent files`, | 409 | label: $localize`Delete Web Video files`, |
410 | handler: ({ video }) => this.removeVideoFiles(video, 'webtorrent'), | 410 | handler: ({ video }) => this.removeVideoFiles(video, 'web-videos'), |
411 | isDisplayed: () => this.displayOptions.removeFiles && this.canRemoveVideoFiles(), | 411 | isDisplayed: () => this.displayOptions.removeFiles && this.canRemoveVideoFiles(), |
412 | iconName: 'delete' | 412 | iconName: 'delete' |
413 | } | 413 | } |
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts index cac82d8d0..146ea7dfe 100644 --- a/client/src/app/shared/shared-video-miniature/video-download.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-download.component.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import { mapValues } from 'lodash-es' | 1 | import { mapValues } from 'lodash-es' |
2 | import { firstValueFrom } from 'rxjs' | 2 | import { firstValueFrom } from 'rxjs' |
3 | import { tap } from 'rxjs/operators' | 3 | import { tap } from 'rxjs/operators' |
4 | import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' | 4 | import { Component, ElementRef, Inject, Input, LOCALE_ID, ViewChild } from '@angular/core' |
5 | import { HooksService } from '@app/core' | 5 | import { HooksService } from '@app/core' |
6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
7 | import { logger } from '@root-helpers/logger' | 7 | import { logger } from '@root-helpers/logger' |
8 | import { videoRequiresAuth } from '@root-helpers/video' | 8 | import { videoRequiresFileToken } from '@root-helpers/video' |
9 | import { objectKeysTyped, pick } from '@shared/core-utils' | 9 | import { objectKeysTyped, pick } from '@shared/core-utils' |
10 | import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' | 10 | import { VideoCaption, VideoFile } from '@shared/models' |
11 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main' | 11 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main' |
12 | 12 | ||
13 | type DownloadType = 'video' | 'subtitles' | 13 | type DownloadType = 'video' | 'subtitles' |
@@ -21,6 +21,8 @@ type FileMetadata = { [key: string]: { label: string, value: string | number } } | |||
21 | export class VideoDownloadComponent { | 21 | export class VideoDownloadComponent { |
22 | @ViewChild('modal', { static: true }) modal: ElementRef | 22 | @ViewChild('modal', { static: true }) modal: ElementRef |
23 | 23 | ||
24 | @Input() videoPassword: string | ||
25 | |||
24 | downloadType: 'direct' | 'torrent' = 'direct' | 26 | downloadType: 'direct' | 'torrent' = 'direct' |
25 | 27 | ||
26 | resolutionId: number | string = -1 | 28 | resolutionId: number | string = -1 |
@@ -89,8 +91,8 @@ export class VideoDownloadComponent { | |||
89 | this.subtitleLanguageId = this.videoCaptions[0].language.id | 91 | this.subtitleLanguageId = this.videoCaptions[0].language.id |
90 | } | 92 | } |
91 | 93 | ||
92 | if (videoRequiresAuth(this.video)) { | 94 | if (this.isConfidentialVideo()) { |
93 | this.videoFileTokenService.getVideoFileToken(this.video.uuid) | 95 | this.videoFileTokenService.getVideoFileToken({ videoUUID: this.video.uuid, videoPassword: this.videoPassword }) |
94 | .subscribe(({ token }) => this.videoFileToken = token) | 96 | .subscribe(({ token }) => this.videoFileToken = token) |
95 | } | 97 | } |
96 | 98 | ||
@@ -201,7 +203,8 @@ export class VideoDownloadComponent { | |||
201 | } | 203 | } |
202 | 204 | ||
203 | isConfidentialVideo () { | 205 | isConfidentialVideo () { |
204 | return this.video.privacy.id === VideoPrivacy.PRIVATE || this.video.privacy.id === VideoPrivacy.INTERNAL | 206 | return videoRequiresFileToken(this.video) |
207 | |||
205 | } | 208 | } |
206 | 209 | ||
207 | switchToType (type: DownloadType) { | 210 | switchToType (type: DownloadType) { |
diff --git a/client/src/app/shared/shared-video-miniature/video-filters-header.component.html b/client/src/app/shared/shared-video-miniature/video-filters-header.component.html index 3d39c6fdc..3fbfaed28 100644 --- a/client/src/app/shared/shared-video-miniature/video-filters-header.component.html +++ b/client/src/app/shared/shared-video-miniature/video-filters-header.component.html | |||
@@ -125,7 +125,7 @@ | |||
125 | <my-peertube-checkbox | 125 | <my-peertube-checkbox |
126 | formControlName="allVideos" | 126 | formControlName="allVideos" |
127 | inputName="allVideos" | 127 | inputName="allVideos" |
128 | i18n-labelText labelText="Display all videos (private, unlisted or not yet published)" | 128 | i18n-labelText labelText="Display all videos (private, unlisted, password protected or not yet published)" |
129 | ></my-peertube-checkbox> | 129 | ></my-peertube-checkbox> |
130 | </div> | 130 | </div> |
131 | </div> | 131 | </div> |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html index 3f0180695..9e0a4f79b 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html | |||
@@ -5,6 +5,7 @@ | |||
5 | > | 5 | > |
6 | <ng-container ngProjectAs="label-warning" *ngIf="displayOptions.privacyLabel && isUnlistedVideo()" i18n>Unlisted</ng-container> | 6 | <ng-container ngProjectAs="label-warning" *ngIf="displayOptions.privacyLabel && isUnlistedVideo()" i18n>Unlisted</ng-container> |
7 | <ng-container ngProjectAs="label-danger" *ngIf="displayOptions.privacyLabel && isPrivateVideo()" i18n>Private</ng-container> | 7 | <ng-container ngProjectAs="label-danger" *ngIf="displayOptions.privacyLabel && isPrivateVideo()" i18n>Private</ng-container> |
8 | <ng-container ngProjectAs="label-danger" *ngIf="displayOptions.privacyLabel && isPasswordProtectedVideo()" i18n>Password protected</ng-container> | ||
8 | </my-video-thumbnail> | 9 | </my-video-thumbnail> |
9 | 10 | ||
10 | <div class="video-bottom"> | 11 | <div class="video-bottom"> |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts index 2384b34d7..d453f37a1 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts | |||
@@ -171,6 +171,10 @@ export class VideoMiniatureComponent implements OnInit { | |||
171 | return this.video.privacy.id === VideoPrivacy.PRIVATE | 171 | return this.video.privacy.id === VideoPrivacy.PRIVATE |
172 | } | 172 | } |
173 | 173 | ||
174 | isPasswordProtectedVideo () { | ||
175 | return this.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
176 | } | ||
177 | |||
174 | getStateLabel (video: Video) { | 178 | getStateLabel (video: Video) { |
175 | if (!video.state) return '' | 179 | if (!video.state) return '' |
176 | 180 | ||
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts index 7b832263e..14a5abd7a 100644 --- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts | |||
@@ -419,6 +419,7 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
419 | this.lastQueryLength = data.length | 419 | this.lastQueryLength = data.length |
420 | 420 | ||
421 | if (reset) this.videos = [] | 421 | if (reset) this.videos = [] |
422 | |||
422 | this.videos = this.videos.concat(data) | 423 | this.videos = this.videos.concat(data) |
423 | 424 | ||
424 | if (this.groupByDate) this.buildGroupedDateLabels() | 425 | if (this.groupByDate) this.buildGroupedDateLabels() |
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html index 75afa0709..882b14c5e 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html +++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html | |||
@@ -21,7 +21,8 @@ | |||
21 | [attr.title]="playlistElement.video.name" | 21 | [attr.title]="playlistElement.video.name" |
22 | >{{ playlistElement.video.name }}</a> | 22 | >{{ playlistElement.video.name }}</a> |
23 | 23 | ||
24 | <span *ngIf="isVideoPrivate()" class="pt-badge badge-yellow">Private</span> | 24 | <span i18n *ngIf="isVideoPrivate()" class="pt-badge badge-yellow">Private</span> |
25 | <span i18n *ngIf="isVideoPasswordProtected()" class="pt-badge badge-yellow">Password protected</span> | ||
25 | </div> | 26 | </div> |
26 | 27 | ||
27 | <span class="video-miniature-created-at-views"> | 28 | <span class="video-miniature-created-at-views"> |
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts index 552ea742b..b9a1d9623 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts | |||
@@ -60,6 +60,10 @@ export class VideoPlaylistElementMiniatureComponent implements OnInit { | |||
60 | return this.playlistElement.video.privacy.id === VideoPrivacy.PRIVATE | 60 | return this.playlistElement.video.privacy.id === VideoPrivacy.PRIVATE |
61 | } | 61 | } |
62 | 62 | ||
63 | isVideoPasswordProtected () { | ||
64 | return this.playlistElement.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED | ||
65 | } | ||
66 | |||
63 | isUnavailable (e: VideoPlaylistElement) { | 67 | isUnavailable (e: VideoPlaylistElement) { |
64 | return e.type === VideoPlaylistElementType.UNAVAILABLE | 68 | return e.type === VideoPlaylistElementType.UNAVAILABLE |
65 | } | 69 | } |