aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html128
-rw-r--r--client/src/app/+about/about.component.html6
-rw-r--r--client/src/app/+accounts/accounts.component.html6
-rw-r--r--client/src/app/+accounts/accounts.component.scss16
-rw-r--r--client/src/app/+accounts/accounts.component.ts9
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html32
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts4
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts12
-rw-r--r--client/src/app/+login/login.component.html3
-rw-r--r--client/src/app/+login/login.component.ts5
-rw-r--r--client/src/app/+plugin-pages/index.ts3
-rw-r--r--client/src/app/+plugin-pages/plugin-pages-routing.module.ts19
-rw-r--r--client/src/app/+plugin-pages/plugin-pages.component.html1
-rw-r--r--client/src/app/+plugin-pages/plugin-pages.component.ts31
-rw-r--r--client/src/app/+plugin-pages/plugin-pages.module.ts21
-rw-r--r--client/src/app/+search/search-filters.component.html25
-rw-r--r--client/src/app/+search/search-filters.component.ts12
-rw-r--r--client/src/app/+search/search.component.ts23
-rw-r--r--client/src/app/+video-channels/video-channels.component.html6
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts14
-rw-r--r--client/src/app/+video-channels/video-channels.module.ts6
-rw-r--r--client/src/app/app-routing.module.ts6
-rw-r--r--client/src/app/core/plugins/plugin.service.ts31
-rw-r--r--client/src/app/core/routing/custom-reuse-strategy.ts2
-rw-r--r--client/src/app/menu/menu.component.html9
-rw-r--r--client/src/app/menu/menu.component.ts10
-rw-r--r--client/src/app/shared/shared-main/account/account.model.ts9
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.html4
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.scss9
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.ts11
-rw-r--r--client/src/app/shared/shared-moderation/blocklist.service.ts20
-rw-r--r--client/src/app/shared/shared-moderation/index.ts1
-rw-r--r--client/src/app/shared/shared-moderation/shared-moderation.module.ts7
-rw-r--r--client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts4
-rw-r--r--client/src/app/shared/shared-search/advanced-search.model.ts30
-rw-r--r--client/src/app/shared/shared-search/search.service.ts14
-rw-r--r--client/src/assets/player/p2p-media-loader/hls-plugin.ts6
-rw-r--r--client/src/assets/player/peertube-plugin.ts52
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-button.ts2
-rw-r--r--client/src/root-helpers/plugins-manager.ts35
-rw-r--r--client/src/sass/player/peertube-skin.scss27
-rw-r--r--client/src/standalone/videos/embed.ts2
-rw-r--r--client/src/types/register-client-option.model.ts9
43 files changed, 511 insertions, 171 deletions
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html
index 1026c4e0d..7f2a6aa77 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -116,95 +116,99 @@
116 <my-custom-markup-container [content]="descriptionContent"></my-custom-markup-container> 116 <my-custom-markup-container [content]="descriptionContent"></my-custom-markup-container>
117 </div> 117 </div>
118 118
119 <div class="anchor" id="moderation"></div> 119 <div myPluginSelector pluginSelectorId="about-instance-moderation">
120 <a 120 <div class="anchor" id="moderation"></div>
121 *ngIf="html.moderationInformation || html.codeOfConduct || html.terms"
122 class="anchor-link"
123 routerLink="/about/instance"
124 fragment="moderation"
125 #anchorLink
126 (click)="onClickCopyLink(anchorLink)">
127 <h2 i18n class="middle-title">
128 MODERATION
129 </h2>
130 </a>
131
132 <div class="block moderation-information" *ngIf="html.moderationInformation">
133 <div class="anchor" id="moderation-information"></div>
134 <a 121 <a
122 *ngIf="html.moderationInformation || html.codeOfConduct || html.terms"
135 class="anchor-link" 123 class="anchor-link"
136 routerLink="/about/instance" 124 routerLink="/about/instance"
137 fragment="moderation-information" 125 fragment="moderation"
138 #anchorLink 126 #anchorLink
139 (click)="onClickCopyLink(anchorLink)"> 127 (click)="onClickCopyLink(anchorLink)">
140 <h3 i18n class="section-title">Moderation information</h3> 128 <h2 i18n class="middle-title">
129 MODERATION
130 </h2>
141 </a> 131 </a>
142 132
143 <div [innerHTML]="html.moderationInformation"></div> 133 <div class="block moderation-information" *ngIf="html.moderationInformation">
144 </div> 134 <div class="anchor" id="moderation-information"></div>
135 <a
136 class="anchor-link"
137 routerLink="/about/instance"
138 fragment="moderation-information"
139 #anchorLink
140 (click)="onClickCopyLink(anchorLink)">
141 <h3 i18n class="section-title">Moderation information</h3>
142 </a>
145 143
146 <div class="block code-of-conduct" *ngIf="html.codeOfConduct"> 144 <div [innerHTML]="html.moderationInformation"></div>
147 <div class="anchor" id="code-of-conduct"></div> 145 </div>
148 <a
149 class="anchor-link"
150 routerLink="/about/instance"
151 fragment="code-of-conduct"
152 #anchorLink
153 (click)="onClickCopyLink(anchorLink)">
154 <h3 i18n class="section-title">Code of conduct</h3>
155 </a>
156 146
157 <div [innerHTML]="html.codeOfConduct"></div> 147 <div class="block code-of-conduct" *ngIf="html.codeOfConduct">
158 </div> 148 <div class="anchor" id="code-of-conduct"></div>
149 <a
150 class="anchor-link"
151 routerLink="/about/instance"
152 fragment="code-of-conduct"
153 #anchorLink
154 (click)="onClickCopyLink(anchorLink)">
155 <h3 i18n class="section-title">Code of conduct</h3>
156 </a>
159 157
160 <div class="block terms"> 158 <div [innerHTML]="html.codeOfConduct"></div>
161 <div class="anchor" id="terms"></div> 159 </div>
162 <a
163 class="anchor-link"
164 routerLink="/about/instance"
165 fragment="terms"
166 #anchorLink
167 (click)="onClickCopyLink(anchorLink)">
168 <h3 i18n class="section-title">Terms</h3>
169 </a>
170 160
171 <div [innerHTML]="html.terms"></div> 161 <div class="block terms">
172 </div> 162 <div class="anchor" id="terms"></div>
163 <a
164 class="anchor-link"
165 routerLink="/about/instance"
166 fragment="terms"
167 #anchorLink
168 (click)="onClickCopyLink(anchorLink)">
169 <h3 i18n class="section-title">Terms</h3>
170 </a>
173 171
174 <div class="anchor" id="other-information"></div> 172 <div [innerHTML]="html.terms"></div>
175 <a 173 </div>
176 *ngIf="html.hardwareInformation" 174 </div>
177 class="anchor-link"
178 routerLink="/about/instance"
179 fragment="other-information"
180 #anchorLink
181 (click)="onClickCopyLink(anchorLink)">
182 <h2 i18n class="middle-title">
183 OTHER INFORMATION
184 </h2>
185 </a>
186 175
187 <div class="block hardware-information" *ngIf="html.hardwareInformation"> 176 <div myPluginSelector pluginSelectorId="about-instance-other-information">
188 <div class="anchor" id="hardware-information"></div> 177 <div class="anchor" id="other-information"></div>
189 <a 178 <a
179 *ngIf="html.hardwareInformation"
190 class="anchor-link" 180 class="anchor-link"
191 routerLink="/about/instance" 181 routerLink="/about/instance"
192 fragment="hardware-information" 182 fragment="other-information"
193 #anchorLink 183 #anchorLink
194 (click)="onClickCopyLink(anchorLink)"> 184 (click)="onClickCopyLink(anchorLink)">
195 <h3 i18n class="section-title">Hardware information</h3> 185 <h2 i18n class="middle-title">
186 OTHER INFORMATION
187 </h2>
196 </a> 188 </a>
197 189
198 <div [innerHTML]="html.hardwareInformation"></div> 190 <div class="block hardware-information" *ngIf="html.hardwareInformation">
191 <div class="anchor" id="hardware-information"></div>
192 <a
193 class="anchor-link"
194 routerLink="/about/instance"
195 fragment="hardware-information"
196 #anchorLink
197 (click)="onClickCopyLink(anchorLink)">
198 <h3 i18n class="section-title">Hardware information</h3>
199 </a>
200
201 <div [innerHTML]="html.hardwareInformation"></div>
202 </div>
199 </div> 203 </div>
200 </div> 204 </div>
201 205
202 <div class="col-md-12 col-xl-6"> 206 <div class="col-md-12 col-xl-6" myPluginSelector pluginSelectorId="about-instance-features">
203 <h2 class="sr-only" i18n>FEATURES</h2> 207 <h2 class="sr-only" i18n>FEATURES</h2>
204 <my-instance-features-table></my-instance-features-table> 208 <my-instance-features-table></my-instance-features-table>
205 </div> 209 </div>
206 210
207 <div class="col"> 211 <div class="col" myPluginSelector pluginSelectorId="about-instance-statistics">
208 <div class="anchor" id="statistics"></div> 212 <div class="anchor" id="statistics"></div>
209 <a 213 <a
210 class="anchor-link" 214 class="anchor-link"
diff --git a/client/src/app/+about/about.component.html b/client/src/app/+about/about.component.html
index 1ab00c5df..63d429ebf 100644
--- a/client/src/app/+about/about.component.html
+++ b/client/src/app/+about/about.component.html
@@ -2,11 +2,11 @@
2 <div class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }"> 2 <div class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }">
3 3
4 <div class="links"> 4 <div class="links">
5 <a i18n routerLink="instance" routerLinkActive="active" class="title-page title-page-about">Instance</a> 5 <a myPluginSelector pluginSelectorId="about-menu-instance" i18n routerLink="instance" routerLinkActive="active" class="title-page title-page-about">Instance</a>
6 6
7 <a i18n routerLink="peertube" routerLinkActive="active" class="title-page title-page-about">PeerTube</a> 7 <a myPluginSelector pluginSelectorId="about-menu-peertube" i18n routerLink="peertube" routerLinkActive="active" class="title-page title-page-about">PeerTube</a>
8 8
9 <a i18n routerLink="follows" routerLinkActive="active" class="title-page title-page-about">Network</a> 9 <a myPluginSelector pluginSelectorId="about-menu-network" i18n routerLink="follows" routerLinkActive="active" class="title-page title-page-about">Network</a>
10 </div> 10 </div>
11 </div> 11 </div>
12 12
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 0bb24de2e..8362e6b7e 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -19,10 +19,8 @@
19 ></my-user-moderation-dropdown> 19 ></my-user-moderation-dropdown>
20 20
21 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> 21 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
22 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> 22
23 <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span> 23 <my-account-block-badges [account]="account"></my-account-block-badges>
24 <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
25 <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
26 </div> 24 </div>
27 25
28 <div class="actor-handle"> 26 <div class="actor-handle">
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index cdd00487b..5043b98c4 100644
--- a/client/src/app/+accounts/accounts.component.scss
+++ b/client/src/app/+accounts/accounts.component.scss
@@ -30,16 +30,10 @@
30 } 30 }
31} 31}
32 32
33my-user-moderation-dropdown, 33my-user-moderation-dropdown {
34.badge { 34 margin: 0 10px;
35 @include margin-left(10px);
36 35
37 position: relative; 36 height: fit-content;
38 top: 3px;
39}
40
41.badge {
42 font-size: 13px;
43} 37}
44 38
45.copy-button { 39.copy-button {
@@ -64,6 +58,10 @@ my-user-moderation-dropdown,
64 @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize)); 58 @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
65} 59}
66 60
61.actor-display-name {
62 align-items: center;
63}
64
67.description { 65.description {
68 grid-column: 1 / 3; 66 grid-column: 1 / 3;
69 max-width: 1000px; 67 max-width: 1000px;
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index 0dcbc250a..898325492 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -12,7 +12,7 @@ import {
12 VideoChannelService, 12 VideoChannelService,
13 VideoService 13 VideoService
14} from '@app/shared/shared-main' 14} from '@app/shared/shared-main'
15import { AccountReportComponent } from '@app/shared/shared-moderation' 15import { AccountReportComponent, BlocklistService } from '@app/shared/shared-moderation'
16import { HttpStatusCode, User, UserRight } from '@shared/models' 16import { HttpStatusCode, User, UserRight } from '@shared/models'
17 17
18@Component({ 18@Component({
@@ -52,6 +52,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
52 private authService: AuthService, 52 private authService: AuthService,
53 private videoService: VideoService, 53 private videoService: VideoService,
54 private markdown: MarkdownService, 54 private markdown: MarkdownService,
55 private blocklist: BlocklistService,
55 private screenService: ScreenService 56 private screenService: ScreenService
56 ) { 57 ) {
57 } 58 }
@@ -159,6 +160,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
159 this.updateModerationActions() 160 this.updateModerationActions()
160 this.loadUserIfNeeded(account) 161 this.loadUserIfNeeded(account)
161 this.loadAccountVideosCount() 162 this.loadAccountVideosCount()
163 this.loadAccountBlockStatus()
162 } 164 }
163 165
164 private showReportModal () { 166 private showReportModal () {
@@ -217,4 +219,9 @@ export class AccountsComponent implements OnInit, OnDestroy {
217 this.accountVideosCount = res.total 219 this.accountVideosCount = res.total
218 }) 220 })
219 } 221 }
222
223 private loadAccountBlockStatus () {
224 this.blocklist.getStatus({ accounts: [ this.account.nameWithHostForced ], hosts: [ this.account.host ] })
225 .subscribe(status => this.account.updateBlockStatus(status))
226 }
220} 227}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
index 318c8e2c2..c9533208a 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
@@ -56,6 +56,36 @@
56 </ng-container> 56 </ng-container>
57 </div> 57 </div>
58 58
59 <ng-container formGroupName="client">
60
61 <ng-container formGroupName="videos">
62 <ng-container formGroupName="miniature">
63 <div class="form-group">
64 <my-peertube-checkbox
65 inputName="clientVideosMiniaturePreferAuthorDisplayName" formControlName="preferAuthorDisplayName"
66 i18n-labelText labelText="Prefer author display name in video miniature"
67 ></my-peertube-checkbox>
68 </div>
69 </ng-container>
70 </ng-container>
71
72 <ng-container formGroupName="menu">
73 <ng-container formGroupName="login">
74 <div class="form-group">
75 <my-peertube-checkbox
76 inputName="clientMenuLoginRedirectOnSingleExternalAuth" formControlName="redirectOnSingleExternalAuth"
77 i18n-labelText labelText="Redirect users on single external auth when users click on the login button in menu"
78 >
79 <ng-container ngProjectAs="description">
80 <span *ngIf="countExternalAuth() === 0" i18n>⚠️ You don't have any external auth plugin enabled.</span>
81 <span *ngIf="countExternalAuth() > 1" i18n>⚠️ You have multiple external auth plugins enabled.</span>
82 </ng-container>
83 </my-peertube-checkbox>
84 </div>
85 </ng-container>
86 </ng-container>
87 </ng-container>
88
59 </div> 89 </div>
60 </div> 90 </div>
61 91
@@ -276,7 +306,7 @@
276 <div class="form-group col-12 col-lg-4 col-xl-3"> 306 <div class="form-group col-12 col-lg-4 col-xl-3">
277 <div i18n class="inner-form-title">VIDEO CHANNELS</div> 307 <div i18n class="inner-form-title">VIDEO CHANNELS</div>
278 </div> 308 </div>
279 309
280 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 310 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
281 <div class="form-group" formGroupName="videoChannels"> 311 <div class="form-group" formGroupName="videoChannels">
282 <label i18n for="videoChannelsMaxPerUser">Max video channels per user</label> 312 <label i18n for="videoChannelsMaxPerUser">Max video channels per user</label>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
index 7a8258820..81457bd36 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
@@ -36,6 +36,10 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
36 } 36 }
37 } 37 }
38 38
39 countExternalAuth () {
40 return this.serverConfig.plugin.registeredExternalAuths.length
41 }
42
39 getVideoQuotaOptions () { 43 getVideoQuotaOptions () {
40 return this.configService.videoQuotaOptions 44 return this.configService.videoQuotaOptions
41 } 45 }
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 fdb0a7532..f2eaa3033 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
@@ -106,6 +106,18 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
106 whitelisted: null 106 whitelisted: null
107 } 107 }
108 }, 108 },
109 client: {
110 videos: {
111 miniature: {
112 preferAuthorDisplayName: null
113 }
114 },
115 menu: {
116 login: {
117 redirectOnSingleExternalAuth: null
118 }
119 }
120 },
109 cache: { 121 cache: {
110 previews: { 122 previews: {
111 size: CACHE_PREVIEWS_SIZE_VALIDATOR 123 size: CACHE_PREVIEWS_SIZE_VALIDATOR
diff --git a/client/src/app/+login/login.component.html b/client/src/app/+login/login.component.html
index 90eea1505..531b06dc9 100644
--- a/client/src/app/+login/login.component.html
+++ b/client/src/app/+login/login.component.html
@@ -48,7 +48,8 @@
48 <input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid"> 48 <input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid">
49 49
50 <div class="additionnal-links"> 50 <div class="additionnal-links">
51 <a i18n class="forgot-password-button" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a> 51 <a i18n role="button" class="forgot-password-button" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a>
52
52 <div *ngIf="signupAllowed" class="signup-link"> 53 <div *ngIf="signupAllowed" class="signup-link">
53 <span>·</span> 54 <span>·</span>
54 <a i18n routerLink="/signup" class="create-an-account">Create an account</a> 55 <a i18n routerLink="/signup" class="create-an-account">Create an account</a>
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts
index 1fa4bd3b5..648b8db36 100644
--- a/client/src/app/+login/login.component.ts
+++ b/client/src/app/+login/login.component.ts
@@ -1,4 +1,4 @@
1import { environment } from 'src/environments/environment' 1
2import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' 2import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute } from '@angular/router' 3import { ActivatedRoute } from '@angular/router'
4import { AuthService, Notifier, RedirectService, UserService } from '@app/core' 4import { AuthService, Notifier, RedirectService, UserService } from '@app/core'
@@ -7,6 +7,7 @@ import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/
7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
8import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' 8import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
10import { PluginsManager } from '@root-helpers/plugins-manager'
10import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' 11import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
11 12
12@Component({ 13@Component({
@@ -98,7 +99,7 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
98 } 99 }
99 100
100 getAuthHref (auth: RegisteredExternalAuthConfig) { 101 getAuthHref (auth: RegisteredExternalAuthConfig) {
101 return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` 102 return PluginsManager.getExternalAuthHref(auth)
102 } 103 }
103 104
104 login () { 105 login () {
diff --git a/client/src/app/+plugin-pages/index.ts b/client/src/app/+plugin-pages/index.ts
new file mode 100644
index 000000000..b988f13f6
--- /dev/null
+++ b/client/src/app/+plugin-pages/index.ts
@@ -0,0 +1,3 @@
1export * from './plugin-pages-routing.module'
2export * from './plugin-pages.component'
3export * from './plugin-pages.module'
diff --git a/client/src/app/+plugin-pages/plugin-pages-routing.module.ts b/client/src/app/+plugin-pages/plugin-pages-routing.module.ts
new file mode 100644
index 000000000..b47a787e0
--- /dev/null
+++ b/client/src/app/+plugin-pages/plugin-pages-routing.module.ts
@@ -0,0 +1,19 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3import { PluginPagesComponent } from './plugin-pages.component'
4
5const pluginPagesRoutes: Routes = [
6 {
7 path: '**',
8 component: PluginPagesComponent,
9 data: {
10 reloadOnSameNavigation: true
11 }
12 }
13]
14
15@NgModule({
16 imports: [ RouterModule.forChild(pluginPagesRoutes) ],
17 exports: [ RouterModule ]
18})
19export class PluginPagesRoutingModule {}
diff --git a/client/src/app/+plugin-pages/plugin-pages.component.html b/client/src/app/+plugin-pages/plugin-pages.component.html
new file mode 100644
index 000000000..cf62d1bd7
--- /dev/null
+++ b/client/src/app/+plugin-pages/plugin-pages.component.html
@@ -0,0 +1 @@
<div #root></div>
diff --git a/client/src/app/+plugin-pages/plugin-pages.component.ts b/client/src/app/+plugin-pages/plugin-pages.component.ts
new file mode 100644
index 000000000..5f294ee13
--- /dev/null
+++ b/client/src/app/+plugin-pages/plugin-pages.component.ts
@@ -0,0 +1,31 @@
1import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { PluginService } from '@app/core'
4
5@Component({
6 templateUrl: './plugin-pages.component.html'
7})
8export class PluginPagesComponent implements AfterViewInit {
9 @ViewChild('root') root: ElementRef
10
11 constructor (
12 private route: ActivatedRoute,
13 private router: Router,
14 private pluginService: PluginService
15 ) {
16
17 }
18
19 ngAfterViewInit () {
20 const path = '/' + this.route.snapshot.url.map(u => u.path).join('/')
21
22 const registered = this.pluginService.getRegisteredClientRoute(path)
23 if (!registered) {
24 console.log('Could not find registered route %s.', path, this.pluginService.getAllRegisteredClientRoutes())
25
26 return this.router.navigate([ '/404' ], { skipLocationChange: true })
27 }
28
29 registered.onMount({ rootEl: this.root.nativeElement })
30 }
31}
diff --git a/client/src/app/+plugin-pages/plugin-pages.module.ts b/client/src/app/+plugin-pages/plugin-pages.module.ts
new file mode 100644
index 000000000..86f86c752
--- /dev/null
+++ b/client/src/app/+plugin-pages/plugin-pages.module.ts
@@ -0,0 +1,21 @@
1import { NgModule } from '@angular/core'
2import { PluginPagesRoutingModule } from './plugin-pages-routing.module'
3import { PluginPagesComponent } from './plugin-pages.component'
4
5@NgModule({
6 imports: [
7 PluginPagesRoutingModule
8 ],
9
10 declarations: [
11 PluginPagesComponent
12 ],
13
14 exports: [
15 PluginPagesComponent
16 ],
17
18 providers: [
19 ]
20})
21export class PluginPagesModule { }
diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html
index 4b87a2102..c4861e8c4 100644
--- a/client/src/app/+search/search-filters.component.html
+++ b/client/src/app/+search/search-filters.component.html
@@ -182,6 +182,31 @@
182 > 182 >
183 </div> 183 </div>
184 184
185 <div class="form-group">
186 <div class="radio-label label-container">
187 <label i18n>Result types</label>
188 <button i18n class="reset-button reset-button-small" (click)="resetField('resultType')" *ngIf="advancedSearch.resultType !== undefined">
189 Reset
190 </button>
191 </div>
192
193 <div class="peertube-radio-container">
194 <input type="radio" name="resultType" id="resultTypeVideos" value="videos" [(ngModel)]="advancedSearch.resultType">
195 <label i18n for="resultTypeVideos" class="radio">Videos</label>
196 </div>
197
198 <div class="peertube-radio-container">
199 <input type="radio" name="resultType" id="resultTypeChannels" value="channels" [(ngModel)]="advancedSearch.resultType">
200 <label i18n for="resultTypeChannels" class="radio">Channels</label>
201 </div>
202
203 <div class="peertube-radio-container">
204 <input type="radio" name="resultType" id="resultTypePlaylists" value="playlists" [(ngModel)]="advancedSearch.resultType">
205 <label i18n for="resultTypePlaylists" class="radio">Playlists</label>
206 </div>
207
208 </div>
209
185 <div class="form-group" *ngIf="isSearchTargetEnabled()"> 210 <div class="form-group" *ngIf="isSearchTargetEnabled()">
186 <div class="radio-label label-container"> 211 <div class="radio-label label-container">
187 <label i18n>Search target</label> 212 <label i18n>Search target</label>
diff --git a/client/src/app/+search/search-filters.component.ts b/client/src/app/+search/search-filters.component.ts
index 5972ba553..aaa4ecc5a 100644
--- a/client/src/app/+search/search-filters.component.ts
+++ b/client/src/app/+search/search-filters.component.ts
@@ -22,7 +22,6 @@ export class SearchFiltersComponent implements OnInit {
22 publishedDateRanges: FormOption[] = [] 22 publishedDateRanges: FormOption[] = []
23 sorts: FormOption[] = [] 23 sorts: FormOption[] = []
24 durationRanges: FormOption[] = [] 24 durationRanges: FormOption[] = []
25 videoType: FormOption[] = []
26 25
27 publishedDateRange: string 26 publishedDateRange: string
28 durationRange: string 27 durationRange: string
@@ -54,17 +53,6 @@ export class SearchFiltersComponent implements OnInit {
54 } 53 }
55 ] 54 ]
56 55
57 this.videoType = [
58 {
59 id: 'vod',
60 label: $localize`VOD videos`
61 },
62 {
63 id: 'live',
64 label: $localize`Live videos`
65 }
66 ]
67
68 this.durationRanges = [ 56 this.durationRanges = [
69 { 57 {
70 id: 'short', 58 id: 'short',
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts
index fcf6ebbec..b9ec6dbcc 100644
--- a/client/src/app/+search/search.component.ts
+++ b/client/src/app/+search/search.component.ts
@@ -47,10 +47,6 @@ export class SearchComponent implements OnInit, OnDestroy {
47 private subActivatedRoute: Subscription 47 private subActivatedRoute: Subscription
48 private isInitialLoad = false // set to false to show the search filters on first arrival 48 private isInitialLoad = false // set to false to show the search filters on first arrival
49 49
50 private channelsPerPage = 2
51 private playlistsPerPage = 2
52 private videosPerPage = 10
53
54 private hasMoreResults = true 50 private hasMoreResults = true
55 private isSearching = false 51 private isSearching = false
56 52
@@ -247,7 +243,6 @@ export class SearchComponent implements OnInit, OnDestroy {
247 private resetPagination () { 243 private resetPagination () {
248 this.pagination.currentPage = 1 244 this.pagination.currentPage = 1
249 this.pagination.totalItems = null 245 this.pagination.totalItems = null
250 this.channelsPerPage = 2
251 246
252 this.results = [] 247 this.results = []
253 } 248 }
@@ -272,7 +267,7 @@ export class SearchComponent implements OnInit, OnDestroy {
272 private getVideosObs () { 267 private getVideosObs () {
273 const params = { 268 const params = {
274 search: this.currentSearch, 269 search: this.currentSearch,
275 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.videosPerPage }), 270 componentPagination: immutableAssign(this.pagination, { itemsPerPage: 10 }),
276 advancedSearch: this.advancedSearch 271 advancedSearch: this.advancedSearch
277 } 272 }
278 273
@@ -288,7 +283,7 @@ export class SearchComponent implements OnInit, OnDestroy {
288 private getVideoChannelObs () { 283 private getVideoChannelObs () {
289 const params = { 284 const params = {
290 search: this.currentSearch, 285 search: this.currentSearch,
291 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }), 286 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.buildChannelsPerPage() }),
292 advancedSearch: this.advancedSearch 287 advancedSearch: this.advancedSearch
293 } 288 }
294 289
@@ -304,7 +299,7 @@ export class SearchComponent implements OnInit, OnDestroy {
304 private getVideoPlaylistObs () { 299 private getVideoPlaylistObs () {
305 const params = { 300 const params = {
306 search: this.currentSearch, 301 search: this.currentSearch,
307 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.playlistsPerPage }), 302 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.buildPlaylistsPerPage() }),
308 advancedSearch: this.advancedSearch 303 advancedSearch: this.advancedSearch
309 } 304 }
310 305
@@ -334,4 +329,16 @@ export class SearchComponent implements OnInit, OnDestroy {
334 329
335 return undefined 330 return undefined
336 } 331 }
332
333 private buildChannelsPerPage () {
334 if (this.advancedSearch.resultType === 'channels') return 10
335
336 return 2
337 }
338
339 private buildPlaylistsPerPage () {
340 if (this.advancedSearch.resultType === 'playlists') return 10
341
342 return 2
343 }
337} 344}
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index 064fbb6f5..aec2e373c 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -23,14 +23,16 @@
23 <div class="section-label" i18n>OWNER ACCOUNT</div> 23 <div class="section-label" i18n>OWNER ACCOUNT</div>
24 24
25 <div class="avatar-row"> 25 <div class="avatar-row">
26 <my-actor-avatar class="account-avatar" [account]="videoChannel.ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar> 26 <my-actor-avatar class="account-avatar" [account]="ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar>
27 27
28 <div class="actor-info"> 28 <div class="actor-info">
29 <h4> 29 <h4>
30 <a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ videoChannel.ownerAccount.displayName }}</a> 30 <a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ ownerAccount.displayName }}</a>
31 </h4> 31 </h4>
32 32
33 <div class="actor-handle">@{{ videoChannel.ownerBy }}</div> 33 <div class="actor-handle">@{{ videoChannel.ownerBy }}</div>
34
35 <my-account-block-badges [account]="ownerAccount"></my-account-block-badges>
34 </div> 36 </div>
35 </div> 37 </div>
36 38
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index 272fc41d9..ebb991f4e 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -4,7 +4,8 @@ import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators
4import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' 4import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
5import { ActivatedRoute } from '@angular/router' 5import { ActivatedRoute } from '@angular/router'
6import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core' 6import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core'
7import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' 7import { Account, ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
8import { BlocklistService } from '@app/shared/shared-moderation'
8import { SupportModalComponent } from '@app/shared/shared-support-modal' 9import { SupportModalComponent } from '@app/shared/shared-support-modal'
9import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 10import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
10import { HttpStatusCode } from '@shared/models' 11import { HttpStatusCode } from '@shared/models'
@@ -18,6 +19,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
18 @ViewChild('supportModal') supportModal: SupportModalComponent 19 @ViewChild('supportModal') supportModal: SupportModalComponent
19 20
20 videoChannel: VideoChannel 21 videoChannel: VideoChannel
22 ownerAccount: Account
21 hotkeys: Hotkey[] 23 hotkeys: Hotkey[]
22 links: ListOverflowItem[] = [] 24 links: ListOverflowItem[] = []
23 isChannelManageable = false 25 isChannelManageable = false
@@ -38,7 +40,8 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
38 private restExtractor: RestExtractor, 40 private restExtractor: RestExtractor,
39 private hotkeysService: HotkeysService, 41 private hotkeysService: HotkeysService,
40 private screenService: ScreenService, 42 private screenService: ScreenService,
41 private markdown: MarkdownService 43 private markdown: MarkdownService,
44 private blocklist: BlocklistService
42 ) { } 45 ) { }
43 46
44 ngOnInit () { 47 ngOnInit () {
@@ -58,8 +61,10 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
58 61
59 // After the markdown renderer to avoid layout changes 62 // After the markdown renderer to avoid layout changes
60 this.videoChannel = videoChannel 63 this.videoChannel = videoChannel
64 this.ownerAccount = new Account(this.videoChannel.ownerAccount)
61 65
62 this.loadChannelVideosCount() 66 this.loadChannelVideosCount()
67 this.loadOwnerBlockStatus()
63 }) 68 })
64 69
65 this.hotkeys = [ 70 this.hotkeys = [
@@ -125,4 +130,9 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
125 sort: '-publishedAt' 130 sort: '-publishedAt'
126 }).subscribe(res => this.channelVideosCount = res.total) 131 }).subscribe(res => this.channelVideosCount = res.total)
127 } 132 }
133
134 private loadOwnerBlockStatus () {
135 this.blocklist.getStatus({ accounts: [ this.ownerAccount.nameWithHostForced ], hosts: [ this.ownerAccount.host ] })
136 .subscribe(status => this.ownerAccount.updateBlockStatus(status))
137 }
128} 138}
diff --git a/client/src/app/+video-channels/video-channels.module.ts b/client/src/app/+video-channels/video-channels.module.ts
index 35c39cc2e..76aaecf83 100644
--- a/client/src/app/+video-channels/video-channels.module.ts
+++ b/client/src/app/+video-channels/video-channels.module.ts
@@ -2,15 +2,16 @@ import { NgModule } from '@angular/core'
2import { SharedFormModule } from '@app/shared/shared-forms' 2import { SharedFormModule } from '@app/shared/shared-forms'
3import { SharedGlobalIconModule } from '@app/shared/shared-icons' 3import { SharedGlobalIconModule } from '@app/shared/shared-icons'
4import { SharedMainModule } from '@app/shared/shared-main' 4import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedModerationModule } from '@app/shared/shared-moderation'
5import { SharedSupportModal } from '@app/shared/shared-support-modal' 6import { SharedSupportModal } from '@app/shared/shared-support-modal'
6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 7import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 8import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
8import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' 9import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
10import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
9import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' 11import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
10import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' 12import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
11import { VideoChannelsRoutingModule } from './video-channels-routing.module' 13import { VideoChannelsRoutingModule } from './video-channels-routing.module'
12import { VideoChannelsComponent } from './video-channels.component' 14import { VideoChannelsComponent } from './video-channels.component'
13import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
14 15
15@NgModule({ 16@NgModule({
16 imports: [ 17 imports: [
@@ -23,7 +24,8 @@ import { SharedActorImageModule } from '../shared/shared-actor-image/shared-acto
23 SharedUserSubscriptionModule, 24 SharedUserSubscriptionModule,
24 SharedGlobalIconModule, 25 SharedGlobalIconModule,
25 SharedSupportModal, 26 SharedSupportModal,
26 SharedActorImageModule 27 SharedActorImageModule,
28 SharedModerationModule
27 ], 29 ],
28 30
29 declarations: [ 31 declarations: [
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts
index 438cb6512..42328d83d 100644
--- a/client/src/app/app-routing.module.ts
+++ b/client/src/app/app-routing.module.ts
@@ -58,6 +58,12 @@ const routes: Routes = [
58 }, 58 },
59 59
60 { 60 {
61 path: 'p',
62 loadChildren: () => import('./+plugin-pages/plugin-pages.module').then(m => m.PluginPagesModule),
63 canActivateChild: [ MetaGuard ]
64 },
65
66 {
61 path: 'about', 67 path: 'about',
62 loadChildren: () => import('./+about/about.module').then(m => m.AboutModule), 68 loadChildren: () => import('./+about/about.module').then(m => m.AboutModule),
63 canActivateChild: [ MetaGuard ] 69 canActivateChild: [ MetaGuard ]
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index 89391c2c5..fdbbd2d56 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -20,7 +20,8 @@ import {
20 PluginType, 20 PluginType,
21 PublicServerSetting, 21 PublicServerSetting,
22 RegisterClientFormFieldOptions, 22 RegisterClientFormFieldOptions,
23 RegisterClientSettingsScript, 23 RegisterClientSettingsScriptOptions,
24 RegisterClientRouteOptions,
24 RegisterClientVideoFieldOptions, 25 RegisterClientVideoFieldOptions,
25 ServerConfigPlugin 26 ServerConfigPlugin
26} from '@shared/models' 27} from '@shared/models'
@@ -48,7 +49,8 @@ export class PluginService implements ClientHook {
48 private formFields: FormFields = { 49 private formFields: FormFields = {
49 video: [] 50 video: []
50 } 51 }
51 private settingsScripts: { [ npmName: string ]: RegisterClientSettingsScript } = {} 52 private settingsScripts: { [ npmName: string ]: RegisterClientSettingsScriptOptions } = {}
53 private clientRoutes: { [ route: string ]: RegisterClientRouteOptions } = {}
52 54
53 private pluginsManager: PluginsManager 55 private pluginsManager: PluginsManager
54 56
@@ -67,7 +69,8 @@ export class PluginService implements ClientHook {
67 this.pluginsManager = new PluginsManager({ 69 this.pluginsManager = new PluginsManager({
68 peertubeHelpersFactory: this.buildPeerTubeHelpers.bind(this), 70 peertubeHelpersFactory: this.buildPeerTubeHelpers.bind(this),
69 onFormFields: this.onFormFields.bind(this), 71 onFormFields: this.onFormFields.bind(this),
70 onSettingsScripts: this.onSettingsScripts.bind(this) 72 onSettingsScripts: this.onSettingsScripts.bind(this),
73 onClientRoute: this.onClientRoute.bind(this)
71 }) 74 })
72 } 75 }
73 76
@@ -123,6 +126,14 @@ export class PluginService implements ClientHook {
123 return this.settingsScripts[npmName] 126 return this.settingsScripts[npmName]
124 } 127 }
125 128
129 getRegisteredClientRoute (route: string) {
130 return this.clientRoutes[route]
131 }
132
133 getAllRegisteredClientRoutes () {
134 return Object.keys(this.clientRoutes)
135 }
136
126 translateBy (npmName: string, toTranslate: string) { 137 translateBy (npmName: string, toTranslate: string) {
127 const helpers = this.helpers[npmName] 138 const helpers = this.helpers[npmName]
128 if (!helpers) { 139 if (!helpers) {
@@ -140,12 +151,20 @@ export class PluginService implements ClientHook {
140 }) 151 })
141 } 152 }
142 153
143 private onSettingsScripts (pluginInfo: PluginInfo, options: RegisterClientSettingsScript) { 154 private onSettingsScripts (pluginInfo: PluginInfo, options: RegisterClientSettingsScriptOptions) {
144 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType) 155 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
145 156
146 this.settingsScripts[npmName] = options 157 this.settingsScripts[npmName] = options
147 } 158 }
148 159
160 private onClientRoute (options: RegisterClientRouteOptions) {
161 const route = options.route.startsWith('/')
162 ? options.route
163 : `/${options.route}`
164
165 this.clientRoutes[route] = options
166 }
167
149 private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers { 168 private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers {
150 const { plugin } = pluginInfo 169 const { plugin } = pluginInfo
151 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType) 170 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
@@ -161,6 +180,10 @@ export class PluginService implements ClientHook {
161 return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/router` 180 return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/router`
162 }, 181 },
163 182
183 getBasePluginClientPath: () => {
184 return '/p'
185 },
186
164 getSettings: () => { 187 getSettings: () => {
165 const path = PluginService.BASE_PLUGIN_API_URL + '/' + npmName + '/public-settings' 188 const path = PluginService.BASE_PLUGIN_API_URL + '/' + npmName + '/public-settings'
166 189
diff --git a/client/src/app/core/routing/custom-reuse-strategy.ts b/client/src/app/core/routing/custom-reuse-strategy.ts
index 1498e221f..5d3ad2e67 100644
--- a/client/src/app/core/routing/custom-reuse-strategy.ts
+++ b/client/src/app/core/routing/custom-reuse-strategy.ts
@@ -58,7 +58,7 @@ export class CustomReuseStrategy implements RouteReuseStrategy {
58 58
59 // Reuse the route if we're going to and from the same route 59 // Reuse the route if we're going to and from the same route
60 shouldReuseRoute (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { 60 shouldReuseRoute (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
61 return future.routeConfig === curr.routeConfig 61 return future.routeConfig === curr.routeConfig && future.routeConfig?.data?.reloadOnSameNavigation !== true
62 } 62 }
63 63
64 private gb () { 64 private gb () {
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index 46dd807ec..9ea991042 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -30,7 +30,10 @@
30 30
31 <div class="dropdown-divider"></div> 31 <div class="dropdown-divider"></div>
32 32
33 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()"> 33 <a
34 myPluginSelector pluginSelectorId="menu-user-dropdown-language-item"
35 ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()"
36 >
34 <my-global-icon iconName="language" aria-hidden="true"></my-global-icon> 37 <my-global-icon iconName="language" aria-hidden="true"></my-global-icon>
35 <span i18n>Interface:</span> 38 <span i18n>Interface:</span>
36 <span class="ml-auto text-muted">{{ currentInterfaceLanguage }}</span> 39 <span class="ml-auto text-muted">{{ currentInterfaceLanguage }}</span>
@@ -96,7 +99,9 @@
96 </div> 99 </div>
97 100
98 <div *ngIf="!isLoggedIn" class="login-buttons-block"> 101 <div *ngIf="!isLoggedIn" class="login-buttons-block">
99 <a i18n routerLink="/login" class="peertube-button-link orange-button">Login</a> 102 <a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a>
103 <a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a>
104
100 <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a> 105 <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a>
101 </div> 106 </div>
102 107
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts
index 97f07c956..d5ddc29cb 100644
--- a/client/src/app/menu/menu.component.ts
+++ b/client/src/app/menu/menu.component.ts
@@ -21,6 +21,7 @@ import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
21import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' 21import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
22import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service' 22import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service'
23import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' 23import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
24import { PluginsManager } from '@root-helpers/plugins-manager'
24import { HTMLServerConfig, ServerConfig, UserRight, VideoConstant } from '@shared/models' 25import { HTMLServerConfig, ServerConfig, UserRight, VideoConstant } from '@shared/models'
25 26
26const logger = debug('peertube:menu:MenuComponent') 27const logger = debug('peertube:menu:MenuComponent')
@@ -129,6 +130,15 @@ export class MenuComponent implements OnInit {
129 .subscribe(() => this.openQuickSettings()) 130 .subscribe(() => this.openQuickSettings())
130 } 131 }
131 132
133 getExternalLoginHref () {
134 if (!this.serverConfig || this.serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined
135
136 const externalAuths = this.serverConfig.plugin.registeredExternalAuths
137 if (externalAuths.length !== 1) return undefined
138
139 return PluginsManager.getExternalAuthHref(externalAuths[0])
140 }
141
132 isRegistrationAllowed () { 142 isRegistrationAllowed () {
133 if (!this.serverConfig) return false 143 if (!this.serverConfig) return false
134 144
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts
index 92606e7fa..8b78d01a6 100644
--- a/client/src/app/shared/shared-main/account/account.model.ts
+++ b/client/src/app/shared/shared-main/account/account.model.ts
@@ -1,4 +1,4 @@
1import { Account as ServerAccount, ActorImage } from '@shared/models' 1import { Account as ServerAccount, ActorImage, BlockStatus } from '@shared/models'
2import { Actor } from './actor.model' 2import { Actor } from './actor.model'
3 3
4export class Account extends Actor implements ServerAccount { 4export class Account extends Actor implements ServerAccount {
@@ -49,4 +49,11 @@ export class Account extends Actor implements ServerAccount {
49 resetAvatar () { 49 resetAvatar () {
50 this.avatar = null 50 this.avatar = null
51 } 51 }
52
53 updateBlockStatus (blockStatus: BlockStatus) {
54 this.mutedByInstance = blockStatus.accounts[this.nameWithHostForced].blockedByServer
55 this.mutedByUser = blockStatus.accounts[this.nameWithHostForced].blockedByUser
56 this.mutedServerByUser = blockStatus.hosts[this.host].blockedByUser
57 this.mutedServerByInstance = blockStatus.hosts[this.host].blockedByServer
58 }
52} 59}
diff --git a/client/src/app/shared/shared-moderation/account-block-badges.component.html b/client/src/app/shared/shared-moderation/account-block-badges.component.html
new file mode 100644
index 000000000..feac707c2
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.html
@@ -0,0 +1,4 @@
1<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
2<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
3<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
4<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
diff --git a/client/src/app/shared/shared-moderation/account-block-badges.component.scss b/client/src/app/shared/shared-moderation/account-block-badges.component.scss
new file mode 100644
index 000000000..ccc3666aa
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.scss
@@ -0,0 +1,9 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4.badge {
5 @include margin-right(10px);
6
7 height: fit-content;
8 font-size: 12px;
9}
diff --git a/client/src/app/shared/shared-moderation/account-block-badges.component.ts b/client/src/app/shared/shared-moderation/account-block-badges.component.ts
new file mode 100644
index 000000000..a72601118
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.ts
@@ -0,0 +1,11 @@
1import { Component, Input } from '@angular/core'
2import { Account } from '../shared-main'
3
4@Component({
5 selector: 'my-account-block-badges',
6 styleUrls: [ './account-block-badges.component.scss' ],
7 templateUrl: './account-block-badges.component.html'
8})
9export class AccountBlockBadgesComponent {
10 @Input() account: Account
11}
diff --git a/client/src/app/shared/shared-moderation/blocklist.service.ts b/client/src/app/shared/shared-moderation/blocklist.service.ts
index db2a8c584..f4836c6c4 100644
--- a/client/src/app/shared/shared-moderation/blocklist.service.ts
+++ b/client/src/app/shared/shared-moderation/blocklist.service.ts
@@ -3,7 +3,7 @@ import { catchError, map } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { RestExtractor, RestPagination, RestService } from '@app/core' 5import { RestExtractor, RestPagination, RestService } from '@app/core'
6import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '@shared/models' 6import { AccountBlock as AccountBlockServer, BlockStatus, ResultList, ServerBlock } from '@shared/models'
7import { environment } from '../../../environments/environment' 7import { environment } from '../../../environments/environment'
8import { Account } from '../shared-main' 8import { Account } from '../shared-main'
9import { AccountBlock } from './account-block.model' 9import { AccountBlock } from './account-block.model'
@@ -12,6 +12,7 @@ export enum BlocklistComponentType { Account, Instance }
12 12
13@Injectable() 13@Injectable()
14export class BlocklistService { 14export class BlocklistService {
15 static BASE_BLOCKLIST_URL = environment.apiUrl + '/api/v1/blocklist'
15 static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist' 16 static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist'
16 static BASE_SERVER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/server/blocklist' 17 static BASE_SERVER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/server/blocklist'
17 18
@@ -21,6 +22,23 @@ export class BlocklistService {
21 private restService: RestService 22 private restService: RestService
22 ) { } 23 ) { }
23 24
25 /** ********************* Blocklist status ***********************/
26
27 getStatus (options: {
28 accounts?: string[]
29 hosts?: string[]
30 }) {
31 const { accounts, hosts } = options
32
33 let params = new HttpParams()
34
35 if (accounts) params = this.restService.addArrayParams(params, 'accounts', accounts)
36 if (hosts) params = this.restService.addArrayParams(params, 'hosts', hosts)
37
38 return this.authHttp.get<BlockStatus>(BlocklistService.BASE_BLOCKLIST_URL + '/status', { params })
39 .pipe(catchError(err => this.restExtractor.handleError(err)))
40 }
41
24 /** ********************* User -> Account blocklist ***********************/ 42 /** ********************* User -> Account blocklist ***********************/
25 43
26 getUserAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) { 44 getUserAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
diff --git a/client/src/app/shared/shared-moderation/index.ts b/client/src/app/shared/shared-moderation/index.ts
index 41c910ffe..da85b2299 100644
--- a/client/src/app/shared/shared-moderation/index.ts
+++ b/client/src/app/shared/shared-moderation/index.ts
@@ -1,6 +1,7 @@
1export * from './report-modals' 1export * from './report-modals'
2 2
3export * from './abuse.service' 3export * from './abuse.service'
4export * from './account-block-badges.component'
4export * from './account-block.model' 5export * from './account-block.model'
5export * from './account-blocklist.component' 6export * from './account-blocklist.component'
6export * from './batch-domains-modal.component' 7export * from './batch-domains-modal.component'
diff --git a/client/src/app/shared/shared-moderation/shared-moderation.module.ts b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
index 95213e2bd..7cadda67c 100644
--- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts
+++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
@@ -13,6 +13,7 @@ import { UserBanModalComponent } from './user-ban-modal.component'
13import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' 13import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
14import { VideoBlockComponent } from './video-block.component' 14import { VideoBlockComponent } from './video-block.component'
15import { VideoBlockService } from './video-block.service' 15import { VideoBlockService } from './video-block.service'
16import { AccountBlockBadgesComponent } from './account-block-badges.component'
16import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module' 17import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
17 18
18@NgModule({ 19@NgModule({
@@ -31,7 +32,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image
31 VideoReportComponent, 32 VideoReportComponent,
32 BatchDomainsModalComponent, 33 BatchDomainsModalComponent,
33 CommentReportComponent, 34 CommentReportComponent,
34 AccountReportComponent 35 AccountReportComponent,
36 AccountBlockBadgesComponent
35 ], 37 ],
36 38
37 exports: [ 39 exports: [
@@ -41,7 +43,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image
41 VideoReportComponent, 43 VideoReportComponent,
42 BatchDomainsModalComponent, 44 BatchDomainsModalComponent,
43 CommentReportComponent, 45 CommentReportComponent,
44 AccountReportComponent 46 AccountReportComponent,
47 AccountBlockBadgesComponent
45 ], 48 ],
46 49
47 providers: [ 50 providers: [
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
index b18d861d6..e2cd2cdc1 100644
--- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
@@ -289,13 +289,13 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
289 { 289 {
290 label: $localize`Mute the instance`, 290 label: $localize`Mute the instance`,
291 description: $localize`Hide any content from that instance for you.`, 291 description: $localize`Hide any content from that instance for you.`,
292 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false, 292 isDisplayed: ({ account }) => !account.userId && account.mutedServerByUser === false,
293 handler: ({ account }) => this.blockServerByUser(account.host) 293 handler: ({ account }) => this.blockServerByUser(account.host)
294 }, 294 },
295 { 295 {
296 label: $localize`Unmute the instance`, 296 label: $localize`Unmute the instance`,
297 description: $localize`Show back content from that instance for you.`, 297 description: $localize`Show back content from that instance for you.`,
298 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true, 298 isDisplayed: ({ account }) => !account.userId && account.mutedServerByUser === true,
299 handler: ({ account }) => this.unblockServerByUser(account.host) 299 handler: ({ account }) => this.unblockServerByUser(account.host)
300 }, 300 },
301 { 301 {
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts
index 2675c6135..724c4d834 100644
--- a/client/src/app/shared/shared-search/advanced-search.model.ts
+++ b/client/src/app/shared/shared-search/advanced-search.model.ts
@@ -8,6 +8,8 @@ import {
8 VideosSearchQuery 8 VideosSearchQuery
9} from '@shared/models' 9} from '@shared/models'
10 10
11export type AdvancedSearchResultType = 'videos' | 'playlists' | 'channels'
12
11export class AdvancedSearch { 13export class AdvancedSearch {
12 startDate: string // ISO 8601 14 startDate: string // ISO 8601
13 endDate: string // ISO 8601 15 endDate: string // ISO 8601
@@ -36,6 +38,7 @@ export class AdvancedSearch {
36 sort: string 38 sort: string
37 39
38 searchTarget: SearchTargetType 40 searchTarget: SearchTargetType
41 resultType: AdvancedSearchResultType
39 42
40 // Filters we don't want to count, because they are mandatory 43 // Filters we don't want to count, because they are mandatory
41 private silentFilters = new Set([ 'sort', 'searchTarget' ]) 44 private silentFilters = new Set([ 'sort', 'searchTarget' ])
@@ -61,6 +64,7 @@ export class AdvancedSearch {
61 durationMax?: string 64 durationMax?: string
62 sort?: string 65 sort?: string
63 searchTarget?: SearchTargetType 66 searchTarget?: SearchTargetType
67 resultType?: AdvancedSearchResultType
64 }) { 68 }) {
65 if (!options) return 69 if (!options) return
66 70
@@ -84,6 +88,12 @@ export class AdvancedSearch {
84 88
85 this.searchTarget = options.searchTarget || undefined 89 this.searchTarget = options.searchTarget || undefined
86 90
91 this.resultType = options.resultType || undefined
92
93 if (!this.resultType && this.hasVideoFilter()) {
94 this.resultType = 'videos'
95 }
96
87 if (isNaN(this.durationMin)) this.durationMin = undefined 97 if (isNaN(this.durationMin)) this.durationMin = undefined
88 if (isNaN(this.durationMax)) this.durationMax = undefined 98 if (isNaN(this.durationMax)) this.durationMax = undefined
89 99
@@ -137,7 +147,8 @@ export class AdvancedSearch {
137 isLive: this.isLive, 147 isLive: this.isLive,
138 host: this.host, 148 host: this.host,
139 sort: this.sort, 149 sort: this.sort,
140 searchTarget: this.searchTarget 150 searchTarget: this.searchTarget,
151 resultType: this.resultType
141 } 152 }
142 } 153 }
143 154
@@ -199,4 +210,21 @@ export class AdvancedSearch {
199 210
200 return true 211 return true
201 } 212 }
213
214 private hasVideoFilter () {
215 return this.startDate !== undefined ||
216 this.endDate !== undefined ||
217 this.originallyPublishedStartDate !== undefined ||
218 this.originallyPublishedEndDate !== undefined ||
219 this.nsfw !== undefined !== undefined ||
220 this.categoryOneOf !== undefined ||
221 this.licenceOneOf !== undefined ||
222 this.languageOneOf !== undefined ||
223 this.tagsOneOf !== undefined ||
224 this.tagsAllOf !== undefined ||
225 this.durationMin !== undefined ||
226 this.durationMax !== undefined ||
227 this.host !== undefined ||
228 this.isLive !== undefined
229 }
202} 230}
diff --git a/client/src/app/shared/shared-search/search.service.ts b/client/src/app/shared/shared-search/search.service.ts
index 71350c733..61acfb466 100644
--- a/client/src/app/shared/shared-search/search.service.ts
+++ b/client/src/app/shared/shared-search/search.service.ts
@@ -1,4 +1,4 @@
1import { Observable } from 'rxjs' 1import { Observable, of } from 'rxjs'
2import { catchError, map, switchMap } from 'rxjs/operators' 2import { catchError, map, switchMap } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
@@ -39,6 +39,10 @@ export class SearchService {
39 }): Observable<ResultList<Video>> { 39 }): Observable<ResultList<Video>> {
40 const { search, uuids, componentPagination, advancedSearch } = parameters 40 const { search, uuids, componentPagination, advancedSearch } = parameters
41 41
42 if (advancedSearch?.resultType !== undefined && advancedSearch.resultType !== 'videos') {
43 return of({ total: 0, data: [] })
44 }
45
42 const url = SearchService.BASE_SEARCH_URL + 'videos' 46 const url = SearchService.BASE_SEARCH_URL + 'videos'
43 let pagination: RestPagination 47 let pagination: RestPagination
44 48
@@ -73,6 +77,10 @@ export class SearchService {
73 }): Observable<ResultList<VideoChannel>> { 77 }): Observable<ResultList<VideoChannel>> {
74 const { search, advancedSearch, componentPagination, handles } = parameters 78 const { search, advancedSearch, componentPagination, handles } = parameters
75 79
80 if (advancedSearch?.resultType !== undefined && advancedSearch.resultType !== 'channels') {
81 return of({ total: 0, data: [] })
82 }
83
76 const url = SearchService.BASE_SEARCH_URL + 'video-channels' 84 const url = SearchService.BASE_SEARCH_URL + 'video-channels'
77 85
78 let pagination: RestPagination 86 let pagination: RestPagination
@@ -107,6 +115,10 @@ export class SearchService {
107 }): Observable<ResultList<VideoPlaylist>> { 115 }): Observable<ResultList<VideoPlaylist>> {
108 const { search, advancedSearch, componentPagination, uuids } = parameters 116 const { search, advancedSearch, componentPagination, uuids } = parameters
109 117
118 if (advancedSearch?.resultType !== undefined && advancedSearch.resultType !== 'playlists') {
119 return of({ total: 0, data: [] })
120 }
121
110 const url = SearchService.BASE_SEARCH_URL + 'video-playlists' 122 const url = SearchService.BASE_SEARCH_URL + 'video-playlists'
111 123
112 let pagination: RestPagination 124 let pagination: RestPagination
diff --git a/client/src/assets/player/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
index 71c31696a..421ce4934 100644
--- a/client/src/assets/player/p2p-media-loader/hls-plugin.ts
+++ b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
@@ -146,7 +146,10 @@ class Html5Hlsjs {
146 } 146 }
147 147
148 duration () { 148 duration () {
149 return this._duration || this.videoElement.duration || 0 149 if (this._duration === Infinity) return Infinity
150 if (!isNaN(this.videoElement.duration)) return this.videoElement.duration
151
152 return this._duration || 0
150 } 153 }
151 154
152 seekable () { 155 seekable () {
@@ -366,6 +369,7 @@ class Html5Hlsjs {
366 369
367 this.isLive = data.details.live 370 this.isLive = data.details.live
368 this.dvrDuration = data.details.totalduration 371 this.dvrDuration = data.details.totalduration
372
369 this._duration = this.isLive ? Infinity : data.details.totalduration 373 this._duration = this.isLive ? Infinity : data.details.totalduration
370 }) 374 })
371 375
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
index 0121e87d7..451b4a161 100644
--- a/client/src/assets/player/peertube-plugin.ts
+++ b/client/src/assets/player/peertube-plugin.ts
@@ -11,6 +11,7 @@ import {
11} from './peertube-player-local-storage' 11} from './peertube-player-local-storage'
12import { PeerTubePluginOptions, UserWatching, VideoJSCaption } from './peertube-videojs-typings' 12import { PeerTubePluginOptions, UserWatching, VideoJSCaption } from './peertube-videojs-typings'
13import { isMobile } from './utils' 13import { isMobile } from './utils'
14import { SettingsButton } from './videojs-components/settings-menu-button'
14 15
15const Plugin = videojs.getPlugin('plugin') 16const Plugin = videojs.getPlugin('plugin')
16 17
@@ -31,7 +32,8 @@ class PeerTubePlugin extends Plugin {
31 32
32 private menuOpened = false 33 private menuOpened = false
33 private mouseInControlBar = false 34 private mouseInControlBar = false
34 private readonly savedInactivityTimeout: number 35 private mouseInSettings = false
36 private readonly initialInactivityTimeout: number
35 37
36 constructor (player: videojs.Player, options?: PeerTubePluginOptions) { 38 constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
37 super(player) 39 super(player)
@@ -40,8 +42,7 @@ class PeerTubePlugin extends Plugin {
40 this.videoDuration = options.videoDuration 42 this.videoDuration = options.videoDuration
41 this.videoCaptions = options.videoCaptions 43 this.videoCaptions = options.videoCaptions
42 this.isLive = options.isLive 44 this.isLive = options.isLive
43 45 this.initialInactivityTimeout = this.player.options_.inactivityTimeout
44 this.savedInactivityTimeout = player.options_.inactivityTimeout
45 46
46 if (options.autoplay) this.player.addClass('vjs-has-autoplay') 47 if (options.autoplay) this.player.addClass('vjs-has-autoplay')
47 48
@@ -108,13 +109,13 @@ class PeerTubePlugin extends Plugin {
108 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) 109 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
109 } 110 }
110 111
111 onMenuOpen () { 112 onMenuOpened () {
112 this.menuOpened = false 113 this.menuOpened = true
113 this.alterInactivity() 114 this.alterInactivity()
114 } 115 }
115 116
116 onMenuClosed () { 117 onMenuClosed () {
117 this.menuOpened = true 118 this.menuOpened = false
118 this.alterInactivity() 119 this.alterInactivity()
119 } 120 }
120 121
@@ -126,6 +127,8 @@ class PeerTubePlugin extends Plugin {
126 this.initCaptions() 127 this.initCaptions()
127 128
128 this.listenControlBarMouse() 129 this.listenControlBarMouse()
130
131 this.listenFullScreenChange()
129 } 132 }
130 133
131 private runViewAdd () { 134 private runViewAdd () {
@@ -198,27 +201,50 @@ class PeerTubePlugin extends Plugin {
198 return fetch(url, { method: 'PUT', body, headers }) 201 return fetch(url, { method: 'PUT', body, headers })
199 } 202 }
200 203
204 private listenFullScreenChange () {
205 this.player.on('fullscreenchange', () => {
206 if (this.player.isFullscreen()) this.player.focus()
207 })
208 }
209
201 private listenControlBarMouse () { 210 private listenControlBarMouse () {
202 this.player.controlBar.on('mouseenter', () => { 211 const controlBar = this.player.controlBar
212 const settingsButton: SettingsButton = (controlBar as any).settingsButton
213
214 controlBar.on('mouseenter', () => {
203 this.mouseInControlBar = true 215 this.mouseInControlBar = true
204 this.alterInactivity() 216 this.alterInactivity()
205 }) 217 })
206 218
207 this.player.controlBar.on('mouseleave', () => { 219 controlBar.on('mouseleave', () => {
208 this.mouseInControlBar = false 220 this.mouseInControlBar = false
209 this.alterInactivity() 221 this.alterInactivity()
210 }) 222 })
223
224 settingsButton.dialog.on('mouseenter', () => {
225 this.mouseInSettings = true
226 this.alterInactivity()
227 })
228
229 settingsButton.dialog.on('mouseleave', () => {
230 this.mouseInSettings = false
231 this.alterInactivity()
232 })
211 } 233 }
212 234
213 private alterInactivity () { 235 private alterInactivity () {
214 if (this.menuOpened) { 236 if (this.menuOpened || this.mouseInSettings || this.mouseInControlBar || this.isTouchEnabled()) {
215 this.player.options_.inactivityTimeout = this.savedInactivityTimeout 237 this.setInactivityTimeout(0)
216 return 238 return
217 } 239 }
218 240
219 if (!this.mouseInControlBar && !this.isTouchEnabled()) { 241 this.setInactivityTimeout(this.initialInactivityTimeout)
220 this.player.options_.inactivityTimeout = 1 242 this.player.reportUserActivity(true)
221 } 243 }
244
245 private setInactivityTimeout (timeout: number) {
246 (this.player as any).cache_.inactivityTimeout = timeout
247 this.player.options_.inactivityTimeout = timeout
222 } 248 }
223 249
224 private isTouchEnabled () { 250 private isTouchEnabled () {
diff --git a/client/src/assets/player/videojs-components/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts
index 75a5c6904..6de390f4d 100644
--- a/client/src/assets/player/videojs-components/settings-menu-button.ts
+++ b/client/src/assets/player/videojs-components/settings-menu-button.ts
@@ -144,7 +144,7 @@ class SettingsButton extends Button {
144 } 144 }
145 145
146 showDialog () { 146 showDialog () {
147 this.player().peertube().onMenuOpen(); 147 this.player().peertube().onMenuOpened();
148 148
149 (this.menu.el() as HTMLElement).style.opacity = '1' 149 (this.menu.el() as HTMLElement).style.opacity = '1'
150 150
diff --git a/client/src/root-helpers/plugins-manager.ts b/client/src/root-helpers/plugins-manager.ts
index a1b763ff2..e574e75a3 100644
--- a/client/src/root-helpers/plugins-manager.ts
+++ b/client/src/root-helpers/plugins-manager.ts
@@ -13,8 +13,10 @@ import {
13 PluginType, 13 PluginType,
14 RegisterClientFormFieldOptions, 14 RegisterClientFormFieldOptions,
15 RegisterClientHookOptions, 15 RegisterClientHookOptions,
16 RegisterClientSettingsScript, 16 RegisterClientRouteOptions,
17 RegisterClientSettingsScriptOptions,
17 RegisterClientVideoFieldOptions, 18 RegisterClientVideoFieldOptions,
19 RegisteredExternalAuthConfig,
18 ServerConfigPlugin 20 ServerConfigPlugin
19} from '../../../shared/models' 21} from '../../../shared/models'
20import { environment } from '../environments/environment' 22import { environment } from '../environments/environment'
@@ -36,7 +38,8 @@ type PluginInfo = {
36 38
37type PeertubeHelpersFactory = (pluginInfo: PluginInfo) => RegisterClientHelpers 39type PeertubeHelpersFactory = (pluginInfo: PluginInfo) => RegisterClientHelpers
38type OnFormFields = (options: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => void 40type OnFormFields = (options: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => void
39type OnSettingsScripts = (pluginInfo: PluginInfo, options: RegisterClientSettingsScript) => void 41type OnSettingsScripts = (pluginInfo: PluginInfo, options: RegisterClientSettingsScriptOptions) => void
42type OnClientRoute = (options: RegisterClientRouteOptions) => void
40 43
41const logger = debug('peertube:plugins') 44const logger = debug('peertube:plugins')
42 45
@@ -63,21 +66,29 @@ class PluginsManager {
63 private readonly peertubeHelpersFactory: PeertubeHelpersFactory 66 private readonly peertubeHelpersFactory: PeertubeHelpersFactory
64 private readonly onFormFields: OnFormFields 67 private readonly onFormFields: OnFormFields
65 private readonly onSettingsScripts: OnSettingsScripts 68 private readonly onSettingsScripts: OnSettingsScripts
69 private readonly onClientRoute: OnClientRoute
66 70
67 constructor (options: { 71 constructor (options: {
68 peertubeHelpersFactory: PeertubeHelpersFactory 72 peertubeHelpersFactory: PeertubeHelpersFactory
69 onFormFields?: OnFormFields 73 onFormFields?: OnFormFields
70 onSettingsScripts?: OnSettingsScripts 74 onSettingsScripts?: OnSettingsScripts
75 onClientRoute?: OnClientRoute
71 }) { 76 }) {
72 this.peertubeHelpersFactory = options.peertubeHelpersFactory 77 this.peertubeHelpersFactory = options.peertubeHelpersFactory
73 this.onFormFields = options.onFormFields 78 this.onFormFields = options.onFormFields
74 this.onSettingsScripts = options.onSettingsScripts 79 this.onSettingsScripts = options.onSettingsScripts
80 this.onClientRoute = options.onClientRoute
75 } 81 }
76 82
77 static getPluginPathPrefix (isTheme: boolean) { 83 static getPluginPathPrefix (isTheme: boolean) {
78 return isTheme ? '/themes' : '/plugins' 84 return isTheme ? '/themes' : '/plugins'
79 } 85 }
80 86
87 static getExternalAuthHref (auth: RegisteredExternalAuthConfig) {
88 return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}`
89
90 }
91
81 loadPluginsList (config: HTMLServerConfig) { 92 loadPluginsList (config: HTMLServerConfig) {
82 for (const plugin of config.plugin.registered) { 93 for (const plugin of config.plugin.registered) {
83 this.addPlugin(plugin) 94 this.addPlugin(plugin)
@@ -215,7 +226,7 @@ class PluginsManager {
215 return this.onFormFields(commonOptions, videoFormOptions) 226 return this.onFormFields(commonOptions, videoFormOptions)
216 } 227 }
217 228
218 const registerSettingsScript = (options: RegisterClientSettingsScript) => { 229 const registerSettingsScript = (options: RegisterClientSettingsScriptOptions) => {
219 if (!this.onSettingsScripts) { 230 if (!this.onSettingsScripts) {
220 throw new Error('Registering settings script is not supported') 231 throw new Error('Registering settings script is not supported')
221 } 232 }
@@ -223,13 +234,29 @@ class PluginsManager {
223 return this.onSettingsScripts(pluginInfo, options) 234 return this.onSettingsScripts(pluginInfo, options)
224 } 235 }
225 236
237 const registerClientRoute = (options: RegisterClientRouteOptions) => {
238 if (!this.onClientRoute) {
239 throw new Error('Registering client route is not supported')
240 }
241
242 return this.onClientRoute(options)
243 }
244
226 const peertubeHelpers = this.peertubeHelpersFactory(pluginInfo) 245 const peertubeHelpers = this.peertubeHelpersFactory(pluginInfo)
227 246
228 console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name) 247 console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
229 248
230 const absURL = (environment.apiUrl || window.location.origin) + clientScript.script 249 const absURL = (environment.apiUrl || window.location.origin) + clientScript.script
231 return dynamicImport(absURL) 250 return dynamicImport(absURL)
232 .then((script: ClientScriptModule) => script.register({ registerHook, registerVideoField, registerSettingsScript, peertubeHelpers })) 251 .then((script: ClientScriptModule) => {
252 return script.register({
253 registerHook,
254 registerVideoField,
255 registerSettingsScript,
256 registerClientRoute,
257 peertubeHelpers
258 })
259 })
233 .then(() => this.sortHooksByPriority()) 260 .then(() => this.sortHooksByPriority())
234 .catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err)) 261 .catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err))
235 } 262 }
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss
index 96d752699..332a0e17d 100644
--- a/client/src/sass/player/peertube-skin.scss
+++ b/client/src/sass/player/peertube-skin.scss
@@ -71,7 +71,7 @@ body {
71 height: $big-play-height; 71 height: $big-play-height;
72 line-height: $big-play-height; 72 line-height: $big-play-height;
73 margin-top: -(math.div($big-play-height, 2)); 73 margin-top: -(math.div($big-play-height, 2));
74 transition: 0.4s opacity; 74 transition: 0.2s background-color;
75 75
76 &::-moz-focus-inner { 76 &::-moz-focus-inner {
77 border: 0; 77 border: 0;
@@ -89,30 +89,6 @@ body {
89 &:hover { 89 &:hover {
90 background-color: var(--mainColor, #696969); 90 background-color: var(--mainColor, #696969);
91 } 91 }
92
93 }
94
95 // Small effect when we click on the play button
96 &.vjs-has-big-play-button-clicked {
97
98 .vjs-big-play-button,
99 .vjs-poster {
100 display: block;
101 visibility: hidden;
102
103 &.vjs-big-play-button,
104 &.vjs-big-play-button::before {
105 opacity: 0;
106 transition: visibility 0.2s, opacity 0.2s;
107 }
108
109 &.vjs-poster,
110 &.vjs-poster::before {
111 opacity: 0;
112 transition: visibility 0.3s, opacity 0.3s;
113 transition-delay: 0.05s;
114 }
115 }
116 } 92 }
117 93
118 // Show poster and controls when playing audio-only content 94 // Show poster and controls when playing audio-only content
@@ -158,6 +134,7 @@ body {
158 background: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.6)); 134 background: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.6));
159 box-shadow: 0 -15px 40px 10px rgba(0, 0, 0, 0.2); 135 box-shadow: 0 -15px 40px 10px rgba(0, 0, 0, 0.2);
160 text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); 136 text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
137 transition: visibility 0.3s, opacity 0.3s !important;
161 138
162 > button:first-child { 139 > button:first-child {
163 @include margin-left($first-control-bar-element-margin-left); 140 @include margin-left($first-control-bar-element-margin-left);
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 9d1c6c443..874be580d 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -758,8 +758,8 @@ export class PeerTubeEmbed {
758 758
759 return { 759 return {
760 getBaseStaticRoute: unimplemented, 760 getBaseStaticRoute: unimplemented,
761
762 getBaseRouterRoute: unimplemented, 761 getBaseRouterRoute: unimplemented,
762 getBasePluginClientPath: unimplemented,
763 763
764 getSettings: unimplemented, 764 getSettings: unimplemented,
765 765
diff --git a/client/src/types/register-client-option.model.ts b/client/src/types/register-client-option.model.ts
index 3415ef08f..73f82e781 100644
--- a/client/src/types/register-client-option.model.ts
+++ b/client/src/types/register-client-option.model.ts
@@ -1,7 +1,8 @@
1import { 1import {
2 RegisterClientFormFieldOptions, 2 RegisterClientFormFieldOptions,
3 RegisterClientHookOptions, 3 RegisterClientHookOptions,
4 RegisterClientSettingsScript, 4 RegisterClientRouteOptions,
5 RegisterClientSettingsScriptOptions,
5 RegisterClientVideoFieldOptions, 6 RegisterClientVideoFieldOptions,
6 ServerConfig 7 ServerConfig
7} from '@shared/models' 8} from '@shared/models'
@@ -11,7 +12,9 @@ export type RegisterClientOptions = {
11 12
12 registerVideoField: (commonOptions: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => void 13 registerVideoField: (commonOptions: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => void
13 14
14 registerSettingsScript: (options: RegisterClientSettingsScript) => void 15 registerSettingsScript: (options: RegisterClientSettingsScriptOptions) => void
16
17 registerClientRoute: (options: RegisterClientRouteOptions) => void
15 18
16 peertubeHelpers: RegisterClientHelpers 19 peertubeHelpers: RegisterClientHelpers
17} 20}
@@ -21,6 +24,8 @@ export type RegisterClientHelpers = {
21 24
22 getBaseRouterRoute: () => string 25 getBaseRouterRoute: () => string
23 26
27 getBasePluginClientPath: () => string
28
24 isLoggedIn: () => boolean 29 isLoggedIn: () => boolean
25 30
26 getAuthHeader: () => { 'Authorization': string } | undefined 31 getAuthHeader: () => { 'Authorization': string } | undefined