aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.html3
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.scss1
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html6
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.scss11
-rw-r--r--client/src/app/+about/about.component.html2
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.html6
-rw-r--r--client/src/app/+accounts/accounts.component.html6
-rw-r--r--client/src/app/+accounts/accounts.component.scss4
-rw-r--r--client/src/app/+accounts/accounts.component.ts10
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html15
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html58
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts8
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html4
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss5
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html6
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html28
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html18
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html32
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html6
-rw-r--r--client/src/app/+admin/follows/following-list/follow-modal.component.ts9
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html6
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.html6
-rw-r--r--client/src/app/+admin/overview/comments/video-comment-list.component.html2
-rw-r--r--client/src/app/+admin/overview/comments/video-comment-list.component.ts9
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.component.html234
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.ts4
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-password.component.html26
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-password.component.scss12
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.html14
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.scss5
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.ts91
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.html26
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.scss6
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.ts41
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.html4
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss3
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.html8
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.scss4
-rw-r--r--client/src/app/+manage/video-channel-edit/video-channel-edit.component.html37
-rw-r--r--client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss32
-rw-r--r--client/src/app/+my-account/my-account-applications/my-account-applications.component.html9
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html6
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.scss4
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.html51
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.scss3
-rw-r--r--client/src/app/+my-account/my-account.component.html2
-rw-r--r--client/src/app/+my-account/my-account.component.scss2
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.html6
-rw-r--r--client/src/app/+my-library/my-follows/my-followers.component.html2
-rw-r--r--client/src/app/+my-library/my-follows/my-subscriptions.component.html2
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.html3
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.ts10
-rw-r--r--client/src/app/+my-library/my-library.component.html2
-rw-r--r--client/src/app/+my-library/my-library.component.scss2
-rw-r--r--client/src/app/+my-library/my-ownership/my-ownership.component.html2
-rw-r--r--client/src/app/+my-library/my-video-imports/my-video-imports.component.html2
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html8
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html3
-rw-r--r--client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html2
-rw-r--r--client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss4
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.html2
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.ts15
-rw-r--r--client/src/app/+page-not-found/page-not-found.component.html6
-rw-r--r--client/src/app/+search/search-filters.component.html4
-rw-r--r--client/src/app/+search/search-filters.component.scss4
-rw-r--r--client/src/app/+search/search.component.html10
-rw-r--r--client/src/app/+search/search.component.ts8
-rw-r--r--client/src/app/+signup/+register/custom-stepper.component.html2
-rw-r--r--client/src/app/+signup/+register/register-step-channel.component.html4
-rw-r--r--client/src/app/+signup/+register/register-step-terms.component.html4
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.html4
-rw-r--r--client/src/app/+signup/+register/register.component.html2
-rw-r--r--client/src/app/+signup/+register/register.component.scss16
-rw-r--r--client/src/app/+video-channels/video-channels.component.html4
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.scss8
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.html2
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html2
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.scss3
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html2
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html4
-rw-r--r--client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html2
-rw-r--r--client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html6
-rw-r--r--client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss58
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.html2
-rw-r--r--client/src/app/+videos/video-list/overview/video-overview.component.html2
-rw-r--r--client/src/app/+videos/video-list/videos-list-common-page.component.ts25
-rw-r--r--client/src/app/core/core.module.ts4
-rw-r--r--client/src/app/core/rest/rest-extractor.service.ts111
-rw-r--r--client/src/app/core/rest/rest-table.ts8
-rw-r--r--client/src/app/header/search-typeahead.component.html6
-rw-r--r--client/src/app/header/suggestion.component.html8
-rw-r--r--client/src/app/helpers/i18n-utils.ts25
-rw-r--r--client/src/app/helpers/utils/upload.ts49
-rw-r--r--client/src/app/menu/menu.component.html10
-rw-r--r--client/src/app/menu/notification.component.html4
-rw-r--r--client/src/app/modal/confirm.component.html2
-rw-r--r--client/src/app/modal/confirm.component.scss4
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-details.component.html10
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.html2
-rw-r--r--client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html4
-rw-r--r--client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.scss52
-rw-r--r--client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts2
-rw-r--r--client/src/app/shared/shared-forms/advanced-input-filter.component.html13
-rw-r--r--client/src/app/shared/shared-forms/advanced-input-filter.component.scss1
-rw-r--r--client/src/app/shared/shared-forms/input-switch.component.html2
-rw-r--r--client/src/app/shared/shared-forms/input-toggle-hidden.component.html24
-rw-r--r--client/src/app/shared/shared-forms/input-toggle-hidden.component.scss11
-rw-r--r--client/src/app/shared/shared-forms/peertube-checkbox.component.html4
-rw-r--r--client/src/app/shared/shared-forms/peertube-checkbox.component.scss2
-rw-r--r--client/src/app/shared/shared-forms/reactive-file.component.ts2
-rw-r--r--client/src/app/shared/shared-forms/select/select-channel.component.html2
-rw-r--r--client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts8
-rw-r--r--client/src/app/shared/shared-forms/select/select-checkbox.component.html4
-rw-r--r--client/src/app/shared/shared-forms/select/select-checkbox.component.scss2
-rw-r--r--client/src/app/shared/shared-instance/instance-about-accordion.component.scss23
-rw-r--r--client/src/app/shared/shared-instance/instance-features-table.component.html2
-rw-r--r--client/src/app/shared/shared-instance/instance-features-table.component.scss5
-rw-r--r--client/src/app/shared/shared-instance/instance-features-table.component.ts18
-rw-r--r--client/src/app/shared/shared-main/angular/from-now.pipe.ts34
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.html5
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.model.ts5
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts11
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.html8
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.scss3
-rw-r--r--client/src/app/shared/shared-moderation/account-blocklist.component.html2
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/report.component.html5
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.html44
-rw-r--r--client/src/app/shared/shared-moderation/server-blocklist.component.html2
-rw-r--r--client/src/app/shared/shared-moderation/user-ban-modal.component.ts21
-rw-r--r--client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts3
-rw-r--r--client/src/app/shared/shared-moderation/video-block.component.ts8
-rw-r--r--client/src/app/shared/shared-share-modal/video-share.component.html3
-rw-r--r--client/src/app/shared/shared-share-modal/video-share.component.scss7
-rw-r--r--client/src/app/shared/shared-user-settings/user-video-settings.component.html6
-rw-r--r--client/src/app/shared/shared-user-settings/user-video-settings.component.scss6
-rw-r--r--client/src/app/shared/shared-user-subscription/remote-subscribe.component.html2
-rw-r--r--client/src/app/shared/shared-video-live/live-stream-information.component.html8
-rw-r--r--client/src/app/shared/shared-video-live/live-stream-information.component.scss7
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.html33
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.scss7
-rw-r--r--client/src/app/shared/shared-video-miniature/video-filters-header.component.html1
-rw-r--r--client/src/app/shared/shared-video-miniature/video-filters-header.component.scss2
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.ts2
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-list.component.html6
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss5
147 files changed, 956 insertions, 852 deletions
diff --git a/client/src/app/+about/about-follows/about-follows.component.html b/client/src/app/+about/about-follows/about-follows.component.html
index 6bc1d0448..f16f8bd71 100644
--- a/client/src/app/+about/about-follows/about-follows.component.html
+++ b/client/src/app/+about/about-follows/about-follows.component.html
@@ -1,5 +1,6 @@
1<div class="row"> 1<div class="row">
2 <h1 class="sr-only" i18n>Follows</h1> 2 <h1 class="visually-hidden" i18n>Follows</h1>
3
3 <div class="col-xl-6 col-md-12"> 4 <div class="col-xl-6 col-md-12">
4 <h2 i18n class="subtitle">Follower instances ({{ followersPagination.totalItems }})</h2> 5 <h2 i18n class="subtitle">Follower instances ({{ followersPagination.totalItems }})</h2>
5 6
diff --git a/client/src/app/+about/about-follows/about-follows.component.scss b/client/src/app/+about/about-follows/about-follows.component.scss
index 80eb997e4..057d04ced 100644
--- a/client/src/app/+about/about-follows/about-follows.component.scss
+++ b/client/src/app/+about/about-follows/about-follows.component.scss
@@ -15,6 +15,7 @@ a {
15 15
16.no-results { 16.no-results {
17 justify-content: flex-start; 17 justify-content: flex-start;
18 align-items: flex-start;
18} 19}
19 20
20.show-more { 21.show-more {
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 7f2a6aa77..b113df82f 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -8,9 +8,9 @@
8 </div> 8 </div>
9 9
10 <div class="instance-badges" *ngIf="categories.length !== 0 || languages.length !== 0"> 10 <div class="instance-badges" *ngIf="categories.length !== 0 || languages.length !== 0">
11 <span *ngFor="let category of categories" class="badge badge-primary category">{{ category }}</span> 11 <span *ngFor="let category of categories" class="pt-badge badge-primary">{{ category }}</span>
12 12
13 <span *ngFor="let language of languages" class="badge badge-secondary language">{{ language }}</span> 13 <span *ngFor="let language of languages" class="pt-badge badge-secondary">{{ language }}</span>
14 </div> 14 </div>
15 15
16 <div class="short-description"> 16 <div class="short-description">
@@ -204,7 +204,7 @@
204 </div> 204 </div>
205 205
206 <div class="col-md-12 col-xl-6" myPluginSelector pluginSelectorId="about-instance-features"> 206 <div class="col-md-12 col-xl-6" myPluginSelector pluginSelectorId="about-instance-features">
207 <h2 class="sr-only" i18n>FEATURES</h2> 207 <h2 class="visually-hidden" i18n>FEATURES</h2>
208 <my-instance-features-table></my-instance-features-table> 208 <my-instance-features-table></my-instance-features-table>
209 </div> 209 </div>
210 210
diff --git a/client/src/app/+about/about-instance/about-instance.component.scss b/client/src/app/+about/about-instance/about-instance.component.scss
index 965c04b6c..a48783639 100644
--- a/client/src/app/+about/about-instance/about-instance.component.scss
+++ b/client/src/app/+about/about-instance/about-instance.component.scss
@@ -19,18 +19,11 @@
19} 19}
20 20
21.instance-badges { 21.instance-badges {
22 font-size: 16px;
23 margin-bottom: 20px; 22 margin-bottom: 20px;
23 font-size: 16px;
24 24
25 .badge { 25 .pt-badge {
26 @include margin-right(5px); 26 @include margin-right(5px);
27
28 font-size: 12px;
29 font-weight: $font-semibold;
30
31 &.category {
32 background-color: pvar(--mainColor);
33 }
34 } 27 }
35} 28}
36 29
diff --git a/client/src/app/+about/about.component.html b/client/src/app/+about/about.component.html
index 63d429ebf..3b7343a04 100644
--- a/client/src/app/+about/about.component.html
+++ b/client/src/app/+about/about.component.html
@@ -1,4 +1,4 @@
1<div class="row"> 1<div>
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">
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
index 379c0443e..0dfa87074 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
@@ -1,4 +1,4 @@
1<h1 class="sr-only" i18n>Video channels</h1> 1<h1 class="visually-hidden" i18n>Video channels</h1>
2 2
3<div class="margin-content"> 3<div class="margin-content">
4 4
@@ -23,10 +23,10 @@
23 </h2> 23 </h2>
24 24
25 <div class="actor-counters"> 25 <div class="actor-counters">
26 <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> 26 <div class="followers" i18n>{videoChannel.followersCount, plural, =0 {No subscribers} =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
27 27
28 <span class="videos-count" *ngIf="getTotalVideosOf(videoChannel) !== undefined" i18n> 28 <span class="videos-count" *ngIf="getTotalVideosOf(videoChannel) !== undefined" i18n>
29 {getTotalVideosOf(videoChannel), plural, =1 {1 videos} other {{{ getTotalVideosOf(videoChannel) }} videos}} 29 {getTotalVideosOf(videoChannel), plural, =0 {No videos} =1 {1 video} other {{{ getTotalVideosOf(videoChannel) }} videos}}
30 </span> 30 </span>
31 </div> 31 </div>
32 32
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 8362e6b7e..d92760ff8 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -18,7 +18,7 @@
18 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()" 18 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
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="pt-badge badge-danger" i18n>Banned</span>
22 22
23 <my-account-block-badges [account]="account"></my-account-block-badges> 23 <my-account-block-badges [account]="account"></my-account-block-badges>
24 </div> 24 </div>
@@ -33,10 +33,10 @@
33 </div> 33 </div>
34 34
35 <div class="actor-counters"> 35 <div class="actor-counters">
36 <span i18n>{naiveAggregatedSubscribers(), plural, =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span> 36 <span i18n>{naiveAggregatedSubscribers(), plural, =0 {No subscribers} =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span>
37 37
38 <span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n> 38 <span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n>
39 {accountVideosCount, plural, =1 {1 videos} other {{{ accountVideosCount }} videos}} 39 {accountVideosCount, plural, =0 {No videos} =1 {1 video} other {{{ accountVideosCount }} videos}}
40 </span> 40 </span>
41 </div> 41 </div>
42 </div> 42 </div>
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index 5043b98c4..a316bd599 100644
--- a/client/src/app/+accounts/accounts.component.scss
+++ b/client/src/app/+accounts/accounts.component.scss
@@ -93,6 +93,10 @@ my-user-moderation-dropdown {
93 } 93 }
94} 94}
95 95
96.pt-badge {
97 @include margin-right(5px);
98}
99
96@media screen and (max-width: $small-view) { 100@media screen and (max-width: $small-view) {
97 .root { 101 .root {
98 --myGlobalTopPadding: 45px; 102 --myGlobalTopPadding: 45px;
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index 898325492..cf66b817a 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -30,8 +30,6 @@ export class AccountsComponent implements OnInit, OnDestroy {
30 links: ListOverflowItem[] = [] 30 links: ListOverflowItem[] = []
31 hideMenu = false 31 hideMenu = false
32 32
33 accountFollowerTitle = ''
34
35 accountVideosCount: number 33 accountVideosCount: number
36 accountDescriptionHTML = '' 34 accountDescriptionHTML = ''
37 accountDescriptionExpanded = false 35 accountDescriptionExpanded = false
@@ -121,12 +119,6 @@ export class AccountsComponent implements OnInit, OnDestroy {
121 this.notifier.success($localize`Username copied`) 119 this.notifier.success($localize`Username copied`)
122 } 120 }
123 121
124 subscribersDisplayFor (count: number) {
125 if (count === 1) return $localize`1 subscriber`
126
127 return $localize`${count} subscribers`
128 }
129
130 searchChanged (search: string) { 122 searchChanged (search: string) {
131 const queryParams = { search } 123 const queryParams = { search }
132 124
@@ -150,8 +142,6 @@ export class AccountsComponent implements OnInit, OnDestroy {
150 } 142 }
151 143
152 private async onAccount (account: Account) { 144 private async onAccount (account: Account) {
153 this.accountFollowerTitle = $localize`${account.followersCount} direct account followers`
154
155 this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description) 145 this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description)
156 146
157 // After the markdown renderer to avoid layout changes 147 // After the markdown renderer to avoid layout changes
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 0ab80e5a9..a17b13fdf 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
@@ -1,15 +1,15 @@
1<ng-container [formGroup]="form"> 1<ng-container [formGroup]="form">
2 2
3 <div class="form-row mt-5"> <!-- cache grid --> 3 <div class="row mt-5"> <!-- cache grid -->
4 <div class="form-group col-12 col-lg-4 col-xl-3"> 4
5 <div class="col-12 col-lg-4 col-xl-3">
5 <div i18n class="inner-form-title">CACHE</div> 6 <div i18n class="inner-form-title">CACHE</div>
6 <div i18n class="inner-form-description"> 7 <div i18n class="inner-form-description">
7 Some files are not federated, and fetched when necessary. Define their caching policies. 8 Some files are not federated, and fetched when necessary. Define their caching policies.
8 </div> 9 </div>
9 </div> 10 </div>
10 11
11 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 12 <div class="col-12 col-lg-8 col-xl-9">
12
13 <ng-container formGroupName="cache"> 13 <ng-container formGroupName="cache">
14 <div class="form-group" formGroupName="previews"> 14 <div class="form-group" formGroupName="previews">
15 <label i18n for="cachePreviewsSize">Number of previews to keep in cache</label> 15 <label i18n for="cachePreviewsSize">Number of previews to keep in cache</label>
@@ -57,8 +57,8 @@
57 </div> 57 </div>
58 </div> 58 </div>
59 59
60 <div class="form-row mt-4"> <!-- cache grid --> 60 <div class="row mt-4"> <!-- cache grid -->
61 <div class="form-group col-12 col-lg-4 col-xl-3"> 61 <div class="col-12 col-lg-4 col-xl-3">
62 <div class="anchor" id="customizations"></div> <!-- customizations anchor --> 62 <div class="anchor" id="customizations"></div> <!-- customizations anchor -->
63 <div i18n class="inner-form-title">CUSTOMIZATIONS</div> 63 <div i18n class="inner-form-title">CUSTOMIZATIONS</div>
64 <div i18n class="inner-form-description"> 64 <div i18n class="inner-form-description">
@@ -66,8 +66,7 @@
66 </div> 66 </div>
67 </div> 67 </div>
68 68
69 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 69 <div class="col-12 col-lg-8 col-xl-9">
70
71 <ng-container formGroupName="instance"> 70 <ng-container formGroupName="instance">
72 <ng-container formGroupName="customizations"> 71 <ng-container formGroupName="customizations">
73 <div class="form-group"> 72 <div class="form-group">
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 bae9d9775..f225c388d 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
@@ -1,13 +1,13 @@
1<ng-container [formGroup]="form"> 1<ng-container [formGroup]="form">
2 <div class="form-row mt-5"> <!-- appearance grid --> 2 <div class="row mt-5"> <!-- appearance grid -->
3 <div class="form-group col-12 col-lg-4 col-xl-3"> 3 <div class="col-12 col-lg-4 col-xl-3">
4 <div i18n class="inner-form-title">APPEARANCE</div> 4 <div i18n class="inner-form-title">APPEARANCE</div>
5 <div i18n class="inner-form-description"> 5 <div i18n class="inner-form-description">
6 Use <a routerLink="/admin/plugins">plugins & themes</a> for more involved changes, or <a routerLink="/admin/config/edit-custom" fragment="advanced-configuration">add slight customizations</a>. 6 Use <a routerLink="/admin/plugins">plugins & themes</a> for more involved changes, or <a routerLink="/admin/config/edit-custom" fragment="advanced-configuration">add slight customizations</a>.
7 </div> 7 </div>
8 </div> 8 </div>
9 9
10 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 10 <div class="col-12 col-lg-8 col-xl-9">
11 11
12 <ng-container formGroupName="theme"> 12 <ng-container formGroupName="theme">
13 <div class="form-group"> 13 <div class="form-group">
@@ -88,15 +88,15 @@
88 </div> 88 </div>
89 </div> 89 </div>
90 90
91 <div class="form-row mt-4"> <!-- broadcast grid --> 91 <div class="row mt-4"> <!-- broadcast grid -->
92 <div class="form-group col-12 col-lg-4 col-xl-3"> 92 <div class="col-12 col-lg-4 col-xl-3">
93 <div i18n class="inner-form-title">BROADCAST MESSAGE</div> 93 <div i18n class="inner-form-title">BROADCAST MESSAGE</div>
94 <div i18n class="inner-for-description"> 94 <div i18n class="inner-for-description">
95 Display a message on your instance 95 Display a message on your instance
96 </div> 96 </div>
97 </div> 97 </div>
98 98
99 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 99 <div class="col-12 col-lg-8 col-xl-9">
100 100
101 <ng-container formGroupName="broadcastMessage"> 101 <ng-container formGroupName="broadcastMessage">
102 102
@@ -144,15 +144,15 @@
144 </div> 144 </div>
145 </div> 145 </div>
146 146
147 <div class="form-row mt-4"> <!-- new users grid --> 147 <div class="row mt-4"> <!-- new users grid -->
148 <div class="form-group col-12 col-lg-4 col-xl-3"> 148 <div class="col-12 col-lg-4 col-xl-3">
149 <div i18n class="inner-form-title">NEW USERS</div> 149 <div i18n class="inner-form-title">NEW USERS</div>
150 <div i18n class="inner-for-description"> 150 <div i18n class="inner-for-description">
151 Manage <a routerLink="/admin/users">users</a> to set their quota individually. 151 Manage <a routerLink="/admin/users">users</a> to set their quota individually.
152 </div> 152 </div>
153 </div> 153 </div>
154 154
155 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 155 <div class="col-12 col-lg-8 col-xl-9">
156 156
157 <ng-container formGroupName="signup"> 157 <ng-container formGroupName="signup">
158 <div class="form-group"> 158 <div class="form-group">
@@ -239,20 +239,20 @@
239 </div> 239 </div>
240 </div> 240 </div>
241 241
242 <div class="form-row mt-4"> <!-- videos grid --> 242 <div class="row mt-4"> <!-- videos grid -->
243 <div class="form-group col-12 col-lg-4 col-xl-3"> 243 <div class="col-12 col-lg-4 col-xl-3">
244 <div i18n class="inner-form-title">VIDEOS</div> 244 <div i18n class="inner-form-title">VIDEOS</div>
245 </div> 245 </div>
246 246
247 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 247 <div class="col-12 col-lg-8 col-xl-9">
248 248
249 <ng-container formGroupName="import"> 249 <ng-container formGroupName="import">
250 250
251 <ng-container formGroupName="videos"> 251 <ng-container formGroupName="videos">
252 252
253 <div class="form-group mt-4"> 253 <div class="form-group">
254 <label i18n for="importConcurrency">Import jobs concurrency</label> 254 <label i18n for="importConcurrency">Import jobs concurrency</label>
255 <span i18n class="muted ml-1">allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.</span> 255 <span i18n class="muted ms-1">allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.</span>
256 256
257 <div class="number-with-unit"> 257 <div class="number-with-unit">
258 <input type="number" name="importConcurrency" formControlName="concurrency" /> 258 <input type="number" name="importConcurrency" formControlName="concurrency" />
@@ -309,12 +309,12 @@
309 </div> 309 </div>
310 </div> 310 </div>
311 311
312 <div class="form-row mt-4"> <!-- video channels grid --> 312 <div class="row mt-4"> <!-- video channels grid -->
313 <div class="form-group col-12 col-lg-4 col-xl-3"> 313 <div class="col-12 col-lg-4 col-xl-3">
314 <div i18n class="inner-form-title">VIDEO CHANNELS</div> 314 <div i18n class="inner-form-title">VIDEO CHANNELS</div>
315 </div> 315 </div>
316 316
317 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 317 <div class="col-12 col-lg-8 col-xl-9">
318 <div class="form-group" formGroupName="videoChannels"> 318 <div class="form-group" formGroupName="videoChannels">
319 <label i18n for="videoChannelsMaxPerUser">Max video channels per user</label> 319 <label i18n for="videoChannelsMaxPerUser">Max video channels per user</label>
320 320
@@ -331,12 +331,12 @@
331 </div> 331 </div>
332 </div> 332 </div>
333 333
334 <div class="form-row mt-4"> <!-- search grid --> 334 <div class="row mt-4"> <!-- search grid -->
335 <div class="form-group col-12 col-lg-4 col-xl-3"> 335 <div class="col-12 col-lg-4 col-xl-3">
336 <div i18n class="inner-form-title">SEARCH</div> 336 <div i18n class="inner-form-title">SEARCH</div>
337 </div> 337 </div>
338 338
339 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 339 <div class="col-12 col-lg-8 col-xl-9">
340 340
341 <ng-container formGroupName="search"> 341 <ng-container formGroupName="search">
342 <ng-container formGroupName="remoteUri"> 342 <ng-container formGroupName="remoteUri">
@@ -420,15 +420,15 @@
420 </div> 420 </div>
421 </div> 421 </div>
422 422
423 <div class="form-row mt-4"> <!-- federation grid --> 423 <div class="row mt-4"> <!-- federation grid -->
424 <div class="form-group col-12 col-lg-4 col-xl-3"> 424 <div class="col-12 col-lg-4 col-xl-3">
425 <div i18n class="inner-form-title">FEDERATION</div> 425 <div i18n class="inner-form-title">FEDERATION</div>
426 <div i18n class="inner-form-description"> 426 <div i18n class="inner-form-description">
427 Manage <a routerLink="/admin/follows">relations</a> with other instances. 427 Manage <a routerLink="/admin/follows">relations</a> with other instances.
428 </div> 428 </div>
429 </div> 429 </div>
430 430
431 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 431 <div class="col-12 col-lg-8 col-xl-9">
432 432
433 <ng-container formGroupName="followers"> 433 <ng-container formGroupName="followers">
434 <ng-container formGroupName="instance"> 434 <ng-container formGroupName="instance">
@@ -499,12 +499,12 @@
499 </div> 499 </div>
500 </div> 500 </div>
501 501
502 <div class="form-row mt-4"> <!-- administrators grid --> 502 <div class="row mt-4"> <!-- administrators grid -->
503 <div class="form-group col-12 col-lg-4 col-xl-3"> 503 <div class="col-12 col-lg-4 col-xl-3">
504 <div i18n class="inner-form-title">ADMINISTRATORS</div> 504 <div i18n class="inner-form-title">ADMINISTRATORS</div>
505 </div> 505 </div>
506 506
507 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 507 <div class="col-12 col-lg-8 col-xl-9">
508 508
509 <div class="form-group" formGroupName="admin"> 509 <div class="form-group" formGroupName="admin">
510 <label i18n for="adminEmail">Admin email</label> 510 <label i18n for="adminEmail">Admin email</label>
@@ -527,8 +527,8 @@
527 </div> 527 </div>
528 </div> 528 </div>
529 529
530 <div class="form-row mt-4"> <!-- Twitter grid --> 530 <div class="row mt-4"> <!-- Twitter grid -->
531 <div class="form-group col-12 col-lg-4 col-xl-3"> 531 <div class="col-12 col-lg-4 col-xl-3">
532 <div i18n class="inner-form-title">TWITTER</div> 532 <div i18n class="inner-form-title">TWITTER</div>
533 <div i18n class="inner-form-description"> 533 <div i18n class="inner-form-description">
534 Provide the Twitter account representing your instance to improve link previews. 534 Provide the Twitter account representing your instance to improve link previews.
@@ -536,7 +536,7 @@
536 </div> 536 </div>
537 </div> 537 </div>
538 538
539 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 539 <div class="col-12 col-lg-8 col-xl-9">
540 540
541 <ng-container formGroupName="services"> 541 <ng-container formGroupName="services">
542 <ng-container formGroupName="twitter"> 542 <ng-container formGroupName="twitter">
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 9b55cb43c..96f5b830e 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,5 +1,6 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { FormGroup } from '@angular/forms' 2import { FormGroup } from '@angular/forms'
3import { prepareIcu } from '@app/helpers'
3 4
4export type ResolutionOption = { 5export type ResolutionOption = {
5 id: string 6 id: string
@@ -86,9 +87,10 @@ export class EditConfigurationService {
86 return { 87 return {
87 value, 88 value,
88 atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible 89 atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
89 unit: value > 1 90 unit: prepareIcu($localize`{value, plural, =1 {thread} other {threads}}`)(
90 ? $localize`threads` 91 { value },
91 : $localize`thread` 92 $localize`threads`
93 )
92 } 94 }
93 } 95 }
94} 96}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 8fef39b79..cc8e699f3 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -1,4 +1,4 @@
1<h1 class="sr-only" i18n>Configuration</h1> 1<h1 class="visually-hidden" i18n>Configuration</h1>
2 2
3<div class="alert alert-warning" *ngIf="!isUpdateAllowed()" i18n> 3<div class="alert alert-warning" *ngIf="!isUpdateAllowed()" i18n>
4 Updating instance configuration from the web interface is disabled by the system administrator. 4 Updating instance configuration from the web interface is disabled by the system administrator.
@@ -64,7 +64,7 @@
64 64
65 <div [ngbNavOutlet]="nav"></div> 65 <div [ngbNavOutlet]="nav"></div>
66 66
67 <div class="form-row mt-4"> <!-- submit placement block --> 67 <div class="row mt-4"> <!-- submit placement block -->
68 <div class="col-md-7 col-xl-5"></div> 68 <div class="col-md-7 col-xl-5"></div>
69 <div class="col-md-5 col-xl-5"> 69 <div class="col-md-5 col-xl-5">
70 70
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
index 0458d257f..31dd9f54f 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
@@ -102,11 +102,6 @@ input[disabled] {
102 opacity: 0.5; 102 opacity: 0.5;
103} 103}
104 104
105
106.form-group-right {
107 padding-top: 2px;
108}
109
110ngb-tabset:not(.previews) ::ng-deep { 105ngb-tabset:not(.previews) ::ng-deep {
111 .nav-link { 106 .nav-link {
112 font-size: 105%; 107 font-size: 105%;
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
index 2286a5a1a..701c3c0c1 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
@@ -2,12 +2,12 @@
2 2
3 <ng-container formGroupName="instanceCustomHomepage"> 3 <ng-container formGroupName="instanceCustomHomepage">
4 4
5 <div class="form-row mt-5"> <!-- homepage grid --> 5 <div class="row mt-5"> <!-- homepage grid -->
6 <div class="form-group col-12 col-lg-4 col-xl-3"> 6 <div class="col-12 col-lg-4 col-xl-3">
7 <div i18n class="inner-form-title">INSTANCE HOMEPAGE</div> 7 <div i18n class="inner-form-title">INSTANCE HOMEPAGE</div>
8 </div> 8 </div>
9 9
10 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 10 <div class="col-12 col-lg-8 col-xl-9">
11 11
12 <div class="form-group"> 12 <div class="form-group">
13 <label i18n for="instanceCustomHomepageContent">Homepage</label> 13 <label i18n for="instanceCustomHomepageContent">Homepage</label>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
index d806616bd..1c567d962 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
@@ -2,13 +2,12 @@
2 2
3 <ng-container formGroupName="instance"> 3 <ng-container formGroupName="instance">
4 4
5 <div class="form-row mt-5"> <!-- instance grid --> 5 <div class="row mt-5"> <!-- instance grid -->
6 <div class="form-group col-12 col-lg-4 col-xl-3"> 6 <div class="col-12 col-lg-4 col-xl-3">
7 <div i18n class="inner-form-title">INSTANCE</div> 7 <div i18n class="inner-form-title">INSTANCE</div>
8 </div> 8 </div>
9 9
10 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 10 <div class="col-12 col-lg-8 col-xl-9">
11
12 <div class="form-group"> 11 <div class="form-group">
13 <label i18n for="instanceName">Name</label> 12 <label i18n for="instanceName">Name</label>
14 13
@@ -77,16 +76,15 @@
77 </div> 76 </div>
78 </div> 77 </div>
79 78
80 <div class="form-row mt-4"> <!-- moderation & nsfw grid --> 79 <div class="row mt-4"> <!-- moderation & nsfw grid -->
81 <div class="form-group col-12 col-lg-4 col-xl-3"> 80 <div class="col-12 col-lg-4 col-xl-3">
82 <div i18n class="inner-form-title">MODERATION & NSFW</div> 81 <div i18n class="inner-form-title">MODERATION & NSFW</div>
83 <div i18n class="inner-for-description"> 82 <div i18row="inner-for-description">
84 Manage <a routerLink="/admin/users">users</a> to build a moderation team. 83 Manage <a routerLink="/admin/users">users</a> to build a moderation team.
85 </div> 84 </div>
86 </div> 85 </div>
87 86
88 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 87 <div class="col-12 col-lg-8 col-xl-9">
89
90 <div class="form-group"> 88 <div class="form-group">
91 <my-peertube-checkbox inputName="instanceIsNSFW" formControlName="isNSFW"> 89 <my-peertube-checkbox inputName="instanceIsNSFW" formControlName="isNSFW">
92 <ng-template ptTemplate="label"> 90 <ng-template ptTemplate="label">
@@ -162,12 +160,12 @@
162 </div> 160 </div>
163 </div> 161 </div>
164 162
165 <div class="form-row mt-4"> <!-- you and your instance grid --> 163 <div class="row mt-4"> <!-- you and your instance grid -->
166 <div class="form-group col-12 col-lg-4 col-xl-3"> 164 <div class="col-12 col-lg-4 col-xl-3">
167 <div i18n class="inner-form-title">YOU AND YOUR INSTANCE</div> 165 <div i18n class="inner-form-title">YOU AND YOUR INSTANCE</div>
168 </div> 166 </div>
169 167
170 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 168 <div class="col-12 col-lg-8 col-xl-9">
171 169
172 <div class="form-group"> 170 <div class="form-group">
173 <label i18n for="instanceAdministrator">Who is behind the instance?</label><my-help helpType="markdownText"></my-help> 171 <label i18n for="instanceAdministrator">Who is behind the instance?</label><my-help helpType="markdownText"></my-help>
@@ -220,12 +218,12 @@
220 </div> 218 </div>
221 </div> 219 </div>
222 220
223 <div class="form-row mt-4"> <!-- other information grid --> 221 <div class="row mt-4"> <!-- other information grid -->
224 <div class="form-group col-12 col-lg-4 col-xl-3"> 222 <div class="col-12 col-lg-4 col-xl-3">
225 <div i18n class="inner-form-title">OTHER INFORMATION</div> 223 <div i18n class="inner-form-title">OTHER INFORMATION</div>
226 </div> 224 </div>
227 225
228 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 226 <div class="col-12 col-lg-8 col-xl-9">
229 227
230 <div class="form-group"> 228 <div class="form-group">
231 <label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label> 229 <label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
index 71d5d91f0..a970f3df3 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
@@ -1,14 +1,14 @@
1<ng-container [formGroup]="form"> 1<ng-container [formGroup]="form">
2 2
3 <div class="form-row mt-5"> 3 <div class="row mt-5">
4 <div class="form-group col-12 col-lg-4 col-xl-3"> 4 <div class="col-12 col-lg-4 col-xl-3">
5 <div i18n class="inner-form-title">LIVE</div> 5 <div i18n class="inner-form-title">LIVE</div>
6 <div i18n class="inner-form-description"> 6 <div i18n class="inner-form-description">
7 Enable users of your instance to stream live. 7 Enable users of your instance to stream live.
8 </div> 8 </div>
9 </div> 9 </div>
10 10
11 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 11 <div class="col-12 col-lg-8 col-xl-9">
12 12
13 <ng-container formGroupName="live"> 13 <ng-container formGroupName="live">
14 14
@@ -89,15 +89,15 @@
89 </div> 89 </div>
90 </div> 90 </div>
91 91
92 <div class="form-row"> <!-- transcoding live streams grid --> 92 <div class="row"> <!-- transcoding live streams grid -->
93 <div class="form-group col-12 col-lg-4 col-xl-3"> 93 <div class="col-12 col-lg-4 col-xl-3">
94 <div i18n class="inner-form-title">TRANSCODING</div> 94 <div i18n class="inner-form-title">TRANSCODING</div>
95 <div i18n class="inner-form-description"> 95 <div i18n class="inner-form-description">
96 Same as VOD transcoding, transcoding live streams so that they are in a streamable form that any device can play. Requires a beefy CPU, and then some. 96 Same as VOD transcoding, transcoding live streams so that they are in a streamable form that any device can play. Requires a beefy CPU, and then some.
97 </div> 97 </div>
98 </div> 98 </div>
99 99
100 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 100 <div class="col-12 col-lg-8 col-xl-9">
101 101
102 <ng-container formGroupName="live"> 102 <ng-container formGroupName="live">
103 <ng-container formGroupName="transcoding"> 103 <ng-container formGroupName="transcoding">
@@ -115,7 +115,7 @@
115 <div class="form-group" [ngClass]="getDisabledLiveTranscodingClass()"> 115 <div class="form-group" [ngClass]="getDisabledLiveTranscodingClass()">
116 <label i18n for="liveTranscodingThreads">Live resolutions to generate</label> 116 <label i18n for="liveTranscodingThreads">Live resolutions to generate</label>
117 117
118 <div class="ml-2 mt-2 d-flex flex-column"> 118 <div class="ms-2 mt-2 d-flex flex-column">
119 <ng-container formGroupName="resolutions"> 119 <ng-container formGroupName="resolutions">
120 120
121 <div class="form-group" *ngFor="let resolution of liveResolutions"> 121 <div class="form-group" *ngFor="let resolution of liveResolutions">
@@ -136,7 +136,7 @@
136 <div class="form-group" [ngClass]="getDisabledLiveTranscodingClass()"> 136 <div class="form-group" [ngClass]="getDisabledLiveTranscodingClass()">
137 <label i18n for="liveTranscodingThreads">Live transcoding threads</label> 137 <label i18n for="liveTranscodingThreads">Live transcoding threads</label>
138 138
139 <span class="muted ml-1"> 139 <span class="muted ms-1">
140 <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n> 140 <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n>
141 will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with VOD transcoding 141 will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with VOD transcoding
142 </ng-container> 142 </ng-container>
@@ -157,7 +157,7 @@
157 157
158 <div class="form-group mt-4" [ngClass]="getDisabledLiveTranscodingClass()"> 158 <div class="form-group mt-4" [ngClass]="getDisabledLiveTranscodingClass()">
159 <label i18n for="liveTranscodingProfile">Live transcoding profile</label> 159 <label i18n for="liveTranscodingProfile">Live transcoding profile</label>
160 <span class="muted ml-1" i18n>new live transcoding profiles can be added by PeerTube plugins</span> 160 <span class="muted ms-1" i18n>new live transcoding profiles can be added by PeerTube plugins</span>
161 161
162 <my-select-options 162 <my-select-options
163 id="liveTranscodingProfile" 163 id="liveTranscodingProfile"
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 5c0bea4a5..3d8ab094f 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
@@ -1,8 +1,8 @@
1<ng-container [formGroup]="form"> 1<ng-container [formGroup]="form">
2 2
3 <div class="form-row mt-4"> <!-- transcoding grid --> 3 <div class="row mt-4"> <!-- transcoding grid -->
4 <div class="form-group col-12 col-lg-4 col-xl-3"></div> 4 <div class="col-12 col-lg-4 col-xl-3"></div>
5 <div class="form-group form-group-right col-12 col-lg-8"> 5 <div class="col-12 col-lg-8">
6 6
7 <div class="callout callout-info"> 7 <div class="callout callout-info">
8 <span i18n> 8 <span i18n>
@@ -21,8 +21,8 @@
21 </div> 21 </div>
22 </div> 22 </div>
23 23
24 <div class="form-row mt-2"> <!-- transcoding grid --> 24 <div class="row mt-4"> <!-- transcoding grid -->
25 <div class="form-group col-12 col-lg-4 col-xl-3"> 25 <div class="col-12 col-lg-4 col-xl-3">
26 <div i18n class="inner-form-title">TRANSCODING</div> 26 <div i18n class="inner-form-title">TRANSCODING</div>
27 <div i18n class="inner-form-description"> 27 <div i18n class="inner-form-description">
28 Process uploaded videos so that they are in a streamable form that any device can play. Though costly in 28 Process uploaded videos so that they are in a streamable form that any device can play. Though costly in
@@ -30,11 +30,11 @@
30 </div> 30 </div>
31 </div> 31 </div>
32 32
33 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 33 <div class="col-12 col-lg-8 col-xl-9">
34 34
35 <ng-container formGroupName="transcoding"> 35 <ng-container formGroupName="transcoding">
36 36
37 <div class="form-group mb-0 col-12 col-xl-11"> 37 <div class="col-12 col-xl-11">
38 <my-peertube-checkbox inputName="transcodingEnabled" formControlName="enabled" [recommended]="true"> 38 <my-peertube-checkbox inputName="transcodingEnabled" formControlName="enabled" [recommended]="true">
39 <ng-template ptTemplate="label"> 39 <ng-template ptTemplate="label">
40 <ng-container i18n>Transcoding enabled</ng-container> 40 <ng-container i18n>Transcoding enabled</ng-container>
@@ -115,7 +115,7 @@
115 <div class="form-group" [ngClass]="getTranscodingDisabledClass()"> 115 <div class="form-group" [ngClass]="getTranscodingDisabledClass()">
116 <label i18n>Resolutions to generate per enabled format</label> 116 <label i18n>Resolutions to generate per enabled format</label>
117 117
118 <div class="ml-2 mt-2 d-flex flex-column"> 118 <div class="ms-2 mt-2 d-flex flex-column">
119 <ng-container formGroupName="resolutions"> 119 <ng-container formGroupName="resolutions">
120 <div class="form-group" *ngFor="let resolution of resolutions"> 120 <div class="form-group" *ngFor="let resolution of resolutions">
121 <my-peertube-checkbox 121 <my-peertube-checkbox
@@ -142,7 +142,7 @@
142 142
143 <div class="form-group mt-4" [ngClass]="getTranscodingDisabledClass()"> 143 <div class="form-group mt-4" [ngClass]="getTranscodingDisabledClass()">
144 <label i18n for="transcodingThreads">Transcoding threads</label> 144 <label i18n for="transcodingThreads">Transcoding threads</label>
145 <span class="muted ml-1"> 145 <span class="muted ms-1">
146 <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n> 146 <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n>
147 will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with live transcoding 147 will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with live transcoding
148 </ng-container> 148 </ng-container>
@@ -162,9 +162,9 @@
162 <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div> 162 <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
163 </div> 163 </div>
164 164
165 <div class="form-group mt-4" [ngClass]="getTranscodingDisabledClass()"> 165 <div class="form-group" [ngClass]="getTranscodingDisabledClass()">
166 <label i18n for="transcodingConcurrency">Transcoding jobs concurrency</label> 166 <label i18n for="transcodingConcurrency">Transcoding jobs concurrency</label>
167 <span class="muted ml-1" i18n>allows to transcode multiple files in parallel. ⚠️ Requires a PeerTube restart</span> 167 <span class="muted ms-1" i18n>allows to transcode multiple files in parallel. ⚠️ Requires a PeerTube restart</span>
168 168
169 <div class="number-with-unit"> 169 <div class="number-with-unit">
170 <input type="number" name="transcodingConcurrency" formControlName="concurrency" /> 170 <input type="number" name="transcodingConcurrency" formControlName="concurrency" />
@@ -174,9 +174,9 @@
174 <div *ngIf="formErrors.transcoding.concurrency" class="form-error">{{ formErrors.transcoding.concurrency }}</div> 174 <div *ngIf="formErrors.transcoding.concurrency" class="form-error">{{ formErrors.transcoding.concurrency }}</div>
175 </div> 175 </div>
176 176
177 <div class="form-group mt-4" [ngClass]="getTranscodingDisabledClass()"> 177 <div class="form-group" [ngClass]="getTranscodingDisabledClass()">
178 <label i18n for="transcodingProfile">Transcoding profile</label> 178 <label i18n for="transcodingProfile">Transcoding profile</label>
179 <span class="muted ml-1" i18n>new transcoding profiles can be added by PeerTube plugins</span> 179 <span class="muted ms-1" i18n>new transcoding profiles can be added by PeerTube plugins</span>
180 180
181 <my-select-options 181 <my-select-options
182 id="transcodingProfile" 182 id="transcodingProfile"
@@ -193,15 +193,15 @@
193 </div> 193 </div>
194 </div> 194 </div>
195 195
196 <div class="form-row mt-2"> <!-- video studio grid --> 196 <div class="row mt-2"> <!-- video studio grid -->
197 <div class="form-group col-12 col-lg-4 col-xl-3"> 197 <div class="col-12 col-lg-4 col-xl-3">
198 <div i18n class="inner-form-title">VIDEO STUDIO</div> 198 <div i18n class="inner-form-title">VIDEO STUDIO</div>
199 <div i18n class="inner-form-description"> 199 <div i18n class="inner-form-description">
200 Allows your users to edit their video (cut, add intro/outro, add a watermark etc) 200 Allows your users to edit their video (cut, add intro/outro, add a watermark etc)
201 </div> 201 </div>
202 </div> 202 </div>
203 203
204 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 204 <div class="col-12 col-lg-8 col-xl-9">
205 205
206 <ng-container formGroupName="videoStudio"> 206 <ng-container formGroupName="videoStudio">
207 <div class="form-group" [ngClass]="getTranscodingDisabledClass()"> 207 <div class="form-group" [ngClass]="getTranscodingDisabledClass()">
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html
index 1df7bb164..38def2fbb 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.html
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html
@@ -12,7 +12,7 @@
12> 12>
13 <ng-template pTemplate="caption"> 13 <ng-template pTemplate="caption">
14 <div class="caption"> 14 <div class="caption">
15 <div class="ml-auto"> 15 <div class="ms-auto">
16 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 16 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
17 </div> 17 </div>
18 </div> 18 </div>
@@ -46,10 +46,10 @@
46 </td> 46 </td>
47 47
48 <td *ngIf="follow.state === 'accepted'"> 48 <td *ngIf="follow.state === 'accepted'">
49 <span class="badge badge-green" i18n>Accepted</span> 49 <span class="pt-badge badge-green" i18n>Accepted</span>
50 </td> 50 </td>
51 <td *ngIf="follow.state === 'pending'"> 51 <td *ngIf="follow.state === 'pending'">
52 <span class="badge badge-yellow" i18n>Pending</span> 52 <span class="pt-badge badge-yellow" i18n>Pending</span>
53 </td> 53 </td>
54 54
55 <td>{{ follow.score }}</td> 55 <td>{{ follow.score }}</td>
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 c40b36e10..bac7b2b01 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,5 +1,6 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { prepareIcu } from '@app/helpers'
3import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' 4import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { InstanceFollowService } from '@app/shared/shared-instance' 6import { InstanceFollowService } from '@app/shared/shared-instance'
@@ -60,7 +61,13 @@ export class FollowModalComponent extends FormReactive implements OnInit {
60 this.followService.follow(hostsOrHandles) 61 this.followService.follow(hostsOrHandles)
61 .subscribe({ 62 .subscribe({
62 next: () => { 63 next: () => {
63 this.notifier.success($localize`Follow request(s) sent!`) 64 this.notifier.success(
65 prepareIcu($localize`{count, plural, =1 {Follow request} other {Follow requests}} sent!`)(
66 { count: hostsOrHandles.length },
67 $localize`Follow request(s) sent!`
68 )
69 )
70
64 this.newFollow.emit() 71 this.newFollow.emit()
65 }, 72 },
66 73
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html
index 767e92d18..207ca81a4 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.html
+++ b/client/src/app/+admin/follows/following-list/following-list.component.html
@@ -19,7 +19,7 @@
19 </a> 19 </a>
20 </div> 20 </div>
21 21
22 <div class="ml-auto"> 22 <div class="ms-auto">
23 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 23 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
24 </div> 24 </div>
25 </div> 25 </div>
@@ -48,10 +48,10 @@
48 </td> 48 </td>
49 49
50 <td *ngIf="follow.state === 'accepted'"> 50 <td *ngIf="follow.state === 'accepted'">
51 <span class="badge badge-green" i18n>Accepted</span> 51 <span class="pt-badge badge-green" i18n>Accepted</span>
52 </td> 52 </td>
53 <td *ngIf="follow.state === 'pending'"> 53 <td *ngIf="follow.state === 'pending'">
54 <span class="badge badge-yellow" i18n>Pending</span> 54 <span class="pt-badge badge-yellow" i18n>Pending</span>
55 </td> 55 </td>
56 56
57 <td>{{ follow.createdAt | date: 'short' }}</td> 57 <td>{{ follow.createdAt | date: 'short' }}</td>
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
index 3634951c9..b302014b6 100644
--- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
@@ -13,7 +13,7 @@
13> 13>
14 <ng-template pTemplate="caption"> 14 <ng-template pTemplate="caption">
15 <div class="caption"> 15 <div class="caption">
16 <div class="ml-auto"> 16 <div class="ms-auto">
17 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> 17 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
18 </div> 18 </div>
19 </div> 19 </div>
@@ -53,11 +53,11 @@
53 </td> 53 </td>
54 54
55 <td> 55 <td>
56 <span *ngIf="videoBlock.video.nsfw" class="badge badge-red" i18n>NSFW</span> 56 <span *ngIf="videoBlock.video.nsfw" class="pt-badge badge-red" i18n>NSFW</span>
57 </td> 57 </td>
58 58
59 <td> 59 <td>
60 <span *ngIf="videoBlock.unfederated" class="badge badge-blue" i18n>Unfederated</span> 60 <span *ngIf="videoBlock.unfederated" class="pt-badge badge-blue" i18n>Unfederated</span>
61 </td> 61 </td>
62 62
63 <td> 63 <td>
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.html b/client/src/app/+admin/overview/comments/video-comment-list.component.html
index 27a5d82ff..6fdefbfe2 100644
--- a/client/src/app/+admin/overview/comments/video-comment-list.component.html
+++ b/client/src/app/+admin/overview/comments/video-comment-list.component.html
@@ -25,7 +25,7 @@
25 </my-action-dropdown> 25 </my-action-dropdown>
26 </div> 26 </div>
27 27
28 <div class="ml-auto right-form"> 28 <div class="ms-auto right-form">
29 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> 29 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
30 30
31 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button> 31 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>
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 f3f43a900..f1b27d846 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,6 +7,7 @@ import { DropdownAction } from '@app/shared/shared-main'
7import { BulkService } from '@app/shared/shared-moderation' 7import { BulkService } from '@app/shared/shared-moderation'
8import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' 8import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment'
9import { FeedFormat, UserRight } from '@shared/models' 9import { FeedFormat, UserRight } from '@shared/models'
10import { prepareIcu } from '@app/helpers'
10 11
11@Component({ 12@Component({
12 selector: 'my-video-comment-list', 13 selector: 'my-video-comment-list',
@@ -145,7 +146,13 @@ export class VideoCommentListComponent extends RestTable implements OnInit {
145 this.videoCommentService.deleteVideoComments(commentArgs) 146 this.videoCommentService.deleteVideoComments(commentArgs)
146 .subscribe({ 147 .subscribe({
147 next: () => { 148 next: () => {
148 this.notifier.success($localize`${commentArgs.length} comments deleted.`) 149 this.notifier.success(
150 prepareIcu($localize`{count, plural, =1 {1 comment} other {{count} comments}} deleted.`)(
151 { count: commentArgs.length },
152 $localize`${commentArgs.length} comment(s) deleted.`
153 )
154 )
155
149 this.reloadData() 156 this.reloadData()
150 }, 157 },
151 158
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html
index 772ebf272..7e0eaf280 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html
@@ -57,7 +57,7 @@
57 </div> 57 </div>
58</ng-template> 58</ng-template>
59 59
60<div class="form-row" *ngIf="!isInBigView()"> <!-- hidden on large screens, as it is then displayed on the right side of the form --> 60<div class="row d-xxl-none"> <!-- hidden on large screens, as it is then displayed on the right side of the form -->
61 <div class="col-12 col-xl-3"></div> 61 <div class="col-12 col-xl-3"></div>
62 62
63 <div class="col-12 col-xl-9"> 63 <div class="col-12 col-xl-9">
@@ -67,8 +67,8 @@
67 67
68<div *ngIf="error" class="alert alert-danger">{{ error }}</div> 68<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
69 69
70<div class="form-row mt-4"> <!-- user grid --> 70<div class="row mt-4"> <!-- user grid -->
71 <div class="form-group col-12 col-lg-4 col-xl-3"> 71 <div class="col-12 col-lg-4 col-xl-3">
72 <div class="anchor" id="user"></div> <!-- user anchor --> 72 <div class="anchor" id="user"></div> <!-- user anchor -->
73 <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div> 73 <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div>
74 <div *ngIf="!isCreation() && user" class="account-title"> 74 <div *ngIf="!isCreation() && user" class="account-title">
@@ -76,150 +76,150 @@
76 </div> 76 </div>
77 </div> 77 </div>
78 78
79 <div class="form-group col-12 col-lg-8 col-xl-9" [ngClass]="{ 'form-row': isInBigView() }"> 79 <div class="col-12 col-lg-8 col-xl-9">
80 80 <div class="row">
81 <form role="form" (ngSubmit)="formValidated()" [formGroup]="form" [ngClass]="{ 'col-5': isInBigView() }"> 81 <form class="col" role="form" (ngSubmit)="formValidated()" [formGroup]="form">
82 <div class="form-group" *ngIf="isCreation()"> 82 <div class="form-group" *ngIf="isCreation()">
83 <label i18n for="username">Username</label> 83 <label i18n for="username">Username</label>
84 <input 84 <input
85 type="text" id="username" i18n-placeholder placeholder="john" class="form-control" 85 type="text" id="username" i18n-placeholder placeholder="john" class="form-control"
86 formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" 86 formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
87 > 87 >
88 <div *ngIf="formErrors.username" class="form-error"> 88 <div *ngIf="formErrors.username" class="form-error">
89 {{ formErrors.username }} 89 {{ formErrors.username }}
90 </div>
90 </div> 91 </div>
91 </div>
92 92
93 <div class="form-group" *ngIf="isCreation()"> 93 <div class="form-group" *ngIf="isCreation()">
94 <label i18n for="channelName">Channel name</label> 94 <label i18n for="channelName">Channel name</label>
95 <input 95 <input
96 type="text" id="channelName" i18n-placeholder placeholder="john_channel" class="form-control" 96 type="text" id="channelName" i18n-placeholder placeholder="john_channel" class="form-control"
97 formControlName="channelName" [ngClass]="{ 'input-error': formErrors['channelName'] }" 97 formControlName="channelName" [ngClass]="{ 'input-error': formErrors['channelName'] }"
98 > 98 >
99 <div *ngIf="formErrors.channelName" class="form-error"> 99 <div *ngIf="formErrors.channelName" class="form-error">
100 {{ formErrors.channelName }} 100 {{ formErrors.channelName }}
101 </div>
101 </div> 102 </div>
102 </div>
103 103
104 <div class="form-group"> 104 <div class="form-group">
105 <label i18n for="email">Email</label> 105 <label i18n for="email">Email</label>
106 <input 106 <input
107 type="text" id="email" i18n-placeholder placeholder="mail@example.com" class="form-control" 107 type="text" id="email" i18n-placeholder placeholder="mail@example.com" class="form-control"
108 formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" 108 formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }"
109 autocomplete="off" [readonly]="user && user.pluginAuth !== null" 109 autocomplete="off" [readonly]="user && user.pluginAuth !== null"
110 > 110 >
111 <div *ngIf="formErrors.email" class="form-error"> 111 <div *ngIf="formErrors.email" class="form-error">
112 {{ formErrors.email }} 112 {{ formErrors.email }}
113 </div>
113 </div> 114 </div>
114 </div>
115 115
116 <div class="form-group" *ngIf="isCreation()"> 116 <div class="form-group" *ngIf="isCreation()">
117 <label i18n for="password">Password</label> 117 <label i18n for="password">Password</label>
118 <my-help *ngIf="isPasswordOptional()"> 118 <my-help *ngIf="isPasswordOptional()">
119 <ng-template ptTemplate="customHtml"> 119 <ng-template ptTemplate="customHtml">
120 <ng-container i18n> 120 <ng-container i18n>
121 If you leave the password empty, an email will be sent to the user. 121 If you leave the password empty, an email will be sent to the user.
122 </ng-container> 122 </ng-container>
123 </ng-template> 123 </ng-template>
124 </my-help> 124 </my-help>
125 125
126 <my-input-toggle-hidden 126 <my-input-toggle-hidden
127 formControlName="password" inputId="password" [ngClass]="{ 'input-error': formErrors['password'] }" autocomplete="new-password" 127 formControlName="password" inputId="password" [ngClass]="{ 'input-error': formErrors['password'] }" autocomplete="new-password"
128 ></my-input-toggle-hidden> 128 ></my-input-toggle-hidden>
129 129
130 <div *ngIf="formErrors.password" class="form-error"> 130 <div *ngIf="formErrors.password" class="form-error">
131 {{ formErrors.password }} 131 {{ formErrors.password }}
132 </div>
132 </div> 133 </div>
133 </div>
134 134
135 <div class="form-group"> 135 <div class="form-group">
136 <label i18n for="role">Role</label> 136 <label i18n for="role">Role</label>
137 <div class="peertube-select-container"> 137 <div class="peertube-select-container">
138 <select id="role" formControlName="role" class="form-control"> 138 <select id="role" formControlName="role" class="form-control">
139 <option *ngFor="let role of roles" [value]="role.value"> 139 <option *ngFor="let role of roles" [value]="role.value">
140 {{ role.label }} 140 {{ role.label }}
141 </option> 141 </option>
142 </select> 142 </select>
143 </div>
144
145 <div *ngIf="formErrors.role" class="form-error">
146 {{ formErrors.role }}
147 </div>
143 </div> 148 </div>
144 149
145 <div *ngIf="formErrors.role" class="form-error"> 150 <div class="form-group">
146 {{ formErrors.role }} 151 <label i18n for="videoQuota">Video quota</label>
152
153 <my-select-custom-value
154 id="videoQuota"
155 [items]="videoQuotaOptions"
156 formControlName="videoQuota"
157 i18n-inputSuffix inputSuffix="bytes" inputType="number"
158 [clearable]="false"
159 ></my-select-custom-value>
160
161 <div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
162 Transcoding is enabled. The video quota only takes into account <strong>original</strong> video size. <br />
163 At most, this user could upload ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
164 </div>
165
166 <div *ngIf="formErrors.videoQuota" class="form-error">
167 {{ formErrors.videoQuota }}
168 </div>
147 </div> 169 </div>
148 </div>
149 170
150 <div class="form-group"> 171 <div class="form-group">
151 <label i18n for="videoQuota">Video quota</label> 172 <label i18n for="videoQuotaDaily">Daily video quota</label>
152
153 <my-select-custom-value
154 id="videoQuota"
155 [items]="videoQuotaOptions"
156 formControlName="videoQuota"
157 i18n-inputSuffix inputSuffix="bytes" inputType="number"
158 [clearable]="false"
159 ></my-select-custom-value>
160
161 <div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
162 Transcoding is enabled. The video quota only takes into account <strong>original</strong> video size. <br />
163 At most, this user could upload ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
164 </div>
165 173
166 <div *ngIf="formErrors.videoQuota" class="form-error"> 174 <my-select-custom-value
167 {{ formErrors.videoQuota }} 175 id="videoQuotaDaily"
168 </div> 176 [items]="videoQuotaDailyOptions"
169 </div> 177 formControlName="videoQuotaDaily"
178 i18n-inputSuffix inputSuffix="bytes" inputType="number"
179 [clearable]="false"
180 ></my-select-custom-value>
170 181
171 <div class="form-group"> 182 <div *ngIf="formErrors.videoQuotaDaily" class="form-error">
172 <label i18n for="videoQuotaDaily">Daily video quota</label> 183 {{ formErrors.videoQuotaDaily }}
173 184 </div>
174 <my-select-custom-value
175 id="videoQuotaDaily"
176 [items]="videoQuotaDailyOptions"
177 formControlName="videoQuotaDaily"
178 i18n-inputSuffix inputSuffix="bytes" inputType="number"
179 [clearable]="false"
180 ></my-select-custom-value>
181
182 <div *ngIf="formErrors.videoQuotaDaily" class="form-error">
183 {{ formErrors.videoQuotaDaily }}
184 </div> 185 </div>
185 </div>
186 186
187 <div class="form-group" *ngIf="!isCreation() && getAuthPlugins().length !== 0"> 187 <div class="form-group" *ngIf="!isCreation() && getAuthPlugins().length !== 0">
188 <label i18n for="pluginAuth">Auth plugin</label> 188 <label i18n for="pluginAuth">Auth plugin</label>
189 189
190 <div class="peertube-select-container"> 190 <div class="peertube-select-container">
191 <select id="pluginAuth" formControlName="pluginAuth" class="form-control"> 191 <select id="pluginAuth" formControlName="pluginAuth" class="form-control">
192 <option [value]="null" i18n>None (local authentication)</option> 192 <option [value]="null" i18n>None (local authentication)</option>
193 <option *ngFor="let authPlugin of getAuthPlugins()" [value]="authPlugin">{{ authPlugin }}</option> 193 <option *ngFor="let authPlugin of getAuthPlugins()" [value]="authPlugin">{{ authPlugin }}</option>
194 </select> 194 </select>
195 </div>
195 </div> 196 </div>
196 </div>
197 197
198 <div class="form-group"> 198 <div class="form-group">
199 <my-peertube-checkbox 199 <my-peertube-checkbox
200 inputName="byPassAutoBlock" formControlName="byPassAutoBlock" 200 inputName="byPassAutoBlock" formControlName="byPassAutoBlock"
201 i18n-labelText labelText="Doesn't need review before a video goes public" 201 i18n-labelText labelText="Doesn't need review before a video goes public"
202 ></my-peertube-checkbox> 202 ></my-peertube-checkbox>
203 </div> 203 </div>
204 204
205 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 205 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
206 </form> 206 </form>
207 207
208 <div *ngIf="isInBigView()" class="col-7"> 208 <div class="d-none d-xxl-block col-7">
209 <ng-template *ngTemplateOutlet="dashboard"></ng-template> 209 <ng-template *ngTemplateOutlet="dashboard"></ng-template>
210 </div>
210 </div> 211 </div>
211
212 </div> 212 </div>
213</div> 213</div>
214 214
215 215
216<div *ngIf="!isCreation() && user && user.pluginAuth === null" class="form-row mt-4"> <!-- danger zone grid --> 216<div *ngIf="!isCreation() && user && user.pluginAuth === null" class="row mt-4"> <!-- danger zone grid -->
217 <div class="form-group col-12 col-lg-4 col-xl-3"> 217 <div class="col-12 col-lg-4 col-xl-3">
218 <div class="anchor" id="danger"></div> <!-- danger zone anchor --> 218 <div class="anchor" id="danger"></div> <!-- danger zone anchor -->
219 <div i18n class="account-title account-title-danger">DANGER ZONE</div> 219 <div i18n class="account-title account-title-danger">DANGER ZONE</div>
220 </div> 220 </div>
221 221
222 <div class="form-group col-12 col-lg-8 col-xl-9" [ngClass]="{ 'form-row': isInBigView() }"> 222 <div class="col-12 col-lg-8 col-xl-9">
223 223
224 <div class="danger-zone"> 224 <div class="danger-zone">
225 <div class="form-group reset-password-email"> 225 <div class="form-group reset-password-email">
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.ts b/client/src/app/+admin/overview/users/user-edit/user-edit.ts
index 069b62a53..395d07423 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.ts
@@ -46,10 +46,6 @@ export abstract class UserEdit extends FormReactive implements OnInit {
46 .concat(this.serverConfig.plugin.registeredExternalAuths.map(p => p.npmName)) 46 .concat(this.serverConfig.plugin.registeredExternalAuths.map(p => p.npmName))
47 } 47 }
48 48
49 isInBigView () {
50 return this.screenService.getWindowInnerWidth() > 1600
51 }
52
53 buildRoles () { 49 buildRoles () {
54 const authUser = this.auth.getUser() 50 const authUser = this.auth.getUser()
55 51
diff --git a/client/src/app/+admin/overview/users/user-edit/user-password.component.html b/client/src/app/+admin/overview/users/user-edit/user-password.component.html
index 1238d1839..35f36e465 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-password.component.html
+++ b/client/src/app/+admin/overview/users/user-edit/user-password.component.html
@@ -1,20 +1,16 @@
1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
2 <div class="form-group"> 2 <div class="input-group">
3 <input id="password" [attr.type]="showPassword ? 'text' : 'password'" class="form-control"
4 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
5 >
6 <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
7 <ng-container *ngIf="!showPassword" i18n>Show</ng-container>
8 <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
9 </button>
10 </div>
3 11
4 <div class="input-group"> 12 <div *ngIf="formErrors.password" class="form-error">
5 <input id="password" [attr.type]="showPassword ? 'text' : 'password'" class="form-control" 13 {{ formErrors.password }}
6 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
7 >
8 <div class="input-group-append">
9 <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
10 <ng-container *ngIf="!showPassword" i18n>Show</ng-container>
11 <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
12 </button>
13 </div>
14 </div>
15 <div *ngIf="formErrors.password" class="form-error">
16 {{ formErrors.password }}
17 </div>
18 </div> 14 </div>
19 15
20 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 16 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
diff --git a/client/src/app/+admin/overview/users/user-edit/user-password.component.scss b/client/src/app/+admin/overview/users/user-edit/user-password.component.scss
index acb680682..54f782086 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-password.component.scss
+++ b/client/src/app/+admin/overview/users/user-edit/user-password.component.scss
@@ -1,13 +1,9 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4input:not([type=submit]):not([type=checkbox]) { 4input[type=text],
5input[type=password] {
5 @include peertube-input-text(340px); 6 @include peertube-input-text(340px);
6
7 display: block;
8 border-top-right-radius: 0;
9 border-bottom-right-radius: 0;
10 border-right: 0;
11} 7}
12 8
13input[type=submit] { 9input[type=submit] {
@@ -17,7 +13,3 @@ input[type=submit] {
17 13
18 margin-top: 10px; 14 margin-top: 10px;
19} 15}
20
21.input-group-append {
22 height: 30px;
23}
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.html b/client/src/app/+admin/overview/users/user-list/user-list.component.html
index 30d10e3cf..f6915ae9a 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.html
@@ -5,7 +5,7 @@
5 5
6<p-table 6<p-table
7 [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" 7 [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
8 [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" [(selection)]="selectedUsers" 8 [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" [(selection)]="selectedUsers"
9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" 9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true"
10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users" 11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users"
@@ -26,7 +26,7 @@
26 </a> 26 </a>
27 </div> 27 </div>
28 28
29 <div class="ml-auto"> 29 <div class="ms-auto">
30 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> 30 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
31 </div> 31 </div>
32 32
@@ -90,7 +90,7 @@
90 </my-user-moderation-dropdown> 90 </my-user-moderation-dropdown>
91 </td> 91 </td>
92 92
93 <td *ngIf="isSelected('username')"> 93 <td *ngIf="isSelected('username')" class="cell-username">
94 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/a/' + user.username ]"> 94 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/a/' + user.username ]">
95 <div class="chip two-lines"> 95 <div class="chip two-lines">
96 <my-actor-avatar [account]="user?.account" size="32"></my-actor-avatar> 96 <my-actor-avatar [account]="user?.account" size="32"></my-actor-avatar>
@@ -101,13 +101,13 @@
101 </div> 101 </div>
102 </a> 102 </a>
103 103
104 <div *ngIf="user.accountMutedStatus.mutedByInstance" class="badges-username badge badge-red" i18n>Muted</div> 104 <div *ngIf="user.accountMutedStatus.mutedByInstance" class="pt-badge badge-red" i18n>Muted</div>
105 <div *ngIf="user.blocked" class="badges-username badge badge-red" i18n>Banned</div> 105 <div *ngIf="user.blocked" class="pt-badge badge-red" i18n>Banned</div>
106 </td> 106 </td>
107 107
108 <td *ngIf="isSelected('role')"> 108 <td *ngIf="isSelected('role')">
109 <span *ngIf="user.blocked" class="badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span> 109 <span *ngIf="user.blocked" class="pt-badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span>
110 <span *ngIf="!user.blocked" class="badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span> 110 <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span>
111 </td> 111 </td>
112 112
113 <td *ngIf="isSelected('email')" [title]="user.email"> 113 <td *ngIf="isSelected('email')" [title]="user.email">
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.scss b/client/src/app/+admin/overview/users/user-list/user-list.component.scss
index 8160703f0..6449f5064 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.scss
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.scss
@@ -1,5 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3@use '~bootstrap/scss/functions' as *;
3 4
4.add-button { 5.add-button {
5 @include create-button; 6 @include create-button;
@@ -23,8 +24,8 @@ tr.banned > td {
23 font-weight: $font-semibold; 24 font-weight: $font-semibold;
24} 25}
25 26
26.badges-username { 27.cell-username .pt-badge {
27 margin-left: 15px; 28 @include margin-left(15px);
28} 29}
29 30
30.user-table-primary-text .glyphicon { 31.user-table-primary-text .glyphicon {
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 d22e1355e..f7dc22256 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
@@ -1,8 +1,8 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { Component, OnInit, ViewChild } from '@angular/core' 2import { Component, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' 4import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
5import { getAPIHost } from '@app/helpers' 5import { prepareIcu, getAPIHost } from '@app/helpers'
6import { AdvancedInputFilter } from '@app/shared/shared-forms' 6import { AdvancedInputFilter } from '@app/shared/shared-forms'
7import { Actor, DropdownAction } from '@app/shared/shared-main' 7import { Actor, DropdownAction } from '@app/shared/shared-main'
8import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation' 8import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation'
@@ -22,6 +22,8 @@ type UserForList = User & {
22 styleUrls: [ './user-list.component.scss' ] 22 styleUrls: [ './user-list.component.scss' ]
23}) 23})
24export class UserListComponent extends RestTable implements OnInit { 24export class UserListComponent extends RestTable implements OnInit {
25 private static readonly LOCAL_STORAGE_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns'
26
25 @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent 27 @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent
26 28
27 users: (User & { accountMutedStatus: AccountMutedStatus })[] = [] 29 users: (User & { accountMutedStatus: AccountMutedStatus })[] = []
@@ -56,7 +58,7 @@ export class UserListComponent extends RestTable implements OnInit {
56 58
57 requiresEmailVerification = false 59 requiresEmailVerification = false
58 60
59 private _selectedColumns: string[] 61 private _selectedColumns: string[] = []
60 62
61 constructor ( 63 constructor (
62 protected route: ActivatedRoute, 64 protected route: ActivatedRoute,
@@ -66,7 +68,8 @@ export class UserListComponent extends RestTable implements OnInit {
66 private serverService: ServerService, 68 private serverService: ServerService,
67 private auth: AuthService, 69 private auth: AuthService,
68 private blocklist: BlocklistService, 70 private blocklist: BlocklistService,
69 private userAdminService: UserAdminService 71 private userAdminService: UserAdminService,
72 private peertubeLocalStorage: LocalStorageService
70 ) { 73 ) {
71 super() 74 super()
72 } 75 }
@@ -76,11 +79,13 @@ export class UserListComponent extends RestTable implements OnInit {
76 } 79 }
77 80
78 get selectedColumns () { 81 get selectedColumns () {
79 return this._selectedColumns 82 return this._selectedColumns || []
80 } 83 }
81 84
82 set selectedColumns (val: string[]) { 85 set selectedColumns (val: string[]) {
83 this._selectedColumns = val 86 this._selectedColumns = val
87
88 this.saveSelectedColumns()
84 } 89 }
85 90
86 ngOnInit () { 91 ngOnInit () {
@@ -126,14 +131,35 @@ export class UserListComponent extends RestTable implements OnInit {
126 { id: 'role', label: $localize`Role` }, 131 { id: 'role', label: $localize`Role` },
127 { id: 'email', label: $localize`Email` }, 132 { id: 'email', label: $localize`Email` },
128 { id: 'quota', label: $localize`Video quota` }, 133 { id: 'quota', label: $localize`Video quota` },
129 { id: 'createdAt', label: $localize`Created` } 134 { id: 'createdAt', label: $localize`Created` },
135 { id: 'lastLoginDate', label: $localize`Last login` },
136
137 { id: 'quotaDaily', label: $localize`Daily quota` },
138 { id: 'pluginAuth', label: $localize`Auth plugin` }
130 ] 139 ]
131 140
132 this.selectedColumns = this.columns.map(c => c.id) 141 this.loadSelectedColumns()
142 }
143
144 loadSelectedColumns () {
145 const result = this.peertubeLocalStorage.getItem(UserListComponent.LOCAL_STORAGE_SELECTED_COLUMNS_KEY)
133 146
134 this.columns.push({ id: 'quotaDaily', label: $localize`Daily quota` }) 147 if (result) {
135 this.columns.push({ id: 'pluginAuth', label: $localize`Auth plugin` }) 148 try {
136 this.columns.push({ id: 'lastLoginDate', label: $localize`Last login` }) 149 this.selectedColumns = JSON.parse(result)
150 return
151 } catch (err) {
152 console.error('Cannot load selected columns.', err)
153 }
154 }
155
156 // Default behaviour
157 this.selectedColumns = [ 'username', 'role', 'email', 'quota', 'createdAt', 'lastLoginDate' ]
158 return
159 }
160
161 saveSelectedColumns () {
162 this.peertubeLocalStorage.setItem(UserListComponent.LOCAL_STORAGE_SELECTED_COLUMNS_KEY, JSON.stringify(this.selectedColumns))
137 } 163 }
138 164
139 getIdentifier () { 165 getIdentifier () {
@@ -183,13 +209,25 @@ export class UserListComponent extends RestTable implements OnInit {
183 } 209 }
184 210
185 async unbanUsers (users: User[]) { 211 async unbanUsers (users: User[]) {
186 const res = await this.confirmService.confirm($localize`Do you really want to unban ${users.length} users?`, $localize`Unban`) 212 const res = await this.confirmService.confirm(
213 prepareIcu($localize`Do you really want to unban {count, plural, =1 {1 user} other {{count} users}}?`)(
214 { count: users.length },
215 $localize`Do you really want to unban ${users.length} users?`
216 ),
217 $localize`Unban`
218 )
219
187 if (res === false) return 220 if (res === false) return
188 221
189 this.userAdminService.unbanUsers(users) 222 this.userAdminService.unbanUsers(users)
190 .subscribe({ 223 .subscribe({
191 next: () => { 224 next: () => {
192 this.notifier.success($localize`${users.length} users unbanned.`) 225 this.notifier.success(
226 prepareIcu($localize`{count, plural, =1 {1 user} other {{count} users}} unbanned.`)(
227 { count: users.length },
228 $localize`${users.length} users unbanned.`
229 )
230 )
193 this.reloadData() 231 this.reloadData()
194 }, 232 },
195 233
@@ -198,21 +236,28 @@ export class UserListComponent extends RestTable implements OnInit {
198 } 236 }
199 237
200 async removeUsers (users: User[]) { 238 async removeUsers (users: User[]) {
201 for (const user of users) { 239 if (users.some(u => u.username === 'root')) {
202 if (user.username === 'root') { 240 this.notifier.error($localize`You cannot delete root.`)
203 this.notifier.error($localize`You cannot delete root.`) 241 return
204 return
205 }
206 } 242 }
207 243
208 const message = $localize`If you remove these users, you will not be able to create others with the same username!` 244 const message = $localize`<p>You can't create users or channels with a username that already used by a deleted user/channel.</p>` +
245 $localize`It means the following usernames will be permanently deleted and cannot be recovered:` +
246 '<ul>' + users.map(u => '<li>' + u.username + '</li>').join('') + '</ul>'
247
209 const res = await this.confirmService.confirm(message, $localize`Delete`) 248 const res = await this.confirmService.confirm(message, $localize`Delete`)
210 if (res === false) return 249 if (res === false) return
211 250
212 this.userAdminService.removeUser(users) 251 this.userAdminService.removeUser(users)
213 .subscribe({ 252 .subscribe({
214 next: () => { 253 next: () => {
215 this.notifier.success($localize`${users.length} users deleted.`) 254 this.notifier.success(
255 prepareIcu($localize`{count, plural, =1 {1 user} other {{count} users}} deleted.`)(
256 { count: users.length },
257 $localize`${users.length} users deleted.`
258 )
259 )
260
216 this.reloadData() 261 this.reloadData()
217 }, 262 },
218 263
@@ -224,7 +269,13 @@ export class UserListComponent extends RestTable implements OnInit {
224 this.userAdminService.updateUsers(users, { emailVerified: true }) 269 this.userAdminService.updateUsers(users, { emailVerified: true })
225 .subscribe({ 270 .subscribe({
226 next: () => { 271 next: () => {
227 this.notifier.success($localize`${users.length} users email set as verified.`) 272 this.notifier.success(
273 prepareIcu($localize`{count, plural, =1 {1 user} other {{count} users}} email set as verified.`)(
274 { count: users.length },
275 $localize`${users.length} users email set as verified.`
276 )
277 )
278
228 this.reloadData() 279 this.reloadData()
229 }, 280 },
230 281
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 75d9be5f1..2f36c27b7 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.html
+++ b/client/src/app/+admin/overview/videos/video-list.component.html
@@ -21,7 +21,7 @@
21 </my-action-dropdown> 21 </my-action-dropdown>
22 </div> 22 </div>
23 23
24 <div class="ml-auto right-form"> 24 <div class="ms-auto right-form">
25 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> 25 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
26 26
27 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button> 27 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>
@@ -67,25 +67,25 @@
67 </td> 67 </td>
68 68
69 <td> 69 <td>
70 <span class="badge badge-blue" *ngIf="video.isLocal">Local</span> 70 <span class="pt-badge badge-blue" *ngIf="video.isLocal">Local</span>
71 <span class="badge badge-purple" *ngIf="!video.isLocal">Remote</span> 71 <span class="pt-badge badge-purple" *ngIf="!video.isLocal">Remote</span>
72 72
73 <span [ngClass]="getPrivacyBadgeClass(video)" class="badge">{{ video.privacy.label }}</span> 73 <span [ngClass]="getPrivacyBadgeClass(video)" class="pt-badge">{{ video.privacy.label }}</span>
74 74
75 <span *ngIf="video.nsfw" class="badge badge-red" i18n>NSFW</span> 75 <span *ngIf="video.nsfw" class="pt-badge badge-red" i18n>NSFW</span>
76 76
77 <span *ngIf="isUnpublished(video)" class="badge badge-yellow" i18n>{{ video.state.label }}</span> 77 <span *ngIf="isUnpublished(video)" class="pt-badge badge-yellow" i18n>{{ video.state.label }}</span>
78 78
79 <span *ngIf="isAccountBlocked(video)" class="badge badge-red" i18n>Account muted</span> 79 <span *ngIf="isAccountBlocked(video)" class="pt-badge badge-red" i18n>Account muted</span>
80 <span *ngIf="isServerBlocked(video)" class="badge badge-red" i18n>Server muted</span> 80 <span *ngIf="isServerBlocked(video)" class="pt-badge badge-red" i18n>Server muted</span>
81 81
82 <span *ngIf="isVideoBlocked(video)" class="badge badge-red" i18n>Blocked</span> 82 <span *ngIf="isVideoBlocked(video)" class="pt-badge badge-red" i18n>Blocked</span>
83 </td> 83 </td>
84 84
85 <td> 85 <td>
86 <span *ngIf="isHLS(video)" class="badge badge-blue">HLS</span> 86 <span *ngIf="isHLS(video)" class="pt-badge badge-blue">HLS</span>
87 <span *ngIf="isWebTorrent(video)" class="badge badge-blue">WebTorrent ({{ video.files.length }})</span> 87 <span *ngIf="isWebTorrent(video)" class="pt-badge badge-blue">WebTorrent ({{ video.files.length }})</span>
88 <span *ngIf="video.isLive" class="badge badge-blue">Live</span> 88 <span *ngIf="video.isLive" class="pt-badge badge-blue">Live</span>
89 89
90 <span *ngIf="!isImport(video) && !video.isLive && video.isLocal">{{ getFilesSize(video) | bytes: 1 }}</span> 90 <span *ngIf="!isImport(video) && !video.isLive && video.isLocal">{{ getFilesSize(video) | bytes: 1 }}</span>
91 </td> 91 </td>
@@ -121,7 +121,7 @@
121 </ul> 121 </ul>
122 </div> 122 </div>
123 123
124 <my-embed class="ml-auto" [video]="video"></my-embed> 124 <my-embed class="ms-auto" [video]="video"></my-embed>
125 </div> 125 </div>
126 </td> 126 </td>
127 </tr> 127 </tr>
diff --git a/client/src/app/+admin/overview/videos/video-list.component.scss b/client/src/app/+admin/overview/videos/video-list.component.scss
index cb47b6548..dcd41a1b4 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.scss
+++ b/client/src/app/+admin/overview/videos/video-list.component.scss
@@ -7,10 +7,8 @@ my-embed {
7 width: 50%; 7 width: 50%;
8} 8}
9 9
10.badge { 10.pt-badge {
11 @include peertube-badge; 11 @include margin-right(5px);
12
13 margin-right: 5px;
14} 12}
15 13
16.video-info > div { 14.video-info > div {
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 82ff372aa..67e52d100 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.ts
+++ b/client/src/app/+admin/overview/videos/video-list.component.ts
@@ -3,6 +3,7 @@ import { finalize } from 'rxjs/operators'
3import { Component, OnInit, ViewChild } from '@angular/core' 3import { Component, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' 5import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
6import { prepareIcu } from '@app/helpers'
6import { AdvancedInputFilter } from '@app/shared/shared-forms' 7import { AdvancedInputFilter } from '@app/shared/shared-forms'
7import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
8import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' 9import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation'
@@ -196,14 +197,24 @@ export class VideoListComponent extends RestTable implements OnInit {
196 } 197 }
197 198
198 private async removeVideos (videos: Video[]) { 199 private async removeVideos (videos: Video[]) {
199 const message = $localize`Are you sure you want to delete these ${videos.length} videos?` 200 const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)(
201 { count: videos.length },
202 $localize`Are you sure you want to delete these ${videos.length} videos?`
203 )
204
200 const res = await this.confirmService.confirm(message, $localize`Delete`) 205 const res = await this.confirmService.confirm(message, $localize`Delete`)
201 if (res === false) return 206 if (res === false) return
202 207
203 this.videoService.removeVideo(videos.map(v => v.id)) 208 this.videoService.removeVideo(videos.map(v => v.id))
204 .subscribe({ 209 .subscribe({
205 next: () => { 210 next: () => {
206 this.notifier.success($localize`Deleted ${videos.length} videos.`) 211 this.notifier.success(
212 prepareIcu($localize`Deleted {count, plural, =1 {1 video} other {{count} videos}}.`)(
213 { count: videos.length },
214 $localize`Deleted ${videos.length} videos.`
215 )
216 )
217
207 this.reloadData() 218 this.reloadData()
208 }, 219 },
209 220
@@ -215,7 +226,13 @@ export class VideoListComponent extends RestTable implements OnInit {
215 this.videoBlockService.unblockVideo(videos.map(v => v.id)) 226 this.videoBlockService.unblockVideo(videos.map(v => v.id))
216 .subscribe({ 227 .subscribe({
217 next: () => { 228 next: () => {
218 this.notifier.success($localize`Unblocked ${videos.length} videos.`) 229 this.notifier.success(
230 prepareIcu($localize`Unblocked {count, plural, =1 {1 video} other {{count} videos}}.`)(
231 { count: videos.length },
232 $localize`Unblocked ${videos.length} videos.`
233 )
234 )
235
219 this.reloadData() 236 this.reloadData()
220 }, 237 },
221 238
@@ -224,9 +241,21 @@ export class VideoListComponent extends RestTable implements OnInit {
224 } 241 }
225 242
226 private async removeVideoFiles (videos: Video[], type: 'hls' | 'webtorrent') { 243 private async removeVideoFiles (videos: Video[], type: 'hls' | 'webtorrent') {
227 const message = type === 'hls' 244 let message: string
228 ? $localize`Are you sure you want to delete ${videos.length} HLS streaming playlists?` 245
229 : $localize`Are you sure you want to delete WebTorrent files of ${videos.length} videos?` 246 if (type === 'hls') {
247 // eslint-disable-next-line max-len
248 message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {1 HLS streaming playlist} other {{count} HLS streaming playlists}}?`)(
249 { count: videos.length },
250 $localize`Are you sure you want to delete ${videos.length} HLS streaming playlists?`
251 )
252 } else {
253 // eslint-disable-next-line max-len
254 message = prepareIcu($localize`Are you sure you want to delete WebTorrent files of {count, plural, =1 {1 video} other {{count} videos}}?`)(
255 { count: videos.length },
256 $localize`Are you sure you want to delete WebTorrent files of ${videos.length} videos?`
257 )
258 }
230 259
231 const res = await this.confirmService.confirm(message, $localize`Delete`) 260 const res = await this.confirmService.confirm(message, $localize`Delete`)
232 if (res === false) return 261 if (res === false) return
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
index 33575ef52..23dde86b4 100644
--- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
+++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
@@ -32,9 +32,9 @@
32 <ng-container *ngFor="let plugin of plugins" > 32 <ng-container *ngFor="let plugin of plugins" >
33 <my-plugin-card [plugin]="plugin" [version]="plugin.latestVersion" [pluginType]="pluginType"> 33 <my-plugin-card [plugin]="plugin" [version]="plugin.latestVersion" [pluginType]="pluginType">
34 <div ngProjectAs="badges"> 34 <div ngProjectAs="badges">
35 <span i18n *ngIf="plugin.installed" class="badge badge-success">Installed</span> 35 <span i18n *ngIf="plugin.installed" class="pt-badge badge-success">Installed</span>
36 36
37 <span *ngIf="plugin.official" class="badge badge-primary" i18n i18n-title title="This plugin is developed by Framasoft"> 37 <span *ngIf="plugin.official" class="pt-badge badge-primary" i18n i18n-title title="This plugin is developed by Framasoft">
38 Official 38 Official
39 </span> 39 </span>
40 </div> 40 </div>
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss
index 10401e9df..55baa038f 100644
--- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss
+++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss
@@ -22,11 +22,10 @@
22 } 22 }
23} 23}
24 24
25.badge { 25.pt-badge {
26 @include margin-left(15px); 26 @include margin-left(15px);
27 27
28 font-size: 13px; 28 font-size: 13px;
29 font-weight: $font-semibold;
30} 29}
31 30
32.alert { 31.alert {
diff --git a/client/src/app/+admin/system/jobs/jobs.component.html b/client/src/app/+admin/system/jobs/jobs.component.html
index 301591786..8068fe626 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.html
+++ b/client/src/app/+admin/system/jobs/jobs.component.html
@@ -21,7 +21,7 @@
21 <span i18n="Selector for the list displaying jobs, filtering by their state">any</span> 21 <span i18n="Selector for the list displaying jobs, filtering by their state">any</span>
22 </ng-option> 22 </ng-option>
23 <ng-option *ngFor="let state of jobStates" [value]="state"> 23 <ng-option *ngFor="let state of jobStates" [value]="state">
24 <span class="badge" [ngClass]="getJobStateClass(state)">{{ state }}</span> 24 <span class="pt-badge" [ngClass]="getJobStateClass(state)">{{ state }}</span>
25 </ng-option> 25 </ng-option>
26 </ng-select> 26 </ng-select>
27 </div> 27 </div>
@@ -62,7 +62,7 @@
62 <td class="job-priority c-hand" [pRowToggler]="job">{{ job.priority }}</td> 62 <td class="job-priority c-hand" [pRowToggler]="job">{{ job.priority }}</td>
63 63
64 <td class="job-state c-hand" [pRowToggler]="job" *ngIf="jobState === 'all'"> 64 <td class="job-state c-hand" [pRowToggler]="job" *ngIf="jobState === 'all'">
65 <span class="badge" [ngClass]="getJobStateClass(job.state)">{{ job.state }}</span> 65 <span class="pt-badge" [ngClass]="getJobStateClass(job.state)">{{ job.state }}</span>
66 </td> 66 </td>
67 67
68 <td *ngIf="hasGlobalProgress()" class="job-progress c-hand" [pRowToggler]="job"> 68 <td *ngIf="hasGlobalProgress()" class="job-progress c-hand" [pRowToggler]="job">
@@ -107,8 +107,8 @@
107 </ng-container> 107 </ng-container>
108 108
109 <ng-container *ngIf="jobState !== 'all'"> 109 <ng-container *ngIf="jobState !== 'all'">
110 <ng-container *ngIf="jobType === 'all'" i18n>No <span class="badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span> jobs found.</ng-container> 110 <ng-container *ngIf="jobType === 'all'" i18n>No <span class="pt-badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span> jobs found.</ng-container>
111 <ng-container *ngIf="jobType !== 'all'" i18n>No <code>{{ jobType }}</code> jobs found that are <span class="badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span>.</ng-container> 111 <ng-container *ngIf="jobType !== 'all'" i18n>No <code>{{ jobType }}</code> jobs found that are <span class="pt-badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span>.</ng-container>
112 </ng-container> 112 </ng-container>
113 </div> 113 </div>
114 </div> 114 </div>
diff --git a/client/src/app/+admin/system/jobs/jobs.component.scss b/client/src/app/+admin/system/jobs/jobs.component.scss
index a9e5e8d4b..eadaf7904 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.scss
+++ b/client/src/app/+admin/system/jobs/jobs.component.scss
@@ -55,7 +55,3 @@ pre {
55.job-error { 55.job-error {
56 color: #ff0000; 56 color: #ff0000;
57} 57}
58
59.select-filter-block .badge {
60 @include peertube-badge;
61}
diff --git a/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html
index 3751747a9..61b89082c 100644
--- a/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html
+++ b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html
@@ -3,42 +3,41 @@
3<div class="margin-content"> 3<div class="margin-content">
4 <form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 4 <form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
5 5
6 <div class="form-row"> <!-- channel grid --> 6 <div class="row"> <!-- channel grid -->
7 <div class="form-group col-12 col-lg-4 col-xl-3"> 7 <div class="col-12 col-lg-4 col-xl-3">
8 <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div> 8 <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
9 <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div> 9 <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div>
10 </div> 10 </div>
11 11
12 <div class="form-group col-12 col-lg-8 col-xl-9"> 12 <div class="col-12 col-lg-8 col-xl-9">
13 <h6 i18n>Banner image of the channel</h6> 13 <h6 i18n>Banner image of the channel</h6>
14 14
15 <my-actor-banner-edit 15 <my-actor-banner-edit
16 *ngIf="videoChannel" [previewImage]="isCreation()" 16 *ngIf="videoChannel" [previewImage]="isCreation()"
17 [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()" 17 [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
18 ></my-actor-banner-edit> 18 ></my-actor-banner-edit>
19 19
20 <my-actor-avatar-edit 20 <my-actor-avatar-edit
21 *ngIf="videoChannel" [previewImage]="isCreation()" 21 *ngIf="videoChannel" [previewImage]="isCreation()"
22 [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()" 22 [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
23 [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()" 23 [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
24 ></my-actor-avatar-edit> 24 ></my-actor-avatar-edit>
25 25
26 <div class="form-group" *ngIf="isCreation()"> 26 <div class="form-group" *ngIf="isCreation()">
27 <label i18n for="name">Name</label> 27 <label i18n for="name">Name</label>
28
28 <div class="input-group"> 29 <div class="input-group">
29 <input 30 <input
30 type="text" id="name" i18n-placeholder placeholder="Example: my_channel" 31 type="text" id="name" i18n-placeholder placeholder="Example: my_channel"
31 formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control" 32 formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control"
32 > 33 >
33 <div class="input-group-append"> 34 <div class="input-group-text">@{{ instanceHost }}</div>
34 <span class="input-group-text">@{{ instanceHost }}</span>
35 </div>
36 </div> 35 </div>
37 <div *ngIf="formErrors['name']" class="form-error"> 36 <div *ngIf="formErrors['name']" class="form-error">
38 {{ formErrors['name'] }} 37 {{ formErrors['name'] }}
39 </div> 38 </div>
40 </div> 39 </div>
41 40
42 <div class="form-group"> 41 <div class="form-group">
43 <label i18n for="display-name">Display name</label> 42 <label i18n for="display-name">Display name</label>
44 <input 43 <input
@@ -49,7 +48,7 @@
49 {{ formErrors['display-name'] }} 48 {{ formErrors['display-name'] }}
50 </div> 49 </div>
51 </div> 50 </div>
52 51
53 <div class="form-group"> 52 <div class="form-group">
54 <label i18n for="description">Description</label> 53 <label i18n for="description">Description</label>
55 <textarea 54 <textarea
@@ -60,7 +59,7 @@
60 {{ formErrors.description }} 59 {{ formErrors.description }}
61 </div> 60 </div>
62 </div> 61 </div>
63 62
64 <div class="form-group"> 63 <div class="form-group">
65 <label for="support">Support</label> 64 <label for="support">Support</label>
66 <my-help 65 <my-help
@@ -75,22 +74,22 @@
75 {{ formErrors.support }} 74 {{ formErrors.support }}
76 </div> 75 </div>
77 </div> 76 </div>
78 77
79 <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()"> 78 <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()">
80 <my-peertube-checkbox 79 <my-peertube-checkbox
81 inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate" 80 inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate"
82 i18n-labelText labelText="Overwrite support field of all videos of this channel" 81 i18n-labelText labelText="Overwrite support field of all videos of this channel"
83 ></my-peertube-checkbox> 82 ></my-peertube-checkbox>
84 </div> 83 </div>
85 84
86 </div> 85 </div>
87 </div> 86 </div>
88 87
89 <div class="form-row"> <!-- submit placement block --> 88 <div class="row"> <!-- submit placement block -->
90 <div class="col-md-7 col-xl-5"></div> 89 <div class="col-md-7 col-xl-5"></div>
91 <div class="col-md-5 col-xl-5 d-inline-flex"> 90 <div class="col-md-5 col-xl-5 d-inline-flex">
92 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 91 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
93 </div> 92 </div>
94 </div> 93 </div>
95 </form> 94 </form>
96</div> \ No newline at end of file 95</div>
diff --git a/client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss
index d010d6277..2c720314a 100644
--- a/client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss
+++ b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss
@@ -24,31 +24,21 @@ my-actor-banner-edit {
24 max-width: 500px; 24 max-width: 500px;
25} 25}
26 26
27.input-group { 27input[type=text] {
28 @include peertube-input-group(fit-content); 28 @include peertube-input-text(340px);
29}
30
31.input-group-append {
32 height: 30px;
33}
34
35input {
36 &[type=text] {
37 @include peertube-input-text(340px);
38 29
39 display: block; 30 display: block;
40 31
41 &#name { 32 &#name {
42 width: auto; 33 width: auto;
43 flex-grow: 1; 34 flex-grow: 1;
44 }
45 } 35 }
36}
46 37
47 &[type=submit] { 38input[type=submit] {
48 @include peertube-button; 39 @include peertube-button;
49 @include orange-button; 40 @include orange-button;
50 @include margin-left(auto); 41 @include margin-left(auto);
51 }
52} 42}
53 43
54textarea { 44textarea {
diff --git a/client/src/app/+my-account/my-account-applications/my-account-applications.component.html b/client/src/app/+my-account/my-account-applications/my-account-applications.component.html
index 68d094a4f..8f3a55be5 100644
--- a/client/src/app/+my-account/my-account-applications/my-account-applications.component.html
+++ b/client/src/app/+my-account/my-account-applications/my-account-applications.component.html
@@ -3,8 +3,9 @@
3 <ng-container i18n>Applications</ng-container> 3 <ng-container i18n>Applications</ng-container>
4</h1> 4</h1>
5 5
6<div class="form-row"> <!-- built-in token grid --> 6<div class="row"> <!-- built-in token grid -->
7 <div class="form-group col-12 col-lg-4 col-xl-3"> 7
8 <div class="group col-12 col-lg-4 col-xl-3">
8 <h2 i18n class="applications-title">SUBSCRIPTION FEED</h2> 9 <h2 i18n class="applications-title">SUBSCRIPTION FEED</h2>
9 <div i18n class="applications-description"> 10 <div i18n class="applications-description">
10 Use third-party feed aggregators to retrieve the list of videos from 11 Use third-party feed aggregators to retrieve the list of videos from
@@ -12,7 +13,7 @@
12 </div> 13 </div>
13 </div> 14 </div>
14 15
15 <div class="form-group col-12 col-lg-8 col-xl-9"> 16 <div class="col-12 col-lg-8 col-xl-9">
16 17
17 <div class="form-group"> 18 <div class="form-group">
18 <label i18n for="feed-url">Feed URL</label> 19 <label i18n for="feed-url">Feed URL</label>
@@ -29,7 +30,7 @@
29 </div> 30 </div>
30</div> 31</div>
31 32
32<div class="form-row mt-4"> <!-- submit placement block --> 33<div class="row mt-4"> <!-- submit placement block -->
33 <div class="col-md-7 col-xl-5"></div> 34 <div class="col-md-7 col-xl-5"></div>
34 <div class="col-md-5 col-xl-5"> 35 <div class="col-md-5 col-xl-5">
35 <input (click)="renewToken()" type="submit" i18n-value value="Renew token"> 36 <input (click)="renewToken()" type="submit" i18n-value value="Renew token">
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html
index f0e9f4010..b98cd1156 100644
--- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html
+++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html
@@ -1,11 +1,11 @@
1<h1 class="sr-only" i18n>Notifications</h1> 1<h1 class="visually-hidden" i18n>Notifications</h1>
2<div class="header"> 2<div class="header">
3 <a routerLink="/my-account/settings" fragment="notifications" i18n> 3 <a routerLink="/my-account/settings" fragment="notifications" i18n>
4 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> 4 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
5 Notification preferences 5 Notification preferences
6 </a> 6 </a>
7 7
8 <div class="peertube-select-container peertube-select-button ml-2 mr-2"> 8 <div class="peertube-select-container peertube-select-button ms-2 me-2">
9 <select [(ngModel)]="notificationSortType" (ngModelChange)="onChangeSortColumn()" class="form-control"> 9 <select [(ngModel)]="notificationSortType" (ngModelChange)="onChangeSortColumn()" class="form-control">
10 <option value="undefined" disabled>Sort by</option> 10 <option value="undefined" disabled>Sort by</option>
11 <option value="createdAt" i18n>Newest first</option> 11 <option value="createdAt" i18n>Newest first</option>
@@ -13,7 +13,7 @@
13 </select> 13 </select>
14 </div> 14 </div>
15 15
16 <button class="btn ml-auto" [disabled]="!hasUnreadNotifications()" (click)="markAllAsRead()"> 16 <button class="btn ms-auto" [disabled]="!hasUnreadNotifications()" (click)="markAllAsRead()">
17 <ng-container *ngIf="hasUnreadNotifications()"> 17 <ng-container *ngIf="hasUnreadNotifications()">
18 <my-global-icon iconName="tick" aria-hidden="true"></my-global-icon> 18 <my-global-icon iconName="tick" aria-hidden="true"></my-global-icon>
19 19
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
index 7c13282fa..769ab647a 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
@@ -37,7 +37,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
37 myVideoPublished: $localize`Video published (after transcoding/scheduled update)`, 37 myVideoPublished: $localize`Video published (after transcoding/scheduled update)`,
38 myVideoImportFinished: $localize`Video import finished`, 38 myVideoImportFinished: $localize`Video import finished`,
39 newUserRegistration: $localize`A new user registered on your instance`, 39 newUserRegistration: $localize`A new user registered on your instance`,
40 newFollow: $localize`You or your channel(s) has a new follower`, 40 newFollow: $localize`You or one of your channels has a new follower`,
41 commentMention: $localize`Someone mentioned you in video comments`, 41 commentMention: $localize`Someone mentioned you in video comments`,
42 newInstanceFollower: $localize`Your instance has a new follower`, 42 newInstanceFollower: $localize`Your instance has a new follower`,
43 autoInstanceFollowing: $localize`Your instance automatically followed another instance`, 43 autoInstanceFollowing: $localize`Your instance automatically followed another instance`,
diff --git a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.scss b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.scss
index cbac81d01..78f7ef1d5 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.scss
+++ b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.scss
@@ -6,10 +6,6 @@ label {
6 font-size: 100%; 6 font-size: 100%;
7} 7}
8 8
9.form-group:first-child {
10 margin-bottom: 15px;
11}
12
13input#username + .muted { 9input#username + .muted {
14 margin-top: 5px; 10 margin-top: 5px;
15} 11}
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
index 8ca197fd4..abcfe54a6 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
@@ -1,18 +1,19 @@
1<h1 class="sr-only" i18n>Settings</h1> 1<h1 class="visually-hidden" i18n>Settings</h1>
2<div class="form-row"> <!-- preview -->
3 <div class="form-group col-12 col-lg-4 col-xl-3"></div>
4 2
5 <div class="form-group col-12 col-lg-8 col-xl-9"> 3<div class="row"> <!-- preview -->
4 <div class="col-12 col-lg-4 col-xl-3"></div>
5
6 <div class="col-12 col-lg-8 col-xl-9">
6 <my-actor-avatar-edit [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-edit> 7 <my-actor-avatar-edit [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-edit>
7 </div> 8 </div>
8</div> 9</div>
9 10
10<div class="form-row"> <!-- profile settings grid --> 11<div class="row mt-3"> <!-- profile settings grid -->
11 <div class="form-group col-12 col-lg-4 col-xl-3"> 12 <div class="col-12 col-lg-4 col-xl-3">
12 <h2 i18n class="account-title">PROFILE SETTINGS</h2> 13 <h2 i18n class="account-title">PROFILE SETTINGS</h2>
13 </div> 14 </div>
14 15
15 <div class="form-group col-12 col-lg-8 col-xl-9"> 16 <div class="col-12 col-lg-8 col-xl-9">
16 17
17 <my-user-quota [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-quota> 18 <my-user-quota [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-quota>
18 19
@@ -20,64 +21,64 @@
20 </div> 21 </div>
21</div> 22</div>
22 23
23<div class="form-row mt-5"> <!-- interface grid --> 24<div class="row mt-5"> <!-- interface grid -->
24 <div class="form-group col-12 col-lg-4 col-xl-3"> 25 <div class="col-12 col-lg-4 col-xl-3">
25 <h2 i18n class="account-title">INTERFACE</h2> 26 <h2 i18n class="account-title">INTERFACE</h2>
26 </div> 27 </div>
27 28
28 <div class="form-group col-12 col-lg-8 col-xl-9"> 29 <div class="col-12 col-lg-8 col-xl-9">
29 <my-user-interface-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-interface-settings> 30 <my-user-interface-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-interface-settings>
30 </div> 31 </div>
31</div> 32</div>
32 33
33<div class="form-row mt-5"> <!-- video settings grid --> 34<div class="row mt-5"> <!-- video settings grid -->
34 <div class="form-group col-12 col-lg-4 col-xl-3"> 35 <div class="col-12 col-lg-4 col-xl-3">
35 <div class="anchor" id="video-settings"></div> <!-- video settings anchor --> 36 <div class="anchor" id="video-settings"></div> <!-- video settings anchor -->
36 <h2 i18n class="account-title">VIDEO SETTINGS</h2> 37 <h2 i18n class="account-title">VIDEO SETTINGS</h2>
37 </div> 38 </div>
38 39
39 <div class="form-group col-12 col-lg-8 col-xl-9"> 40 <div class="col-12 col-lg-8 col-xl-9">
40 <my-user-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-video-settings> 41 <my-user-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-video-settings>
41 </div> 42 </div>
42</div> 43</div>
43 44
44<div class="form-row mt-5"> <!-- notifications grid --> 45<div class="row mt-5"> <!-- notifications grid -->
45 <div class="form-group col-12 col-lg-4 col-xl-3"> 46 <div class="col-12 col-lg-4 col-xl-3">
46 <div class="anchor" id="notifications"></div> <!-- notifications anchor --> 47 <div class="anchor" id="notifications"></div> <!-- notifications anchor -->
47 <h2 i18n class="account-title">NOTIFICATIONS</h2> 48 <h2 i18n class="account-title">NOTIFICATIONS</h2>
48 </div> 49 </div>
49 50
50 <div class="form-group col-12 col-lg-8 col-xl-9"> 51 <div class="col-12 col-lg-8 col-xl-9">
51 <my-account-notification-preferences [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-notification-preferences> 52 <my-account-notification-preferences [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-notification-preferences>
52 </div> 53 </div>
53</div> 54</div>
54 55
55<div class="form-row mt-5" *ngIf="user.pluginAuth === null"> <!-- password grid --> 56<div class="row mt-5" *ngIf="user.pluginAuth === null"> <!-- password grid -->
56 <div class="form-group col-12 col-lg-4 col-xl-3"> 57 <div class="col-12 col-lg-4 col-xl-3">
57 <h2 i18n class="account-title">PASSWORD</h2> 58 <h2 i18n class="account-title">PASSWORD</h2>
58 </div> 59 </div>
59 60
60 <div class="form-group col-12 col-lg-8 col-xl-9"> 61 <div class="col-12 col-lg-8 col-xl-9">
61 <my-account-change-password></my-account-change-password> 62 <my-account-change-password></my-account-change-password>
62 </div> 63 </div>
63</div> 64</div>
64 65
65<div class="form-row mt-5"> <!-- email grid --> 66<div class="row mt-5"> <!-- email grid -->
66 <div class="form-group col-12 col-lg-4 col-xl-3"> 67 <div class="col-12 col-lg-4 col-xl-3">
67 <h2 i18n class="account-title">EMAIL</h2> 68 <h2 i18n class="account-title">EMAIL</h2>
68 </div> 69 </div>
69 70
70 <div class="form-group col-12 col-lg-8 col-xl-9"> 71 <div class="col-12 col-lg-8 col-xl-9">
71 <my-account-change-email></my-account-change-email> 72 <my-account-change-email></my-account-change-email>
72 </div> 73 </div>
73</div> 74</div>
74 75
75<div class="form-row mt-5"> <!-- danger zone grid --> 76<div class="row mt-5"> <!-- danger zone grid -->
76 <div class="form-group col-12 col-lg-4 col-xl-3"> 77 <div class="col-12 col-lg-4 col-xl-3">
77 <h2 i18n class="account-title account-title-danger">DANGER ZONE</h2> 78 <h2 i18n class="account-title account-title-danger">DANGER ZONE</h2>
78 </div> 79 </div>
79 80
80 <div class="form-group col-12 col-lg-8 col-xl-9"> 81 <div class="col-12 col-lg-8 col-xl-9">
81 <my-account-danger-zone [user]="user"></my-account-danger-zone> 82 <my-account-danger-zone [user]="user"></my-account-danger-zone>
82 </div> 83 </div>
83</div> 84</div>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss b/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss
index 1c216d79d..8206f4dd8 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss
+++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss
@@ -1,5 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3@use '~bootstrap/scss/functions' as *;
3 4
4.account-title { 5.account-title {
5 @include settings-big-title; 6 @include settings-big-title;
@@ -9,6 +10,6 @@
9 } 10 }
10} 11}
11 12
12.form-group { 13.row > div {
13 max-width: 500px; 14 max-width: 500px;
14} 15}
diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html
index b465d0156..1c44c8472 100644
--- a/client/src/app/+my-account/my-account.component.html
+++ b/client/src/app/+my-account/my-account.component.html
@@ -1,4 +1,4 @@
1<div class="row"> 1<div class="root">
2 <my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown> 2 <my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
3 3
4 <div class="margin-content pb-5" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }"> 4 <div class="margin-content pb-5" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }">
diff --git a/client/src/app/+my-account/my-account.component.scss b/client/src/app/+my-account/my-account.component.scss
index 1ec25315a..6275b7ac2 100644
--- a/client/src/app/+my-account/my-account.component.scss
+++ b/client/src/app/+my-account/my-account.component.scss
@@ -1,7 +1,7 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.row { 4.root {
5 @include sub-menu-h1; 5 @include sub-menu-h1;
6 6
7 flex-direction: column; 7 flex-direction: column;
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
index 89327b065..aa51764be 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
@@ -1,7 +1,7 @@
1<h1> 1<h1>
2 <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon> 2 <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>My channels</ng-container> 3 <ng-container i18n>My channels</ng-container>
4 <span class="badge badge-secondary">{{ totalItems }}</span> 4 <span *ngIf="totalItems" class="pt-badge badge-secondary">{{ totalItems }}</span>
5</h1> 5</h1>
6 6
7<my-channels-setup-message [hideLink]="true"></my-channels-setup-message> 7<my-channels-setup-message [hideLink]="true"></my-channels-setup-message>
@@ -31,11 +31,13 @@
31 i18n class="video-channel-followers" 31 i18n class="video-channel-followers"
32 [routerLink]="[ '/my-library', 'followers' ]" [queryParams]="{ search: 'channel:' + videoChannel.name }" 32 [routerLink]="[ '/my-library', 'followers' ]" [queryParams]="{ search: 'channel:' + videoChannel.name }"
33 > 33 >
34 {videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}} 34 {videoChannel.followersCount, plural, =0 {No subscribers} =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
35 </a> 35 </a>
36 36
37 <div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div> 37 <div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div>
38 38
39 <div i18n class="video-channel-views">{videoChannel.totalViews, plural, =0 {No views} =1 {1 view} other {{{ videoChannel.totalViews }} views}}</div>
40
39 <div class="video-channel-buttons"> 41 <div class="video-channel-buttons">
40 <my-edit-button label [routerLink]="[ '/manage/update', videoChannel.nameWithHost ]"></my-edit-button> 42 <my-edit-button label [routerLink]="[ '/manage/update', videoChannel.nameWithHost ]"></my-edit-button>
41 <my-delete-button label (click)="deleteVideoChannel(videoChannel)"></my-delete-button> 43 <my-delete-button label (click)="deleteVideoChannel(videoChannel)"></my-delete-button>
diff --git a/client/src/app/+my-library/my-follows/my-followers.component.html b/client/src/app/+my-library/my-follows/my-followers.component.html
index a8a3da863..2827f8c41 100644
--- a/client/src/app/+my-library/my-follows/my-followers.component.html
+++ b/client/src/app/+my-library/my-follows/my-followers.component.html
@@ -2,7 +2,7 @@
2 <span> 2 <span>
3 <my-global-icon iconName="follower" aria-hidden="true"></my-global-icon> 3 <my-global-icon iconName="follower" aria-hidden="true"></my-global-icon>
4 <ng-container i18n>My followers</ng-container> 4 <ng-container i18n>My followers</ng-container>
5 <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> 5 <span *ngIf="pagination.totalItems" class="pt-badge badge-secondary"> {{ pagination.totalItems }}</span>
6 </span> 6 </span>
7</h1> 7</h1>
8 8
diff --git a/client/src/app/+my-library/my-follows/my-subscriptions.component.html b/client/src/app/+my-library/my-follows/my-subscriptions.component.html
index 391c4d3be..11d460b19 100644
--- a/client/src/app/+my-library/my-follows/my-subscriptions.component.html
+++ b/client/src/app/+my-library/my-follows/my-subscriptions.component.html
@@ -2,7 +2,7 @@
2 <span> 2 <span>
3 <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon> 3 <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon>
4 <ng-container i18n>My subscriptions</ng-container> 4 <ng-container i18n>My subscriptions</ng-container>
5 <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> 5 <span *ngIf="pagination.totalItems" class="pt-badge badge-secondary"> {{ pagination.totalItems }}</span>
6 </span> 6 </span>
7</h1> 7</h1>
8 8
diff --git a/client/src/app/+my-library/my-history/my-history.component.html b/client/src/app/+my-library/my-history/my-history.component.html
index 14bf01804..76367c029 100644
--- a/client/src/app/+my-library/my-history/my-history.component.html
+++ b/client/src/app/+my-library/my-history/my-history.component.html
@@ -1,6 +1,7 @@
1<h1> 1<h1>
2 <my-global-icon iconName="history" aria-hidden="true"></my-global-icon> 2 <my-global-icon iconName="history" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>My watch history</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> 3 <ng-container i18n>My watch history</ng-container>
4 <span *ngIf="pagination.totalItems" class="pt-badge badge-secondary">{{ pagination.totalItems }}</span>
4</h1> 5</h1>
5 6
6<div class="top-buttons"> 7<div class="top-buttons">
diff --git a/client/src/app/+my-library/my-history/my-history.component.ts b/client/src/app/+my-library/my-history/my-history.component.ts
index f6b712908..766869637 100644
--- a/client/src/app/+my-library/my-history/my-history.component.ts
+++ b/client/src/app/+my-library/my-history/my-history.component.ts
@@ -93,8 +93,8 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook {
93 .subscribe({ 93 .subscribe({
94 next: () => { 94 next: () => {
95 const message = this.videosHistoryEnabled === true 95 const message = this.videosHistoryEnabled === true
96 ? $localize`Videos history is enabled` 96 ? $localize`Video history is enabled`
97 : $localize`Videos history is disabled` 97 : $localize`Video history is disabled`
98 98
99 this.notifier.success(message) 99 this.notifier.success(message)
100 100
@@ -117,8 +117,8 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook {
117 } 117 }
118 118
119 async clearAllHistory () { 119 async clearAllHistory () {
120 const title = $localize`Delete videos history` 120 const title = $localize`Delete video history`
121 const message = $localize`Are you sure you want to delete all your videos history?` 121 const message = $localize`Are you sure you want to delete all your video history?`
122 122
123 const res = await this.confirmService.confirm(message, title) 123 const res = await this.confirmService.confirm(message, title)
124 if (res !== true) return 124 if (res !== true) return
@@ -126,7 +126,7 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook {
126 this.userHistoryService.clearAll() 126 this.userHistoryService.clearAll()
127 .subscribe({ 127 .subscribe({
128 next: () => { 128 next: () => {
129 this.notifier.success($localize`Videos history deleted`) 129 this.notifier.success($localize`Video history deleted`)
130 130
131 this.reloadData() 131 this.reloadData()
132 }, 132 },
diff --git a/client/src/app/+my-library/my-library.component.html b/client/src/app/+my-library/my-library.component.html
index b465d0156..1c44c8472 100644
--- a/client/src/app/+my-library/my-library.component.html
+++ b/client/src/app/+my-library/my-library.component.html
@@ -1,4 +1,4 @@
1<div class="row"> 1<div class="root">
2 <my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown> 2 <my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
3 3
4 <div class="margin-content pb-5" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }"> 4 <div class="margin-content pb-5" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }">
diff --git a/client/src/app/+my-library/my-library.component.scss b/client/src/app/+my-library/my-library.component.scss
index 1ec25315a..6275b7ac2 100644
--- a/client/src/app/+my-library/my-library.component.scss
+++ b/client/src/app/+my-library/my-library.component.scss
@@ -1,7 +1,7 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.row { 4.root {
5 @include sub-menu-h1; 5 @include sub-menu-h1;
6 6
7 flex-direction: column; 7 flex-direction: column;
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.html b/client/src/app/+my-library/my-ownership/my-ownership.component.html
index c29c71c0a..01470e46f 100644
--- a/client/src/app/+my-library/my-ownership/my-ownership.component.html
+++ b/client/src/app/+my-library/my-ownership/my-ownership.component.html
@@ -65,7 +65,7 @@
65 <td>{{ videoChangeOwnership.createdAt | date: 'short' }}</td> 65 <td>{{ videoChangeOwnership.createdAt | date: 'short' }}</td>
66 66
67 <td> 67 <td>
68 <span class="badge" 68 <span class="pt-badge"
69 [ngClass]="getStatusClass(videoChangeOwnership.status)">{{ videoChangeOwnership.status }}</span> 69 [ngClass]="getStatusClass(videoChangeOwnership.status)">{{ videoChangeOwnership.status }}</span>
70 </td> 70 </td>
71 </tr> 71 </tr>
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.html b/client/src/app/+my-library/my-video-imports/my-video-imports.component.html
index 1525d0bd1..79fb4da26 100644
--- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.html
+++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.html
@@ -50,7 +50,7 @@
50 </td> 50 </td>
51 51
52 <td> 52 <td>
53 <span class="badge" [ngClass]="getVideoImportStateClass(videoImport.state.id)"> 53 <span class="pt-badge" [ngClass]="getVideoImportStateClass(videoImport.state.id)">
54 {{ videoImport.state.label }} 54 {{ videoImport.state.label }}
55 </span> 55 </span>
56 </td> 56 </td>
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html
index c39e90a1e..35682cf81 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html
@@ -20,13 +20,13 @@
20 20
21<form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 21<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
22 22
23 <div class="form-row"> <!-- playlist grid --> 23 <div class="row"> <!-- playlist grid -->
24 <div class="form-group col-12 col-lg-4 col-xl-3"> 24 <div class="col-12 col-lg-4 col-xl-3">
25 <div *ngIf="isCreation()" class="video-playlist-title" i18n>NEW PLAYLIST</div> 25 <div *ngIf="isCreation()" class="video-playlist-title" i18n>NEW PLAYLIST</div>
26 <div *ngIf="!isCreation() && videoPlaylistToUpdate" class="video-playlist-title" i18n>PLAYLIST</div> 26 <div *ngIf="!isCreation() && videoPlaylistToUpdate" class="video-playlist-title" i18n>PLAYLIST</div>
27 </div> 27 </div>
28 28
29 <div class="form-group col-12 col-lg-8 col-xl-9"> 29 <div class="col-12 col-lg-8 col-xl-9">
30 30
31 <div class="col-md-12 col-xl-6"> 31 <div class="col-md-12 col-xl-6">
32 <div class="form-group"> 32 <div class="form-group">
@@ -88,7 +88,7 @@
88 </div> 88 </div>
89 </div> 89 </div>
90 90
91 <div class="form-row"> <!-- submit placement block --> 91 <div class="row"> <!-- submit placement block -->
92 <div class="col-md-7 col-xl-5"></div> 92 <div class="col-md-7 col-xl-5"></div>
93 <div class="col-md-5 col-xl-5 d-inline-flex"> 93 <div class="col-md-5 col-xl-5 d-inline-flex">
94 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 94 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
index 25b742bff..0091f70be 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
@@ -1,6 +1,7 @@
1<h1> 1<h1>
2 <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> 2 <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> 3 <ng-container i18n>My playlists</ng-container>
4 <span *ngIf="pagination.totalItems" class="pt-badge badge-secondary">{{ pagination.totalItems }}</span>
4</h1> 5</h1>
5 6
6<my-channels-setup-message></my-channels-setup-message> 7<my-channels-setup-message></my-channels-setup-message>
diff --git a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html
index 955fd4884..56ff0e788 100644
--- a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html
+++ b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html
@@ -17,7 +17,7 @@
17 </div> 17 </div>
18 18
19 <div class="modal-footer"> 19 <div class="modal-footer">
20 <div class="form-group inputs"> 20 <div class="inputs">
21 <input 21 <input
22 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button" 22 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
23 (click)="dismiss()" (key.enter)="dismiss()" 23 (click)="dismiss()" (key.enter)="dismiss()"
diff --git a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss
index 0eb694162..48c073192 100644
--- a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss
+++ b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss
@@ -4,7 +4,3 @@
4p-autocomplete { 4p-autocomplete {
5 display: block; 5 display: block;
6} 6}
7
8.form-group {
9 margin: 20px 0;
10}
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.html b/client/src/app/+my-library/my-videos/my-videos.component.html
index 7f12e2c71..146dcf41e 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.html
+++ b/client/src/app/+my-library/my-videos/my-videos.component.html
@@ -2,7 +2,7 @@
2 <span> 2 <span>
3 <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon> 3 <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon>
4 <ng-container i18n>My videos</ng-container> 4 <ng-container i18n>My videos</ng-container>
5 <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> 5 <span *ngIf="pagination.totalItems" class="pt-badge badge-secondary"> {{ pagination.totalItems }}</span>
6 </span> 6 </span>
7 7
8 <div> 8 <div>
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 91cc06702..2f1eb84ba 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
@@ -4,7 +4,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' 5import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core'
6import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' 6import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
7import { immutableAssign } from '@app/helpers' 7import { prepareIcu, immutableAssign } from '@app/helpers'
8import { AdvancedInputFilter } from '@app/shared/shared-forms' 8import { AdvancedInputFilter } from '@app/shared/shared-forms'
9import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 9import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
10import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' 10import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
@@ -167,7 +167,10 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
167 .map(k => parseInt(k, 10)) 167 .map(k => parseInt(k, 10))
168 168
169 const res = await this.confirmService.confirm( 169 const res = await this.confirmService.confirm(
170 $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 170 prepareIcu($localize`Do you really want to delete {length, plural, =1 {this video} other {{length} videos}}?`)(
171 { length: toDeleteVideosIds.length },
172 $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?`
173 ),
171 $localize`Delete` 174 $localize`Delete`
172 ) 175 )
173 if (res === false) return 176 if (res === false) return
@@ -184,7 +187,13 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
184 .pipe(toArray()) 187 .pipe(toArray())
185 .subscribe({ 188 .subscribe({
186 next: () => { 189 next: () => {
187 this.notifier.success($localize`${toDeleteVideosIds.length} videos deleted.`) 190 this.notifier.success(
191 prepareIcu($localize`{length, plural, =1 {Video has been deleted} other {{length} videos have been deleted}}`)(
192 { length: toDeleteVideosIds.length },
193 $localize`${toDeleteVideosIds.length} have been deleted.`
194 )
195 )
196
188 this.selection = {} 197 this.selection = {}
189 }, 198 },
190 199
diff --git a/client/src/app/+page-not-found/page-not-found.component.html b/client/src/app/+page-not-found/page-not-found.component.html
index 0333f9550..70ede26e8 100644
--- a/client/src/app/+page-not-found/page-not-found.component.html
+++ b/client/src/app/+page-not-found/page-not-found.component.html
@@ -1,7 +1,7 @@
1<div class="root"> 1<div class="root">
2 <div *ngIf="status !== 403 && status !== 418" class="box"> 2 <div *ngIf="status !== 403 && status !== 418" class="box">
3 <strong>{{ status }}.</strong> 3 <strong>{{ status }}.</strong>
4 <span class="ml-1 muted" i18n>That's an error.</span> 4 <span class="ms-1 muted" i18n>That's an error.</span>
5 5
6 <div class="text mt-4"> 6 <div class="text mt-4">
7 <ng-container *ngIf="type === 'video'" i18n>We couldn't find any video tied to the URL {{ pathname }} you were looking for.</ng-container> 7 <ng-container *ngIf="type === 'video'" i18n>We couldn't find any video tied to the URL {{ pathname }} you were looking for.</ng-container>
@@ -24,7 +24,7 @@
24 24
25 <div *ngIf="status === 403" class="box"> 25 <div *ngIf="status === 403" class="box">
26 <strong>{{ status }}.</strong> 26 <strong>{{ status }}.</strong>
27 <span class="ml-1 muted" i18n>You are not authorized here.</span> 27 <span class="ms-1 muted" i18n>You are not authorized here.</span>
28 28
29 <div class="text mt-4"> 29 <div class="text mt-4">
30 <ng-container *ngIf="type === 'video'" i18n>You might need to check your account is allowed by the video or instance owner.</ng-container> 30 <ng-container *ngIf="type === 'video'" i18n>You might need to check your account is allowed by the video or instance owner.</ng-container>
@@ -34,7 +34,7 @@
34 34
35 <div *ngIf="status === 418" class="box"> 35 <div *ngIf="status === 418" class="box">
36 <strong>{{ status }}.</strong> 36 <strong>{{ status }}.</strong>
37 <span class="ml-1 muted">I'm a teapot.</span> 37 <span class="ms-1 muted">I'm a teapot.</span>
38 38
39 <div class="text mt-4" i18n="Description of a tea flavour, keeping the 'requested entity body' as a technical expression referring to a web request"> 39 <div class="text mt-4" i18n="Description of a tea flavour, keeping the 'requested entity body' as a technical expression referring to a web request">
40 The requested entity body blends sweet bits with a mellow earthiness. 40 The requested entity body blends sweet bits with a mellow earthiness.
diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html
index c4861e8c4..5bce009d5 100644
--- a/client/src/app/+search/search-filters.component.html
+++ b/client/src/app/+search/search-filters.component.html
@@ -77,7 +77,7 @@
77 </div> 77 </div>
78 78
79 <div class="row"> 79 <div class="row">
80 <div class="pl-0 col-sm-6"> 80 <div class="ps-0 col-sm-6">
81 <input 81 <input
82 (change)="onDurationOrPublishedUpdated()" 82 (change)="onDurationOrPublishedUpdated()"
83 (keydown.enter)="$event.preventDefault()" 83 (keydown.enter)="$event.preventDefault()"
@@ -87,7 +87,7 @@
87 class="form-control" 87 class="form-control"
88 > 88 >
89 </div> 89 </div>
90 <div class="pr-0 col-sm-6"> 90 <div class="pe-0 col-sm-6">
91 <input 91 <input
92 (change)="onDurationOrPublishedUpdated()" 92 (change)="onDurationOrPublishedUpdated()"
93 (keydown.enter)="$event.preventDefault()" 93 (keydown.enter)="$event.preventDefault()"
diff --git a/client/src/app/+search/search-filters.component.scss b/client/src/app/+search/search-filters.component.scss
index ece4ba5b5..5fd1764d3 100644
--- a/client/src/app/+search/search-filters.component.scss
+++ b/client/src/app/+search/search-filters.component.scss
@@ -22,10 +22,6 @@ form {
22 margin-bottom: 1rem; 22 margin-bottom: 1rem;
23} 23}
24 24
25.form-group {
26 margin-bottom: 25px;
27}
28
29input[type=text] { 25input[type=text] {
30 @include peertube-input-text(100%); 26 @include peertube-input-text(100%);
31 display: block; 27 display: block;
diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html
index 2c84dd930..247dfb56a 100644
--- a/client/src/app/+search/search.component.html
+++ b/client/src/app/+search/search.component.html
@@ -2,22 +2,22 @@
2 <div class="results-header"> 2 <div class="results-header">
3 <div class="first-line"> 3 <div class="first-line">
4 <div class="results-counter" *ngIf="pagination.totalItems"> 4 <div class="results-counter" *ngIf="pagination.totalItems">
5 <span class="mr-1" i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}}</span> 5 <span class="me-1" i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}}</span>
6 6
7 <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span> 7 <span class="me-1" i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span>
8 <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span> 8 <span class="me-1" i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span>
9 9
10 <span *ngIf="currentSearch" i18n>for <span class="search-value">{{ currentSearch }}</span></span> 10 <span *ngIf="currentSearch" i18n>for <span class="search-value">{{ currentSearch }}</span></span>
11 </div> 11 </div>
12 12
13 <div 13 <div
14 class="results-filter-button ml-auto" (click)="isSearchFilterCollapsed = !isSearchFilterCollapsed" role="button" 14 class="results-filter-button ms-auto" (click)="isSearchFilterCollapsed = !isSearchFilterCollapsed" role="button"
15 [attr.aria-expanded]="!isSearchFilterCollapsed" aria-controls="collapseBasic" 15 [attr.aria-expanded]="!isSearchFilterCollapsed" aria-controls="collapseBasic"
16 > 16 >
17 <span class="icon icon-filter"></span> 17 <span class="icon icon-filter"></span>
18 <ng-container i18n> 18 <ng-container i18n>
19 Filters 19 Filters
20 <span *ngIf="numberOfFilters() > 0" class="badge badge-secondary">{{ numberOfFilters() }}</span> 20 <span *ngIf="numberOfFilters() > 0" class="pt-badge badge-secondary">{{ numberOfFilters() }}</span>
21 </ng-container> 21 </ng-container>
22 </div> 22 </div>
23 </div> 23 </div>
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts
index b9ec6dbcc..62b1c4446 100644
--- a/client/src/app/+search/search.component.ts
+++ b/client/src/app/+search/search.component.ts
@@ -248,11 +248,11 @@ export class SearchComponent implements OnInit, OnDestroy {
248 } 248 }
249 249
250 private updateTitle () { 250 private updateTitle () {
251 const suffix = this.currentSearch 251 const title = this.currentSearch
252 ? ' ' + this.currentSearch 252 ? $localize`Search ${this.currentSearch}`
253 : '' 253 : $localize`Search`
254 254
255 this.metaService.setTitle($localize`Search` + suffix) 255 this.metaService.setTitle(title)
256 } 256 }
257 257
258 private updateUrlFromAdvancedSearch () { 258 private updateUrlFromAdvancedSearch () {
diff --git a/client/src/app/+signup/+register/custom-stepper.component.html b/client/src/app/+signup/+register/custom-stepper.component.html
index aad2f31d3..a07e2fca3 100644
--- a/client/src/app/+signup/+register/custom-stepper.component.html
+++ b/client/src/app/+signup/+register/custom-stepper.component.html
@@ -6,7 +6,7 @@
6 (click)="onClick(i)" 6 (click)="onClick(i)"
7 > 7 >
8 <div class="step-index"> 8 <div class="step-index">
9 <ng-container *ngIf="!isCompleted(step)"><span class="sr-only" i18n>Step</span> {{ i + 1 }}</ng-container> 9 <ng-container *ngIf="!isCompleted(step)"><span class="visually-hidden" i18n>Step</span> {{ i + 1 }}</ng-container>
10 <my-global-icon *ngIf="isCompleted(step)" iconName="tick"></my-global-icon> 10 <my-global-icon *ngIf="isCompleted(step)" iconName="tick"></my-global-icon>
11 </div> 11 </div>
12 12
diff --git a/client/src/app/+signup/+register/register-step-channel.component.html b/client/src/app/+signup/+register/register-step-channel.component.html
index 67f332409..888e3245d 100644
--- a/client/src/app/+signup/+register/register-step-channel.component.html
+++ b/client/src/app/+signup/+register/register-step-channel.component.html
@@ -34,9 +34,7 @@
34 type="text" id="name" i18n-placeholder placeholder="Example: my_super_channel" 34 type="text" id="name" i18n-placeholder placeholder="Example: my_super_channel"
35 formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" 35 formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }"
36 > 36 >
37 <div class="input-group-append"> 37 <div class="input-group-text">@{{ instanceHost }}</div>
38 <span class="input-group-text">@{{ instanceHost }}</span>
39 </div>
40 </div> 38 </div>
41 39
42 <div class="name-information" i18n> 40 <div class="name-information" i18n>
diff --git a/client/src/app/+signup/+register/register-step-terms.component.html b/client/src/app/+signup/+register/register-step-terms.component.html
index 28a6e0021..717a289e6 100644
--- a/client/src/app/+signup/+register/register-step-terms.component.html
+++ b/client/src/app/+signup/+register/register-step-terms.component.html
@@ -1,8 +1,8 @@
1<form role="form" [formGroup]="form"> 1<form role="form" [formGroup]="form">
2 <div class="form-group form-group-terms"> 2 <div class="form-group">
3 <my-peertube-checkbox inputName="terms" formControlName="terms"> 3 <my-peertube-checkbox inputName="terms" formControlName="terms">
4 <ng-template ptTemplate="label"> 4 <ng-template ptTemplate="label">
5 <ng-container i18n> 5 <ng-container i18n>
6 I am at least {{ minimumAge }} years old and agree 6 I am at least {{ minimumAge }} years old and agree
7 to the <a class="terms-anchor" (click)="onTermsClick($event)" href='#'>Terms</a> 7 to the <a class="terms-anchor" (click)="onTermsClick($event)" href='#'>Terms</a>
8 <ng-container *ngIf="hasCodeOfConduct"> and to the <a (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container> 8 <ng-container *ngIf="hasCodeOfConduct"> and to the <a (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container>
diff --git a/client/src/app/+signup/+register/register-step-user.component.html b/client/src/app/+signup/+register/register-step-user.component.html
index cab21c655..745b37c10 100644
--- a/client/src/app/+signup/+register/register-step-user.component.html
+++ b/client/src/app/+signup/+register/register-step-user.component.html
@@ -27,9 +27,7 @@
27 type="text" id="username" i18n-placeholder="Username choice placeholder in the registration form" placeholder="e.g. jane_doe" 27 type="text" id="username" i18n-placeholder="Username choice placeholder in the registration form" placeholder="e.g. jane_doe"
28 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" 28 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }"
29 > 29 >
30 <div class="input-group-append"> 30 <span class="input-group-text">@{{ instanceHost }}</span>
31 <span class="input-group-text">@{{ instanceHost }}</span>
32 </div>
33 </div> 31 </div>
34 32
35 <div class="name-information" i18n> 33 <div class="name-information" i18n>
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html
index 2d0e6e865..5c4fe5f0b 100644
--- a/client/src/app/+signup/+register/register.component.html
+++ b/client/src/app/+signup/+register/register.component.html
@@ -32,7 +32,7 @@
32 <button cdkStepperNext [disabled]="!formStepTerms || !formStepTerms.valid">{{ defaultNextStepButtonLabel }}</button> 32 <button cdkStepperNext [disabled]="!formStepTerms || !formStepTerms.valid">{{ defaultNextStepButtonLabel }}</button>
33 </cdk-step> 33 </cdk-step>
34 34
35 <cdk-step [stepControl]="formStepUser" i18n-label="Stepper label for the registration page asking user informations" label="User"> 35 <cdk-step [stepControl]="formStepUser" i18n-label="Stepper label for the registration page asking user information" label="User">
36 <my-register-step-user (formBuilt)="onUserFormBuilt($event)" [videoUploadDisabled]="videoUploadDisabled"></my-register-step-user> 36 <my-register-step-user (formBuilt)="onUserFormBuilt($event)" [videoUploadDisabled]="videoUploadDisabled"></my-register-step-user>
37 37
38 <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button> 38 <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
diff --git a/client/src/app/+signup/+register/register.component.scss b/client/src/app/+signup/+register/register.component.scss
index 4be67a858..53093a81a 100644
--- a/client/src/app/+signup/+register/register.component.scss
+++ b/client/src/app/+signup/+register/register.component.scss
@@ -29,22 +29,6 @@
29 } 29 }
30} 30}
31 31
32.form-group-terms {
33 margin: 30px 0;
34}
35
36.input-group {
37 @include peertube-input-group(100%);
38}
39
40.input-group-append {
41 height: 30px;
42}
43
44.form-group-terms {
45 width: 100% !important;
46}
47
48input:not([type=submit]) { 32input:not([type=submit]) {
49 @include peertube-input-text(100%); 33 @include peertube-input-text(100%);
50 display: block; 34 display: block;
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index 212e2f867..780db79b0 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -72,10 +72,10 @@
72 </div> 72 </div>
73 73
74 <div class="actor-counters"> 74 <div class="actor-counters">
75 <span i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span> 75 <span i18n>{videoChannel.followersCount, plural, =0 {No subscribers} =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span>
76 76
77 <span class="videos-count" *ngIf="channelVideosCount !== undefined" i18n> 77 <span class="videos-count" *ngIf="channelVideosCount !== undefined" i18n>
78 {channelVideosCount, plural, =1 {1 videos} other {{{ channelVideosCount }} videos}} 78 {channelVideosCount, plural, =0 {No videos} =1 {1 video} other {{{ channelVideosCount }} videos}}
79 </span> 79 </span>
80 </div> 80 </div>
81 </div> 81 </div>
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
index 5344e5431..7add71886 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
@@ -33,10 +33,6 @@ my-peertube-checkbox {
33 height: 100%; 33 height: 100%;
34 min-height: 300px; 34 min-height: 300px;
35 35
36 .form-group {
37 margin-bottom: 25px;
38 }
39
40 input { 36 input {
41 @include peertube-input-text(100%); 37 @include peertube-input-text(100%);
42 display: block; 38 display: block;
@@ -45,10 +41,6 @@ my-peertube-checkbox {
45 .label-tags + span { 41 .label-tags + span {
46 font-size: 15px; 42 font-size: 15px;
47 } 43 }
48
49 .advanced-settings .form-group {
50 margin-bottom: 20px;
51 }
52} 44}
53 45
54.captions-header { 46.captions-header {
diff --git a/client/src/app/+videos/+video-edit/video-update.component.html b/client/src/app/+videos/+video-edit/video-update.component.html
index 4376f6fe4..446406d1a 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.html
+++ b/client/src/app/+videos/+video-edit/video-update.component.html
@@ -1,6 +1,6 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <div class="title-page title-page-single"> 2 <div class="title-page title-page-single">
3 <span class="mr-1" i18n>Update</span> 3 <span class="me-1" i18n>Update</span>
4 <a [routerLink]="getVideoUrl()">{{ video?.name }}</a> 4 <a [routerLink]="getVideoUrl()">{{ video?.name }}</a>
5 </div> 5 </div>
6 6
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html
index 3ee818c8b..95d117eec 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html
@@ -2,7 +2,7 @@
2 <div class="avatar-and-textarea"> 2 <div class="avatar-and-textarea">
3 <my-actor-avatar [account]="user?.account" size="25"></my-actor-avatar> 3 <my-actor-avatar [account]="user?.account" size="25"></my-actor-avatar>
4 4
5 <div class="form-group"> 5 <div class="textarea-wrapper">
6 <textarea i18n-placeholder placeholder="Add comment..." myAutoResize 6 <textarea i18n-placeholder placeholder="Add comment..." myAutoResize
7 [readonly]="(user === null) ? true : false" 7 [readonly]="(user === null) ? true : false"
8 (click)="openVisitorModal($event)" 8 (click)="openVisitorModal($event)"
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.scss b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.scss
index ae889dd38..023d625e9 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.scss
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.scss
@@ -17,9 +17,8 @@ form {
17 @include margin-right(10px); 17 @include margin-right(10px);
18 } 18 }
19 19
20 .form-group { 20 .textarea-wrapper {
21 flex-grow: 1; 21 flex-grow: 1;
22 margin: 0;
23 position: relative; 22 position: relative;
24 } 23 }
25 24
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 5014b9692..160865519 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
@@ -16,7 +16,7 @@
16 {{ comment.account.displayName }} 16 {{ comment.account.displayName }}
17 </span> 17 </span>
18 18
19 <span class="comment-account-fid ml-1">{{ comment.by }}</span> 19 <span class="comment-account-fid ms-1">{{ comment.by }}</span>
20 </a> 20 </a>
21 </div> 21 </div>
22 22
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 0e00c9c0e..a76379924 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
@@ -6,7 +6,7 @@
6 6
7 <my-feed [syndicationItems]="syndicationItems"></my-feed> 7 <my-feed [syndicationItems]="syndicationItems"></my-feed>
8 8
9 <div ngbDropdown class="d-inline-block ml-4 dropdown-root"> 9 <div ngbDropdown class="d-inline-block ms-4 dropdown-root">
10 <button class="btn btn-sm btn-outline-secondary" id="dropdown-sort-comments" ngbDropdownToggle i18n> 10 <button class="btn btn-sm btn-outline-secondary" id="dropdown-sort-comments" ngbDropdownToggle i18n>
11 SORT BY 11 SORT BY
12 </button> 12 </button>
@@ -80,7 +80,7 @@
80 80
81 <ng-template i18n #noAuthorComments>View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}}</ng-template> 81 <ng-template i18n #noAuthorComments>View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}}</ng-template>
82 82
83 <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader> 83 <my-small-loader class="comment-thread-loading ms-1" [loading]="threadLoading[comment.id]"></my-small-loader>
84 </div> 84 </div>
85 </my-video-comment> 85 </my-video-comment>
86 86
diff --git a/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html b/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html
index d579aaddb..b64d45564 100644
--- a/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html
+++ b/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html
@@ -1,6 +1,6 @@
1<div class="privacy-concerns" *ngIf="display"> 1<div class="privacy-concerns" *ngIf="display">
2 <div class="privacy-concerns-text"> 2 <div class="privacy-concerns-text">
3 <span class="mr-2"> 3 <span class="me-2">
4 <strong i18n>Friendly Reminder: </strong> 4 <strong i18n>Friendly Reminder: </strong>
5 <ng-container i18n> 5 <ng-container i18n>
6 the sharing system used for this video implies that some technical information about your system (such as a public IP address) can be sent to other peers. 6 the sharing system used for this video implies that some technical information about your system (such as a public IP address) can be sent to other peers.
diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html
index f5dd352a3..b04bd3548 100644
--- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html
+++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html
@@ -6,9 +6,9 @@
6 <div class="playlist-display-name"> 6 <div class="playlist-display-name">
7 {{ playlist.displayName }} 7 {{ playlist.displayName }}
8 8
9 <span *ngIf="isUnlistedPlaylist()" class="badge badge-warning" i18n>Unlisted</span> 9 <span *ngIf="isUnlistedPlaylist()" class="pt-badge badge-warning" i18n>Unlisted</span>
10 <span *ngIf="isPrivatePlaylist()" class="badge badge-danger" i18n>Private</span> 10 <span *ngIf="isPrivatePlaylist()" class="pt-badge badge-danger" i18n>Private</span>
11 <span *ngIf="isPublicPlaylist()" class="badge badge-info" i18n>Public</span> 11 <span *ngIf="isPublicPlaylist()" class="pt-badge badge-info" i18n>Public</span>
12 </div> 12 </div>
13 13
14 <div class="playlist-by-index"> 14 <div class="playlist-by-index">
diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss
index 5c3453e4b..0f0ac1979 100644
--- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss
+++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss
@@ -15,43 +15,47 @@
15 .playlist-info { 15 .playlist-info {
16 padding: 5px 30px; 16 padding: 5px 30px;
17 background-color: pvar(--greyBackgroundColor); 17 background-color: pvar(--greyBackgroundColor);
18 }
19
20 .playlist-display-name {
21 font-size: 18px;
22 font-weight: $font-semibold;
23 margin-bottom: 5px;
18 24
19 .playlist-display-name { 25 .pt-badge {
20 font-size: 18px; 26 @include margin-left(5px);
21 font-weight: $font-semibold;
22 margin-bottom: 5px;
23 } 27 }
28 }
24 29
25 .playlist-by-index { 30 .playlist-by-index {
26 color: pvar(--greyForegroundColor); 31 color: pvar(--greyForegroundColor);
27 display: flex; 32 display: flex;
28 33
29 .playlist-by { 34 .playlist-by {
30 @include margin-right(5px); 35 @include margin-right(5px);
31 } 36 }
32 37
33 .playlist-index span:first-child::after { 38 .playlist-index span:first-child::after {
34 content: '/'; 39 content: '/';
35 margin: 0 3px; 40 margin: 0 3px;
36 }
37 } 41 }
42 }
38 43
39 .playlist-controls { 44 .playlist-controls {
40 display: flex; 45 display: flex;
41 margin: 10px 0; 46 margin: 10px 0;
42 47
43 my-global-icon:not(:last-child) { 48 my-global-icon:not(:last-child) {
44 @include margin-right(.5rem); 49 @include margin-right(.5rem);
45 } 50 }
46 51
47 my-global-icon { 52 my-global-icon {
48 &:not(.active) { 53 &:not(.active) {
49 opacity: .5; 54 opacity: .5;
50 } 55 }
51 56
52 ::ng-deep { 57 ::ng-deep {
53 cursor: pointer; 58 cursor: pointer;
54 }
55 } 59 }
56 } 60 }
57 } 61 }
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 1ea0cf6b8..461891779 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch.component.html
@@ -61,7 +61,7 @@
61 <div class="video-info-channel-left d-flex"> 61 <div class="video-info-channel-left d-flex">
62 <my-video-avatar-channel [video]="video" [showChannel]="!isChannelDisplayNameGeneric()"></my-video-avatar-channel> 62 <my-video-avatar-channel [video]="video" [showChannel]="!isChannelDisplayNameGeneric()"></my-video-avatar-channel>
63 63
64 <div class="video-info-channel-left-links ml-1"> 64 <div class="video-info-channel-left-links ms-1">
65 <ng-container *ngIf="!isChannelDisplayNameGeneric()"> 65 <ng-container *ngIf="!isChannelDisplayNameGeneric()">
66 <a [routerLink]="[ '/c', video.byVideoChannel ]" i18n-title title="Channel page"> 66 <a [routerLink]="[ '/c', video.byVideoChannel ]" i18n-title title="Channel page">
67 {{ video.channel.displayName }} 67 {{ video.channel.displayName }}
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.html b/client/src/app/+videos/video-list/overview/video-overview.component.html
index f250c2407..1d5131092 100644
--- a/client/src/app/+videos/video-list/overview/video-overview.component.html
+++ b/client/src/app/+videos/video-list/overview/video-overview.component.html
@@ -1,4 +1,4 @@
1<h1 class="sr-only" i18n>Discover</h1> 1<h1 class="visually-hidden" i18n>Discover</h1>
2<div class="margin-content"> 2<div class="margin-content">
3 3
4 <div class="no-results" i18n *ngIf="notResults">No results.</div> 4 <div class="no-results" i18n *ngIf="notResults">No results.</div>
diff --git a/client/src/app/+videos/video-list/videos-list-common-page.component.ts b/client/src/app/+videos/video-list/videos-list-common-page.component.ts
index d2782036b..c8fa8ef30 100644
--- a/client/src/app/+videos/video-list/videos-list-common-page.component.ts
+++ b/client/src/app/+videos/video-list/videos-list-common-page.component.ts
@@ -204,13 +204,28 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable
204 if ([ 'hot', 'trending', 'likes', 'views' ].includes(sanitizedSort)) { 204 if ([ 'hot', 'trending', 'likes', 'views' ].includes(sanitizedSort)) {
205 this.title = $localize`Trending` 205 this.title = $localize`Trending`
206 206
207 if (sanitizedSort === 'hot') this.titleTooltip = $localize`Videos with the most interactions for recent videos` 207 if (sanitizedSort === 'hot') {
208 if (sanitizedSort === 'likes') this.titleTooltip = $localize`Videos that have the most likes` 208 this.titleTooltip = $localize`Videos with the most interactions for recent videos`
209 if (sanitizedSort === 'views') this.titleTooltip = undefined 209 return
210 }
211
212 if (sanitizedSort === 'likes') {
213 this.titleTooltip = $localize`Videos that have the most likes`
214 return
215 }
216
217 if (sanitizedSort === 'views') {
218 this.titleTooltip = undefined
219 return
220 }
210 221
211 if (sanitizedSort === 'trending') { 222 if (sanitizedSort === 'trending') {
212 if (this.trendingDays === 1) this.titleTooltip = $localize`Videos with the most views during the last 24 hours` 223 if (this.trendingDays === 1) {
213 else this.titleTooltip = $localize`Videos with the most views during the last ${this.trendingDays} days` 224 this.titleTooltip = $localize`Videos with the most views during the last 24 hours`
225 return
226 }
227
228 this.titleTooltip = $localize`Videos with the most views during the last ${this.trendingDays} days`
214 } 229 }
215 230
216 return 231 return
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index d80f95ed6..4a4c2321b 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -41,7 +41,9 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra
41 ToastModule, 41 ToastModule,
42 42
43 HotkeyModule.forRoot({ 43 HotkeyModule.forRoot({
44 cheatSheetCloseEsc: true 44 cheatSheetCloseEsc: true,
45 cheatSheetDescription: $localize`Show/hide this help menu`,
46 cheatSheetCloseEscDescription: $localize`Hide this help menu`
45 }) 47 })
46 ], 48 ],
47 49
diff --git a/client/src/app/core/rest/rest-extractor.service.ts b/client/src/app/core/rest/rest-extractor.service.ts
index 17053811c..86c7484a5 100644
--- a/client/src/app/core/rest/rest-extractor.service.ts
+++ b/client/src/app/core/rest/rest-extractor.service.ts
@@ -34,49 +34,17 @@ export class RestExtractor {
34 return target 34 return target
35 } 35 }
36 36
37 handleError (err: any) { 37 redirectTo404IfNotFound (obj: { status: number }, type: 'video' | 'other', status = [ HttpStatusCode.NOT_FOUND_404 ]) {
38 let errorMessage 38 if (obj?.status && status.includes(obj.status)) {
39 // Do not use redirectService to avoid circular dependencies
40 this.router.navigate([ '/404' ], { state: { type, obj }, skipLocationChange: true })
41 }
39 42
40 if (err.error instanceof Error) { 43 return observableThrowError(() => obj)
41 // A client-side or network error occurred. Handle it accordingly. 44 }
42 errorMessage = err.error.detail || err.error.title
43 console.error('An error occurred:', errorMessage)
44 } else if (typeof err.error === 'string') {
45 errorMessage = err.error
46 } else if (err.status !== undefined) {
47 // A server-side error occurred.
48 if (err.error?.errors) {
49 const errors = err.error.errors
50 const errorsArray: string[] = []
51
52 Object.keys(errors).forEach(key => {
53 errorsArray.push(errors[key].msg)
54 })
55
56 errorMessage = errorsArray.join('. ')
57 } else if (err.error?.error) {
58 errorMessage = err.error.error
59 } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
60 // eslint-disable-next-line max-len
61 errorMessage = $localize`Media is too large for the server. Please contact you administrator if you want to increase the limit size.`
62 } else if (err.status === HttpStatusCode.TOO_MANY_REQUESTS_429) {
63 const secondsLeft = err.headers.get('retry-after')
64 if (secondsLeft) {
65 const minutesLeft = Math.floor(parseInt(secondsLeft, 10) / 60)
66 errorMessage = $localize`Too many attempts, please try again after ${minutesLeft} minutes.`
67 } else {
68 errorMessage = $localize`Too many attempts, please try again later.`
69 }
70 } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
71 errorMessage = $localize`Server error. Please retry later.`
72 }
73 45
74 errorMessage = errorMessage || 'Unknown error.' 46 handleError (err: any) {
75 console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) 47 const errorMessage = this.buildErrorMessage(err)
76 } else {
77 console.error(err)
78 errorMessage = err
79 }
80 48
81 const errorObj: { message: string, status: string, body: string } = { 49 const errorObj: { message: string, status: string, body: string } = {
82 message: errorMessage, 50 message: errorMessage,
@@ -92,12 +60,63 @@ export class RestExtractor {
92 return observableThrowError(() => errorObj) 60 return observableThrowError(() => errorObj)
93 } 61 }
94 62
95 redirectTo404IfNotFound (obj: { status: number }, type: 'video' | 'other', status = [ HttpStatusCode.NOT_FOUND_404 ]) { 63 private buildErrorMessage (err: any) {
96 if (obj?.status && status.includes(obj.status)) { 64 if (err.error instanceof Error) {
97 // Do not use redirectService to avoid circular dependencies 65 // A client-side or network error occurred. Handle it accordingly.
98 this.router.navigate([ '/404' ], { state: { type, obj }, skipLocationChange: true }) 66 const errorMessage = err.error.detail || err.error.title
67 console.error('An error occurred:', errorMessage)
68
69 return errorMessage
99 } 70 }
100 71
101 return observableThrowError(() => obj) 72 if (typeof err.error === 'string') {
73 return err.error
74 }
75
76 if (err.status !== undefined) {
77 const errorMessage = this.buildServerErrorMessage(err)
78 console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`)
79
80 return errorMessage
81 }
82
83 console.error(err)
84 return err
85 }
86
87 private buildServerErrorMessage (err: any) {
88 // A server-side error occurred.
89 if (err.error?.errors) {
90 const errors = err.error.errors
91
92 return Object.keys(errors)
93 .map(key => errors[key].msg)
94 .join('. ')
95 }
96
97 if (err.error?.error) {
98 return err.error.error
99 }
100
101 if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
102 return $localize`Media is too large for the server. Please contact you administrator if you want to increase the limit size.`
103 }
104
105 if (err.status === HttpStatusCode.TOO_MANY_REQUESTS_429) {
106 const secondsLeft = err.headers.get('retry-after')
107
108 if (secondsLeft) {
109 const minutesLeft = Math.floor(parseInt(secondsLeft, 10) / 60)
110 return $localize`Too many attempts, please try again after ${minutesLeft} minutes.`
111 }
112
113 return $localize`Too many attempts, please try again later.`
114 }
115
116 if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
117 return $localize`Server error. Please retry later.`
118 }
119
120 return $localize`Unknown server error`
102 } 121 }
103} 122}
diff --git a/client/src/app/core/rest/rest-table.ts b/client/src/app/core/rest/rest-table.ts
index d8b039187..7b765f7fc 100644
--- a/client/src/app/core/rest/rest-table.ts
+++ b/client/src/app/core/rest/rest-table.ts
@@ -39,6 +39,10 @@ export abstract class RestTable {
39 } 39 }
40 } 40 }
41 41
42 saveSort () {
43 peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort))
44 }
45
42 loadLazy (event: LazyLoadEvent) { 46 loadLazy (event: LazyLoadEvent) {
43 logger('Load lazy %o.', event) 47 logger('Load lazy %o.', event)
44 48
@@ -60,10 +64,6 @@ export abstract class RestTable {
60 this.saveSort() 64 this.saveSort()
61 } 65 }
62 66
63 saveSort () {
64 peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort))
65 }
66
67 onSearch (search: string) { 67 onSearch (search: string) {
68 this.search = search 68 this.search = search
69 this.reloadData() 69 this.reloadData()
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html
index 7a9b6c51f..d08e2f0fb 100644
--- a/client/src/app/header/search-typeahead.component.html
+++ b/client/src/app/header/search-typeahead.component.html
@@ -25,7 +25,7 @@
25 <div class="d-flex justify-content-between"> 25 <div class="d-flex justify-content-between">
26 <label class="small-title" i18n>GLOBAL SEARCH</label> 26 <label class="small-title" i18n>GLOBAL SEARCH</label>
27 <div class="advanced-search-status muted"> 27 <div class="advanced-search-status muted">
28 <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.search.searchIndex.url }}</span> 28 <span *ngIf="serverConfig" class="me-1" i18n>using {{ serverConfig.search.searchIndex.url }}</span>
29 <i class="glyphicon glyphicon-globe"></i> 29 <i class="glyphicon glyphicon-globe"></i>
30 </div> 30 </div>
31 </div> 31 </div>
@@ -39,8 +39,8 @@
39 <label class="small-title" i18n>ADVANCED SEARCH</label> 39 <label class="small-title" i18n>ADVANCED SEARCH</label>
40 <div class="advanced-search-status c-help"> 40 <div class="advanced-search-status c-help">
41 <span [ngClass]="canSearchAnyURI ? 'text-success' : 'muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows."> 41 <span [ngClass]="canSearchAnyURI ? 'text-success' : 'muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.">
42 <span *ngIf="canSearchAnyURI()" class="mr-1" i18n>any instance</span> 42 <span *ngIf="canSearchAnyURI()" class="me-1" i18n>any instance</span>
43 <span *ngIf="!canSearchAnyURI()" class="mr-1" i18n>only followed instances</span> 43 <span *ngIf="!canSearchAnyURI()" class="me-1" i18n>only followed instances</span>
44 <i [ngClass]="canSearchAnyURI() ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i> 44 <i [ngClass]="canSearchAnyURI() ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
45 </span> 45 </span>
46 </div> 46 </div>
diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html
index 4ac9809e1..3f85ed6ae 100644
--- a/client/src/app/header/suggestion.component.html
+++ b/client/src/app/header/suggestion.component.html
@@ -1,16 +1,16 @@
1<a tabindex="-1" class="d-flex flex-auto flex-items-center p-2" [class.focus-visible]="active"> 1<a tabindex="-1" class="d-flex flex-auto flex-items-center p-2" [class.focus-visible]="active">
2 <div class="flex-shrink-0 mr-2 text-center"> 2 <div class="flex-shrink-0 me-2 text-center">
3 <my-global-icon iconName="search"></my-global-icon> 3 <my-global-icon iconName="search"></my-global-icon>
4 </div> 4 </div>
5 5
6 <img class="avatar mr-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28"> 6 <img class="avatar me-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28">
7 7
8 <div 8 <div
9 class="flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target" 9 class="flex-auto overflow-hidden text-start no-wrap css-truncate css-truncate-target"
10 [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : highlight" 10 [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : highlight"
11 ></div> 11 ></div>
12 12
13 <div class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6"> 13 <div class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ms-1 f6">
14 <span *ngIf="result.type === 'search-instance'" i18n>In this instance's network</span> 14 <span *ngIf="result.type === 'search-instance'" i18n>In this instance's network</span>
15 <span *ngIf="result.type === 'search-index'" i18n>In the vidiverse</span> 15 <span *ngIf="result.type === 'search-index'" i18n>In the vidiverse</span>
16 </div> 16 </div>
diff --git a/client/src/app/helpers/i18n-utils.ts b/client/src/app/helpers/i18n-utils.ts
index bbfb12959..2017a31ea 100644
--- a/client/src/app/helpers/i18n-utils.ts
+++ b/client/src/app/helpers/i18n-utils.ts
@@ -1,4 +1,5 @@
1import { environment } from '../../environments/environment' 1import { environment } from '../../environments/environment'
2import IntlMessageFormat from 'intl-messageformat'
2 3
3function isOnDevLocale () { 4function isOnDevLocale () {
4 return environment.production === false && window.location.search === '?lang=fr' 5 return environment.production === false && window.location.search === '?lang=fr'
@@ -8,7 +9,31 @@ function getDevLocale () {
8 return 'fr-FR' 9 return 'fr-FR'
9} 10}
10 11
12function prepareIcu (icu: string) {
13 let alreadyWarned = false
14
15 try {
16 const msg = new IntlMessageFormat(icu, $localize.locale)
17
18 return (context: { [id: string]: number | string }, fallback: string) => {
19 try {
20 return msg.format(context) as string
21 } catch (err) {
22 if (!alreadyWarned) console.warn('Cannot format ICU %s.', icu, err)
23
24 alreadyWarned = true
25 return fallback
26 }
27 }
28 } catch (err) {
29 console.warn('Cannot build intl message %s.', icu, err)
30
31 return (_context: unknown, fallback: string) => fallback
32 }
33}
34
11export { 35export {
12 getDevLocale, 36 getDevLocale,
37 prepareIcu,
13 isOnDevLocale 38 isOnDevLocale
14} 39}
diff --git a/client/src/app/helpers/utils/upload.ts b/client/src/app/helpers/utils/upload.ts
index a3fce7fee..5c2600a0d 100644
--- a/client/src/app/helpers/utils/upload.ts
+++ b/client/src/app/helpers/utils/upload.ts
@@ -2,36 +2,43 @@ import { HttpErrorResponse } from '@angular/common/http'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { HttpStatusCode } from '@shared/models' 3import { HttpStatusCode } from '@shared/models'
4 4
5function genericUploadErrorHandler (parameters: { 5function genericUploadErrorHandler (options: {
6 err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'> 6 err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'>
7 name: string 7 name: string
8 notifier: Notifier 8 notifier: Notifier
9 sticky?: boolean 9 sticky?: boolean
10}) { 10}) {
11 const { err, name, notifier, sticky } = { sticky: false, ...parameters } 11 const { err, name, notifier, sticky = false } = options
12 const title = $localize`The upload failed` 12 const title = $localize`Upload failed`
13 let message = err.message 13 const message = buildMessage(name, err)
14
15 if (err instanceof ErrorEvent) { // network error
16 message = $localize`The connection was interrupted`
17 notifier.error(message, title, null, sticky)
18 } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
19 message = $localize`The server encountered an error`
20 notifier.error(message, title, null, sticky)
21 } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) {
22 message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)`
23 notifier.error(message, title, null, sticky)
24 } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
25 const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G'
26 message = $localize`Your ${name} file was too large (max. size: ${maxFileSize})`
27 notifier.error(message, title, null, sticky)
28 } else {
29 notifier.error(err.message, title)
30 }
31 14
15 notifier.error(message, title, null, sticky)
32 return message 16 return message
33} 17}
34 18
35export { 19export {
36 genericUploadErrorHandler 20 genericUploadErrorHandler
37} 21}
22
23// ---------------------------------------------------------------------------
24
25function buildMessage (name: string, err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'>) {
26 if (err instanceof ErrorEvent) { // network error
27 return $localize`The connection was interrupted`
28 }
29
30 if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
31 return $localize`The server encountered an error`
32 }
33
34 if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) {
35 return $localize`Your ${name} file couldn't be transferred before the server proxy timeout`
36 }
37
38 if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
39 const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G'
40 return $localize`Your ${name} file was too large (max. size: ${maxFileSize})`
41 }
42
43 return err.message
44}
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index b8f8d68ab..2961daaa3 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -36,14 +36,14 @@
36 > 36 >
37 <my-global-icon iconName="language" aria-hidden="true"></my-global-icon> 37 <my-global-icon iconName="language" aria-hidden="true"></my-global-icon>
38 <span i18n>Interface:</span> 38 <span i18n>Interface:</span>
39 <span class="ml-auto muted">{{ currentInterfaceLanguage }}</span> 39 <span class="ms-auto muted">{{ currentInterfaceLanguage }}</span>
40 </a> 40 </a>
41 41
42 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account/settings" fragment="video-languages-subtitles" 42 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account/settings" fragment="video-languages-subtitles"
43 #settingsLanguagesSubtitles (click)="onActiveLinkScrollToAnchor(settingsLanguagesSubtitles)"> 43 #settingsLanguagesSubtitles (click)="onActiveLinkScrollToAnchor(settingsLanguagesSubtitles)">
44 <my-global-icon iconName="video-lang" aria-hidden="true"></my-global-icon> 44 <my-global-icon iconName="video-lang" aria-hidden="true"></my-global-icon>
45 <span i18n>Videos:</span> 45 <span i18n>Videos:</span>
46 <span class="ml-auto muted">{{ videoLanguages.join(', ') }}</span> 46 <span class="ms-auto muted">{{ videoLanguages.join(', ') }}</span>
47 </a> 47 </a>
48 48
49 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item settings-sensitive" routerLink="/my-account/settings" 49 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item settings-sensitive" routerLink="/my-account/settings"
@@ -53,14 +53,14 @@
53 <my-global-icon class="hover-display-toggle" [hidden]="user.nsfwPolicy === 'display'" iconName="sensitive" aria-hidden="true"></my-global-icon> 53 <my-global-icon class="hover-display-toggle" [hidden]="user.nsfwPolicy === 'display'" iconName="sensitive" aria-hidden="true"></my-global-icon>
54 <my-global-icon class="hover-display-toggle" [hidden]="user.nsfwPolicy !== 'display'" iconName="unsensitive" aria-hidden="true"></my-global-icon> 54 <my-global-icon class="hover-display-toggle" [hidden]="user.nsfwPolicy !== 'display'" iconName="unsensitive" aria-hidden="true"></my-global-icon>
55 <span i18n>Sensitive:</span> 55 <span i18n>Sensitive:</span>
56 <span class="ml-auto muted">{{ nsfwPolicy }}</span> 56 <span class="ms-auto muted">{{ nsfwPolicy }}</span>
57 </a> 57 </a>
58 58
59 <a ngbDropdownItem class="dropdown-item" (click)="toggleUseP2P()"> 59 <a ngbDropdownItem class="dropdown-item" (click)="toggleUseP2P()">
60 <my-global-icon iconName="p2p" aria-hidden="true"></my-global-icon> 60 <my-global-icon iconName="p2p" aria-hidden="true"></my-global-icon>
61 <ng-container i18n>Help share videos</ng-container> 61 <ng-container i18n>Help share videos</ng-container>
62 62
63 <my-input-switch class="ml-auto" [checked]="user.p2pEnabled"></my-input-switch> 63 <my-input-switch class="ms-auto" [checked]="user.p2pEnabled"></my-input-switch>
64 </a> 64 </a>
65 65
66 <div class="dropdown-divider"></div> 66 <div class="dropdown-divider"></div>
@@ -149,7 +149,7 @@
149 149
150 <div class="footer-copyleft"> 150 <div class="footer-copyleft">
151 <small class="d-inline" i18n-title title="powered by PeerTube - CopyLeft 2015-2022"> 151 <small class="d-inline" i18n-title title="powered by PeerTube - CopyLeft 2015-2022">
152 <a href="https://joinpeertube.org" class="mr-1" target="_blank" rel="noopener noreferrer" i18n>powered by PeerTube</a> 152 <a href="https://joinpeertube.org" class="me-1" target="_blank" rel="noopener noreferrer" i18n>powered by PeerTube</a>
153 153
154 <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" target="_blank" rel="noopener noreferrer"> 154 <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" target="_blank" rel="noopener noreferrer">
155 <span aria-label="copyleft" class="d-inline-block" style="transform: rotateY(180deg)">&copy;</span> 2015-2022 155 <span aria-label="copyleft" class="d-inline-block" style="transform: rotateY(180deg)">&copy;</span> 2015-2022
diff --git a/client/src/app/menu/notification.component.html b/client/src/app/menu/notification.component.html
index beda1c43c..2bd11c6f7 100644
--- a/client/src/app/menu/notification.component.html
+++ b/client/src/app/menu/notification.component.html
@@ -24,7 +24,7 @@
24 <div> 24 <div>
25 <button 25 <button
26 *ngIf="unreadNotifications" 26 *ngIf="unreadNotifications"
27 i18n-title title="Mark all as read" class="glyphicon glyphicon-ok mr-2" 27 i18n-title title="Mark all as read" class="glyphicon glyphicon-ok me-2"
28 (click)="markAllAsRead()" 28 (click)="markAllAsRead()"
29 ></button> 29 ></button>
30 <a 30 <a
@@ -45,7 +45,7 @@
45 ></my-user-notifications> 45 ></my-user-notifications>
46 46
47 <a *ngIf="loaded" class="all-notifications" routerLink="/my-account/notifications" #notifications (click)="onNavigate(notifications)"> 47 <a *ngIf="loaded" class="all-notifications" routerLink="/my-account/notifications" #notifications (click)="onNavigate(notifications)">
48 <my-global-icon class="mr-1" iconName="bell" aria-hidden="true"></my-global-icon> 48 <my-global-icon class="me-1" iconName="bell" aria-hidden="true"></my-global-icon>
49 <span i18n>See all your notifications</span> 49 <span i18n>See all your notifications</span>
50 </a> 50 </a>
51 </div> 51 </div>
diff --git a/client/src/app/modal/confirm.component.html b/client/src/app/modal/confirm.component.html
index f07501726..c59c25770 100644
--- a/client/src/app/modal/confirm.component.html
+++ b/client/src/app/modal/confirm.component.html
@@ -9,7 +9,7 @@
9 <div class="modal-body" > 9 <div class="modal-body" >
10 <div [innerHtml]="message"></div> 10 <div [innerHtml]="message"></div>
11 11
12 <div *ngIf="inputLabel && expectedInputValue" class="form-group"> 12 <div *ngIf="inputLabel && expectedInputValue" class="form-group mt-3">
13 <label for="confirmInput">{{ inputLabel }}</label> 13 <label for="confirmInput">{{ inputLabel }}</label>
14 <input type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" /> 14 <input type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" />
15 </div> 15 </div>
diff --git a/client/src/app/modal/confirm.component.scss b/client/src/app/modal/confirm.component.scss
index 77ea4d307..21aa81eeb 100644
--- a/client/src/app/modal/confirm.component.scss
+++ b/client/src/app/modal/confirm.component.scss
@@ -13,7 +13,3 @@ input[type=text] {
13 @include peertube-input-text(100%); 13 @include peertube-input-text(100%);
14 display: block; 14 display: block;
15} 15}
16
17.form-group {
18 margin: 20px 0;
19}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
index 986de15ed..bb87279d7 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-details.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
@@ -17,9 +17,9 @@
17 </a> 17 </a>
18 18
19 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" 19 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
20 class="ml-auto muted abuse-details-links" i18n 20 class="ms-auto muted abuse-details-links" i18n
21 > 21 >
22 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> 22 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ms-1 glyphicon glyphicon-flag"></span>
23 </a> 23 </a>
24 </span> 24 </span>
25 </div> 25 </div>
@@ -37,9 +37,9 @@
37 </a> 37 </a>
38 38
39 <a *ngIf="isAdminView" [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }" 39 <a *ngIf="isAdminView" [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
40 class="ml-auto muted abuse-details-links" i18n 40 class="ms-auto muted abuse-details-links" i18n
41 > 41 >
42 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> 42 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ms-1 glyphicon glyphicon-flag"></span>
43 </a> 43 </a>
44 </span> 44 </span>
45 </div> 45 </div>
@@ -53,7 +53,7 @@
53 <div class="mt-3 d-flex"> 53 <div class="mt-3 d-flex">
54 <span class="moderation-expanded-label"> 54 <span class="moderation-expanded-label">
55 <ng-container i18n>Report</ng-container> 55 <ng-container i18n>Report</ng-container>
56 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 muted">#{{ abuse.id }}</a> 56 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ms-1 muted">#{{ abuse.id }}</a>
57 </span> 57 </span>
58 <span class="moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span> 58 <span class="moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
59 </div> 59 </div>
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
index f0a27c6e2..6d1de808d 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
@@ -8,7 +8,7 @@
8> 8>
9 <ng-template pTemplate="caption"> 9 <ng-template pTemplate="caption">
10 <div class="caption"> 10 <div class="caption">
11 <div class="ml-auto"> 11 <div class="ms-auto">
12 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> 12 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
13 </div> 13 </div>
14 </div> 14 </div>
diff --git a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html
index e9c5fadcf..b63bf1f92 100644
--- a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html
+++ b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html
@@ -6,7 +6,7 @@
6 6
7 <div *ngIf="editable && !hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body"> 7 <div *ngIf="editable && !hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body">
8 <my-global-icon iconName="upload"></my-global-icon> 8 <my-global-icon iconName="upload"></my-global-icon>
9 <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label> 9 <label class="visually-hidden" for="avatarfile" i18n>Upload a new avatar</label>
10 <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/> 10 <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
11 </div> 11 </div>
12 12
@@ -15,7 +15,7 @@
15 #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right" 15 #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right"
16 > 16 >
17 <my-global-icon iconName="edit"></my-global-icon> 17 <my-global-icon iconName="edit"></my-global-icon>
18 <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label> 18 <label class="visually-hidden" for="avatarMenu" i18n>Change your avatar</label>
19 </div> 19 </div>
20 20
21 </div> 21 </div>
diff --git a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.scss b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.scss
index cc5e36a32..a50a2bb0e 100644
--- a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.scss
+++ b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.scss
@@ -3,37 +3,37 @@
3 3
4.actor { 4.actor {
5 display: flex; 5 display: flex;
6}
6 7
7 my-actor-avatar { 8my-actor-avatar {
8 @include margin-right(15px); 9 @include margin-right(15px);
9 } 10}
11
12.actor-info {
13 display: inline-flex;
14 flex-direction: column;
15}
16
17.actor-info-display-name {
18 font-size: 20px;
19 font-weight: $font-bold;
10 20
11 .actor-info { 21 @media screen and (max-width: $small-view) {
12 display: inline-flex; 22 font-size: 16px;
13 flex-direction: column;
14
15 .actor-info-display-name {
16 font-size: 20px;
17 font-weight: $font-bold;
18
19 @media screen and (max-width: $small-view) {
20 font-size: 16px;
21 }
22 }
23
24 .actor-info-username {
25 position: relative;
26 font-size: 14px;
27 color: pvar(--greyForegroundColor);
28 }
29
30 .actor-info-followers {
31 font-size: 15px;
32 padding-bottom: .5rem;
33 }
34 } 23 }
35} 24}
36 25
26.actor-info-username {
27 position: relative;
28 font-size: 14px;
29 color: pvar(--greyForegroundColor);
30}
31
32.actor-info-followers {
33 font-size: 15px;
34 padding-bottom: .5rem;
35}
36
37.actor-img-edit-container { 37.actor-img-edit-container {
38 position: relative; 38 position: relative;
39 width: 0; 39 width: 0;
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
index 0e4d5fb12..7d3498d4c 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
@@ -7,7 +7,7 @@ import { MiniatureDisplayOptions } from '../../shared-video-miniature'
7import { CustomMarkupComponent } from './shared' 7import { CustomMarkupComponent } from './shared'
8 8
9/* 9/*
10 * Markup component list videos depending on criterias 10 * Markup component list videos depending on criteria
11*/ 11*/
12 12
13@Component({ 13@Component({
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.html b/client/src/app/shared/shared-forms/advanced-input-filter.component.html
index 7031cb53b..1d6b3df7e 100644
--- a/client/src/app/shared/shared-forms/advanced-input-filter.component.html
+++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.html
@@ -1,6 +1,7 @@
1<div class="input-group has-feedback has-clear"> 1<div class="input-group has-clear" ngbDropdown placement="bottom-left auto" container="body">
2 <div *ngIf="hasFilters()" class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body"> 2
3 <div class="input-group-text" ngbDropdownToggle> 3 <ng-container *ngIf="hasFilters()">
4 <div class="input-group-text c-hand" ngbDropdownToggle>
4 <span class="caret" aria-haspopup="menu" role="button"></span> 5 <span class="caret" aria-haspopup="menu" role="button"></span>
5 </div> 6 </div>
6 7
@@ -15,14 +16,14 @@
15 </button> 16 </button>
16 </ng-container> 17 </ng-container>
17 </div> 18 </div>
18 </div> 19 </ng-container>
19 20
20 <input 21 <input
21 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." 22 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
23 class="last-in-group"
22 [(ngModel)]="searchValue" 24 [(ngModel)]="searchValue"
23 (keyup)="onInputSearch($event)" 25 (keyup)="onInputSearch($event)"
24 > 26 >
25 27
26 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="onResetTableFilter()"></a> 28 <a class="glyphicon glyphicon-remove-sign form-control-clear" title="Clear filter" i18n-title (click)="onResetTableFilter()"></a>
27 <span class="sr-only" i18n>Clear filters</span>
28</div> 29</div>
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
index ee1b3b508..ed85385fd 100644
--- a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
+++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
@@ -23,7 +23,6 @@ my-global-icon {
23 width: 18px; 23 width: 18px;
24 height: 18px; 24 height: 18px;
25 25
26
27 &.no-visible { 26 &.no-visible {
28 @include margin-right($size + $margin); 27 @include margin-right($size + $margin);
29 28
diff --git a/client/src/app/shared/shared-forms/input-switch.component.html b/client/src/app/shared/shared-forms/input-switch.component.html
index ce1dee470..9a466055a 100644
--- a/client/src/app/shared/shared-forms/input-switch.component.html
+++ b/client/src/app/shared/shared-forms/input-switch.component.html
@@ -1,4 +1,4 @@
1<div (click)="update()"> 1<div (click)="update()">
2 <input type="checkbox" [checked]="checked"/> 2 <input type="checkbox" [checked]="checked"/>
3 <label class="ml-auto">Toggle</label> 3 <label class="ms-auto">Toggle</label>
4</div> 4</div>
diff --git a/client/src/app/shared/shared-forms/input-toggle-hidden.component.html b/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
index dfe646d2f..e5edb6c97 100644
--- a/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
+++ b/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
@@ -1,21 +1,19 @@
1<div class="input-group input-group-sm"> 1<div class="input-group">
2 <input 2 <input
3 [id]="inputId" [autocomplete]="autocomplete" [value]="value" [placeholder]="placeholder" [tabindex]="tabindex" 3 [id]="inputId" [autocomplete]="autocomplete" [value]="value" [placeholder]="placeholder" [tabindex]="tabindex"
4 [(ngModel)]="value" (ngModelChange)="update()" [readonly]="readonly" 4 [(ngModel)]="value" (ngModelChange)="update()" [readonly]="readonly"
5 #input (click)="input.select()" (input)="update()" (change)="update()" [type]="inputType" class="form-control" 5 #input (click)="input.select()" (input)="update()" (change)="update()" [type]="inputType" class="form-control"
6 /> 6 />
7 7
8 <div *ngIf="withToggle || withCopy" class="input-group-append"> 8 <button *ngIf="withToggle" (click)="toggle()" type="button" class="btn btn-outline-secondary eye-button" [title]="toggleTitle">
9 <button *ngIf="withToggle" (click)="toggle()" type="button" class="btn btn-outline-secondary eye-button" [title]="toggleTitle"> 9 <span class="glyphicon glyphicon-eye-{{ show ? 'open' : 'close' }}"></span>
10 <span class="glyphicon glyphicon-eye-{{ show ? 'open' : 'close' }}"></span> 10 </button>
11 </button>
12 11
13 <button 12 <button
14 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button" 13 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button"
15 class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy" 14 class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy"
16 > 15 >
17 <span class="glyphicon glyphicon-duplicate"></span> 16 <span class="glyphicon glyphicon-duplicate"></span>
18 Copy 17 <span class="copy-text">Copy</span>
19 </button> 18 </button>
20 </div>
21</div> 19</div>
diff --git a/client/src/app/shared/shared-forms/input-toggle-hidden.component.scss b/client/src/app/shared/shared-forms/input-toggle-hidden.component.scss
index ef4236ebc..02a8d7d0e 100644
--- a/client/src/app/shared/shared-forms/input-toggle-hidden.component.scss
+++ b/client/src/app/shared/shared-forms/input-toggle-hidden.component.scss
@@ -5,11 +5,14 @@ input {
5 @include peertube-input-text(auto); 5 @include peertube-input-text(auto);
6 @include padding-left(15px !important); 6 @include padding-left(15px !important);
7 @include padding-right(15px !important); 7 @include padding-right(15px !important);
8}
8 9
9 // set again properties of peertube-input-text that are overriden by .input-group 10.btn {
10 font-size: 15px !important; 11 font-size: 15px;
11} 12}
12 13
13.eye-button { 14.copy-text {
14 line-height: 1 !important; 15 font-size: 14px;
16 margin-left: 5px;
17 vertical-align: top;
15} 18}
diff --git a/client/src/app/shared/shared-forms/peertube-checkbox.component.html b/client/src/app/shared/shared-forms/peertube-checkbox.component.html
index c679e1403..38a48a2a5 100644
--- a/client/src/app/shared/shared-forms/peertube-checkbox.component.html
+++ b/client/src/app/shared/shared-forms/peertube-checkbox.component.html
@@ -1,6 +1,6 @@
1<div class="root flex-column"> 1<div class="root flex-column">
2 <div class="d-flex"> 2 <div class="d-flex">
3 <label class="form-group-checkbox"> 3 <label>
4 <input 4 <input
5 type="checkbox" 5 type="checkbox"
6 [(ngModel)]="checked" 6 [(ngModel)]="checked"
@@ -33,7 +33,7 @@
33 <div *ngIf="recommended" class="recommended" i18n>Recommended</div> 33 <div *ngIf="recommended" class="recommended" i18n>Recommended</div>
34 </div> 34 </div>
35 35
36 <div class="ml-4 d-flex flex-column"> 36 <div class="ms-4 d-flex flex-column">
37 <small class="wrapper mt-2 muted"> 37 <small class="wrapper mt-2 muted">
38 <ng-content select="description"></ng-content> 38 <ng-content select="description"></ng-content>
39 </small> 39 </small>
diff --git a/client/src/app/shared/shared-forms/peertube-checkbox.component.scss b/client/src/app/shared/shared-forms/peertube-checkbox.component.scss
index 4e384e7e0..5fe20c3a5 100644
--- a/client/src/app/shared/shared-forms/peertube-checkbox.component.scss
+++ b/client/src/app/shared/shared-forms/peertube-checkbox.component.scss
@@ -4,7 +4,7 @@
4.root { 4.root {
5 display: flex; 5 display: flex;
6 6
7 .form-group-checkbox { 7 label {
8 display: flex; 8 display: flex;
9 align-items: center; 9 align-items: center;
10 10
diff --git a/client/src/app/shared/shared-forms/reactive-file.component.ts b/client/src/app/shared/shared-forms/reactive-file.component.ts
index 50b7d4c3e..48055a51c 100644
--- a/client/src/app/shared/shared-forms/reactive-file.component.ts
+++ b/client/src/app/shared/shared-forms/reactive-file.component.ts
@@ -57,7 +57,7 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
57 57
58 const extension = '.' + file.name.split('.').pop() 58 const extension = '.' + file.name.split('.').pop()
59 if (this.extensions.includes(extension.toLowerCase()) === false) { 59 if (this.extensions.includes(extension.toLowerCase()) === false) {
60 const message = $localize`PeerTube cannot handle this kind of file. Accepted extensions are ${this.allowedExtensionsMessage}}.` 60 const message = $localize`PeerTube cannot handle this kind of file. Accepted extensions are ${this.allowedExtensionsMessage}.`
61 this.notifier.error(message) 61 this.notifier.error(message)
62 62
63 return 63 return
diff --git a/client/src/app/shared/shared-forms/select/select-channel.component.html b/client/src/app/shared/shared-forms/select/select-channel.component.html
index f83f17a16..b49fd36fa 100644
--- a/client/src/app/shared/shared-forms/select/select-channel.component.html
+++ b/client/src/app/shared/shared-forms/select/select-channel.component.html
@@ -7,7 +7,7 @@
7 [searchable]="searchable" 7 [searchable]="searchable"
8> 8>
9 <ng-option *ngFor="let channel of channels" [value]="channel.id"> 9 <ng-option *ngFor="let channel of channels" [value]="channel.id">
10 <img alt="" class="avatar mr-1" [src]="channel.avatarPath" /> 10 <img alt="" class="avatar me-1" [src]="channel.avatarPath" />
11 {{ channel.label }} 11 {{ channel.label }}
12 </ng-option> 12 </ng-option>
13</ng-select> 13</ng-select>
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 ebf7b77a6..2c3226f68 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,6 +1,7 @@
1import { Component, forwardRef, Input } from '@angular/core' 1import { Component, forwardRef, Input } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { prepareIcu } from '@app/helpers'
4import { SelectOptionsItem } from '../../../../types/select-options-item.model' 5import { SelectOptionsItem } from '../../../../types/select-options-item.model'
5import { ItemSelectCheckboxValue } from './select-checkbox.component' 6import { ItemSelectCheckboxValue } from './select-checkbox.component'
6 7
@@ -78,7 +79,12 @@ export class SelectCheckboxAllComponent implements ControlValueAccessor {
78 if (!outputItems) return true 79 if (!outputItems) return true
79 80
80 if (outputItems.length >= this.maxItems) { 81 if (outputItems.length >= this.maxItems) {
81 this.notifier.error($localize`You can't select more than ${this.maxItems} items`) 82 this.notifier.error(
83 prepareIcu($localize`You can't select more than {maxItems, plural, =1 {1 item} other {{maxItems} items}}`)(
84 { maxItems: this.maxItems },
85 $localize`You can't select more than ${this.maxItems} items`
86 )
87 )
82 88
83 return false 89 return false
84 } 90 }
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox.component.html b/client/src/app/shared/shared-forms/select/select-checkbox.component.html
index 03db2875b..2799ccdcc 100644
--- a/client/src/app/shared/shared-forms/select/select-checkbox.component.html
+++ b/client/src/app/shared/shared-forms/select/select-checkbox.component.html
@@ -22,7 +22,7 @@
22> 22>
23 23
24 <ng-template ng-optgroup-tmp let-item="item" let-item$="item$" let-index="index"> 24 <ng-template ng-optgroup-tmp let-item="item" let-item$="item$" let-index="index">
25 <div class="form-group-checkbox"> 25 <div class="checkbox-wrapper">
26 <input id="item-{{index}}" type="checkbox" [ngModel]="item$.selected"/> 26 <input id="item-{{index}}" type="checkbox" [ngModel]="item$.selected"/>
27 <span role="checkbox" [attr.aria-checked]="item$.selected"></span> 27 <span role="checkbox" [attr.aria-checked]="item$.selected"></span>
28 <span>{{ item.group }}</span> 28 <span>{{ item.group }}</span>
@@ -30,7 +30,7 @@
30 </ng-template> 30 </ng-template>
31 31
32 <ng-template ng-option-tmp let-item="item" let-item$="item$" let-index="index"> 32 <ng-template ng-option-tmp let-item="item" let-item$="item$" let-index="index">
33 <div class="form-group-checkbox"> 33 <div class="checkbox-wrapper">
34 <input id="item-{{index}}" type="checkbox" [ngModel]="item$.selected"/> 34 <input id="item-{{index}}" type="checkbox" [ngModel]="item$.selected"/>
35 <span role="checkbox" [attr.aria-checked]="item$.selected"></span> 35 <span role="checkbox" [attr.aria-checked]="item$.selected"></span>
36 <span>{{ item.label }}</span> 36 <span>{{ item.label }}</span>
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox.component.scss b/client/src/app/shared/shared-forms/select/select-checkbox.component.scss
index d47c4f9da..892f22dff 100644
--- a/client/src/app/shared/shared-forms/select/select-checkbox.component.scss
+++ b/client/src/app/shared/shared-forms/select/select-checkbox.component.scss
@@ -7,7 +7,7 @@ ng-select ::ng-deep {
7 align-items: center; 7 align-items: center;
8 } 8 }
9 9
10 .form-group-checkbox { 10 .checkbox-wrapper {
11 display: flex; 11 display: flex;
12 align-items: center; 12 align-items: center;
13 13
diff --git a/client/src/app/shared/shared-instance/instance-about-accordion.component.scss b/client/src/app/shared/shared-instance/instance-about-accordion.component.scss
index be6099a97..8e5dfb064 100644
--- a/client/src/app/shared/shared-instance/instance-about-accordion.component.scss
+++ b/client/src/app/shared/shared-instance/instance-about-accordion.component.scss
@@ -16,26 +16,3 @@
16 font-size: 15px; 16 font-size: 15px;
17 margin-bottom: 15px; 17 margin-bottom: 15px;
18} 18}
19
20ngb-accordion ::ng-deep {
21 .card {
22 border-color: var(--mainBackgroundColor);
23
24 .card-header {
25 background-color: unset;
26 padding: 0;
27
28 + .collapse.show {
29 background-color: var(--submenuBackgroundColor);
30 }
31 }
32 }
33
34 .btn {
35 @include peertube-button;
36 @include grey-button;
37
38 border-radius: unset;
39 width: 100%;
40 }
41}
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.html b/client/src/app/shared/shared-instance/instance-features-table.component.html
index 1fdef95ff..761243bfe 100644
--- a/client/src/app/shared/shared-instance/instance-features-table.component.html
+++ b/client/src/app/shared/shared-instance/instance-features-table.component.html
@@ -1,6 +1,6 @@
1<div *ngIf="serverConfig" class="feature-table"> 1<div *ngIf="serverConfig" class="feature-table">
2 2
3 <table class="table" *ngIf="serverConfig"> 3 <table *ngIf="serverConfig">
4 <caption i18n>Features found on this instance</caption> 4 <caption i18n>Features found on this instance</caption>
5 <tr> 5 <tr>
6 <th i18n class="label" scope="row">PeerTube version</th> 6 <th i18n class="label" scope="row">PeerTube version</th>
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.scss b/client/src/app/shared/shared-instance/instance-features-table.component.scss
index 56ca105f4..105a7681f 100644
--- a/client/src/app/shared/shared-instance/instance-features-table.component.scss
+++ b/client/src/app/shared/shared-instance/instance-features-table.component.scss
@@ -4,6 +4,7 @@
4table { 4table {
5 font-size: 14px; 5 font-size: 14px;
6 color: pvar(--mainForegroundColor); 6 color: pvar(--mainForegroundColor);
7 width: 100%;
7 8
8 .label, 9 .label,
9 .sub-label { 10 .sub-label {
@@ -24,8 +25,10 @@ table {
24 } 25 }
25 } 26 }
26 27
28 th,
27 td { 29 td {
28 vertical-align: middle; 30 padding: 0.75rem;
31 border-top: 1px solid #dee2e6;
29 } 32 }
30 33
31 caption { 34 caption {
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 6335de450..e405c5790 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,5 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
3import { prepareIcu } from '@app/helpers'
3import { ServerConfig } from '@shared/models' 4import { ServerConfig } from '@shared/models'
4import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service' 5import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service'
5 6
@@ -65,15 +66,20 @@ export class InstanceFeaturesTableComponent implements OnInit {
65 66
66 private getApproximateTime (seconds: number) { 67 private getApproximateTime (seconds: number) {
67 const hours = Math.floor(seconds / 3600) 68 const hours = Math.floor(seconds / 3600)
68 let pluralSuffix = ''
69 if (hours > 1) pluralSuffix = 's'
70 if (hours > 0) return `~ ${hours} hour${pluralSuffix}`
71 69
72 const minutes = Math.floor(seconds % 3600 / 60) 70 if (hours !== 0) {
71 return prepareIcu($localize`~ {hours, plural, =1 {1 hour} other {{hours} hours}}`)(
72 { hours },
73 $localize`~ ${hours} hours`
74 )
75 }
73 76
74 if (minutes === 1) return $localize`~ 1 minute` 77 const minutes = Math.floor(seconds % 3600 / 60)
75 78
76 return $localize`~ ${minutes} minutes` 79 return prepareIcu($localize`~ {minutes, plural, =1 {1 minute} other {{minutes} minutes}}`)(
80 { minutes },
81 $localize`~ ${minutes} minutes`
82 )
77 } 83 }
78 84
79 private buildQuotaHelpIndication () { 85 private buildQuotaHelpIndication () {
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 d62c1f88e..dc6a25e83 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,37 +1,51 @@
1import { Pipe, PipeTransform } from '@angular/core' 1import { Pipe, PipeTransform } from '@angular/core'
2import { prepareIcu } from '@app/helpers'
2 3
3// 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
4@Pipe({ name: 'myFromNow' }) 5@Pipe({ name: 'myFromNow' })
5export class FromNowPipe implements PipeTransform { 6export 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
6 transform (arg: number | Date | string) { 13 transform (arg: number | Date | string) {
7 const argDate = new Date(arg) 14 const argDate = new Date(arg)
8 const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) 15 const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000)
9 16
10 let interval = Math.floor(seconds / 31536000) 17 let interval = Math.floor(seconds / 31536000)
11 if (interval > 1) return $localize`${interval} years ago` 18 if (interval >= 1) {
12 if (interval === 1) return $localize`1 year ago` 19 return this.yearICU({ interval }, $localize`${interval} year(s) ago`)
20 }
13 21
14 interval = Math.floor(seconds / 2419200) 22 interval = Math.floor(seconds / 2419200)
15 // 12 months = 360 days, but a year ~ 365 days 23 // 12 months = 360 days, but a year ~ 365 days
16 // Display "1 year ago" rather than "12 months ago" 24 // Display "1 year ago" rather than "12 months ago"
17 if (interval >= 12) return $localize`1 year ago` 25 if (interval >= 12) return $localize`1 year ago`
18 if (interval > 1) return $localize`${interval} months ago` 26
19 if (interval === 1) return $localize`1 month ago` 27 if (interval >= 1) {
28 return this.monthICU({ interval }, $localize`${interval} month(s) ago`)
29 }
20 30
21 interval = Math.floor(seconds / 604800) 31 interval = Math.floor(seconds / 604800)
22 // 4 weeks ~ 28 days, but our month is 30 days 32 // 4 weeks ~ 28 days, but our month is 30 days
23 // Display "1 month ago" rather than "4 weeks ago" 33 // Display "1 month ago" rather than "4 weeks ago"
24 if (interval >= 4) return $localize`1 month ago` 34 if (interval >= 4) return $localize`1 month ago`
25 if (interval > 1) return $localize`${interval} weeks ago` 35
26 if (interval === 1) return $localize`1 week ago` 36 if (interval >= 1) {
37 return this.weekICU({ interval }, $localize`${interval} week(s) ago`)
38 }
27 39
28 interval = Math.floor(seconds / 86400) 40 interval = Math.floor(seconds / 86400)
29 if (interval > 1) return $localize`${interval} days ago` 41 if (interval >= 1) {
30 if (interval === 1) return $localize`1 day ago` 42 return this.dayICU({ interval }, $localize`${interval} day(s) ago`)
43 }
31 44
32 interval = Math.floor(seconds / 3600) 45 interval = Math.floor(seconds / 3600)
33 if (interval > 1) return $localize`${interval} hours ago` 46 if (interval >= 1) {
34 if (interval === 1) return $localize`1 hour ago` 47 return this.hourICU({ interval }, $localize`${interval} hour(s) ago`)
48 }
35 49
36 interval = Math.floor(seconds / 60) 50 interval = Math.floor(seconds / 60)
37 if (interval >= 1) return $localize`${interval} min ago` 51 if (interval >= 1) return $localize`${interval} min ago`
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.html b/client/src/app/shared/shared-main/misc/simple-search-input.component.html
index 1e2f6c6a9..11f5a1ab0 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.html
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.html
@@ -1,5 +1,5 @@
1<div class="root"> 1<div class="root">
2 <div class="input-group has-feedback has-clear"> 2 <div class="input-group has-clear">
3 <input 3 <input
4 #ref 4 #ref
5 type="text" 5 type="text"
@@ -10,8 +10,7 @@
10 [placeholder]="placeholder" 10 [placeholder]="placeholder"
11 > 11 >
12 12
13 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="onResetFilter()"></a> 13 <a class="glyphicon glyphicon-remove-sign form-control-clear" title="Clear filter" i18n-title (click)="onResetFilter()"></a>
14 <span class="sr-only" i18n>Clear filters</span>
15 </div> 14 </div>
16 15
17 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon> 16 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon>
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
index 32376bf62..62bd94349 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
@@ -27,6 +27,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
27 videosCount?: number 27 videosCount?: number
28 28
29 viewsPerDay?: ViewsPerDate[] 29 viewsPerDay?: ViewsPerDate[]
30 totalViews?: number
30 31
31 static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) { 32 static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) {
32 return Actor.GET_ACTOR_AVATAR_URL(actor, size) 33 return Actor.GET_ACTOR_AVATAR_URL(actor, size)
@@ -74,6 +75,10 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
74 this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date) })) 75 this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date) }))
75 } 76 }
76 77
78 if (hash.totalViews !== null && hash.totalViews !== undefined) {
79 this.totalViews = hash.totalViews
80 }
81
77 if (hash.ownerAccount) { 82 if (hash.ownerAccount) {
78 this.ownerAccount = hash.ownerAccount 83 this.ownerAccount = hash.ownerAccount
79 this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host) 84 this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
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 022bb95ad..2e4ab87d7 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 @@
1import { AuthUser } from '@app/core' 1import { AuthUser } from '@app/core'
2import { User } from '@app/core/users/user.model' 2import { User } from '@app/core/users/user.model'
3import { durationToString, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers' 3import { durationToString, prepareIcu, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
4import { Actor } from '@app/shared/shared-main/account/actor.model' 4import { Actor } from '@app/shared/shared-main/account/actor.model'
5import { buildVideoWatchPath } from '@shared/core-utils' 5import { buildVideoWatchPath } from '@shared/core-utils'
6import { peertubeTranslate } from '@shared/core-utils/i18n' 6import { peertubeTranslate } from '@shared/core-utils/i18n'
@@ -19,6 +19,9 @@ import {
19} from '@shared/models' 19} from '@shared/models'
20 20
21export class Video implements VideoServerModel { 21export 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
22 byVideoChannel: string 25 byVideoChannel: string
23 byAccount: string 26 byAccount: string
24 27
@@ -269,12 +272,10 @@ export class Video implements VideoServerModel {
269 } 272 }
270 273
271 getExactNumberOfViews () { 274 getExactNumberOfViews () {
272 if (this.views < 1000) return ''
273
274 if (this.isLive) { 275 if (this.isLive) {
275 return $localize`${this.views} viewers` 276 return Video.viewersICU({ viewers: this.viewers }, $localize`${this.viewers} viewer(s)`)
276 } 277 }
277 278
278 return $localize`${this.views} views` 279 return Video.viewsICU({ views: this.views }, $localize`{${this.views} view(s)}`)
279 } 280 }
280} 281}
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
index feac707c2..fd3709676 100644
--- a/client/src/app/shared/shared-moderation/account-block-badges.component.html
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.html
@@ -1,4 +1,4 @@
1<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> 1<span *ngIf="account.mutedByUser" class="pt-badge badge-danger" i18n>Muted</span>
2<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span> 2<span *ngIf="account.mutedServerByUser" class="pt-badge badge-danger" i18n>Instance muted</span>
3<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span> 3<span *ngIf="account.mutedByInstance" class="pt-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> 4<span *ngIf="account.mutedServerByInstance" class="pt-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
index ccc3666aa..301d8305e 100644
--- a/client/src/app/shared/shared-moderation/account-block-badges.component.scss
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.scss
@@ -1,9 +1,8 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.badge { 4.pt-badge {
5 @include margin-right(10px); 5 @include margin-right(10px);
6 6
7 height: fit-content;
8 font-size: 12px; 7 font-size: 12px;
9} 8}
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.html b/client/src/app/shared/shared-moderation/account-blocklist.component.html
index a4f81d824..15632f654 100644
--- a/client/src/app/shared/shared-moderation/account-blocklist.component.html
+++ b/client/src/app/shared/shared-moderation/account-blocklist.component.html
@@ -11,7 +11,7 @@
11> 11>
12 <ng-template pTemplate="caption"> 12 <ng-template pTemplate="caption">
13 <div class="caption"> 13 <div class="caption">
14 <div class="ml-auto"> 14 <div class="ms-auto">
15 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 15 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
16 </div> 16 </div>
17 </div> 17 </div>
diff --git a/client/src/app/shared/shared-moderation/report-modals/report.component.html b/client/src/app/shared/shared-moderation/report-modals/report.component.html
index 6c99180ef..8e0b0993c 100644
--- a/client/src/app/shared/shared-moderation/report-modals/report.component.html
+++ b/client/src/app/shared/shared-moderation/report-modals/report.component.html
@@ -8,11 +8,11 @@
8 <form novalidate [formGroup]="form" (ngSubmit)="report()"> 8 <form novalidate [formGroup]="form" (ngSubmit)="report()">
9 9
10 <div class="row"> 10 <div class="row">
11 <div class="col-5 form-group"> 11 <div class="col-5">
12 12
13 <label i18n for="reportPredefinedReasons">What is the issue?</label> 13 <label i18n for="reportPredefinedReasons">What is the issue?</label>
14 14
15 <div class="ml-2 mt-2 d-flex flex-column"> 15 <div class="ms-2 mt-2 d-flex flex-column">
16 <ng-container formGroupName="predefinedReasons"> 16 <ng-container formGroupName="predefinedReasons">
17 17
18 <div class="form-group" *ngFor="let reason of predefinedReasons"> 18 <div class="form-group" *ngFor="let reason of predefinedReasons">
@@ -29,7 +29,6 @@
29 29
30 </ng-container> 30 </ng-container>
31 </div> 31 </div>
32
33 </div> 32 </div>
34 33
35 <div class="col-7"> 34 <div class="col-7">
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.html b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
index afac108fc..51ca0b9d6 100644
--- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
@@ -8,38 +8,32 @@
8 <form novalidate [formGroup]="form" (ngSubmit)="report()"> 8 <form novalidate [formGroup]="form" (ngSubmit)="report()">
9 9
10 <div class="row"> 10 <div class="row">
11 <div class="col-5 form-group"> 11 <div class="col-12 col-md-5">
12 <label i18n for="reportPredefinedReasons">What is the issue?</label>
12 13
13 <label i18n for="reportPredefinedReasons">What is the issue?</label> 14 <div class="ms-2 mt-2 d-flex flex-column">
15 <ng-container formGroupName="predefinedReasons">
14 16
15 <div class="ml-2 mt-2 d-flex flex-column"> 17 <div class="form-group" *ngFor="let reason of predefinedReasons">
16 <ng-container formGroupName="predefinedReasons"> 18 <my-peertube-checkbox [inputName]="reason.id" [formControlName]="reason.id" [labelText]="reason.label">
19 <ng-template *ngIf="reason.help" ptTemplate="help">
20 <div [innerHTML]="reason.help"></div>
21 </ng-template>
17 22
18 <div class="form-group" *ngFor="let reason of predefinedReasons"> 23 <ng-container *ngIf="reason.description" ngProjectAs="description">
19 <my-peertube-checkbox [inputName]="reason.id" [formControlName]="reason.id" [labelText]="reason.label"> 24 <div [innerHTML]="reason.description"></div>
20 <ng-template *ngIf="reason.help" ptTemplate="help"> 25 </ng-container>
21 <div [innerHTML]="reason.help"></div> 26 </my-peertube-checkbox>
22 </ng-template> 27 </div>
23
24 <ng-container *ngIf="reason.description" ngProjectAs="description">
25 <div [innerHTML]="reason.description"></div>
26 </ng-container>
27 </my-peertube-checkbox>
28 </div>
29
30 </ng-container>
31 </div>
32 28
29 </ng-container>
30 </div>
33 </div> 31 </div>
34 32
35 <div class="col-7"> 33 <div class="col-12 col-md-7">
36 <div class="row justify-content-center"> 34 <my-embed [video]="video"></my-embed>
37 <div class="col-12 col-lg-9 mb-2">
38 <my-embed [video]="video"></my-embed>
39 </div>
40 </div>
41 35
42 <div class="mb-1 start-at" formGroupName="timestamp"> 36 <div class="mb-1 mt-3 start-at" formGroupName="timestamp">
43 <my-peertube-checkbox 37 <my-peertube-checkbox
44 formControlName="hasStart" 38 formControlName="hasStart"
45 i18n-labelText labelText="Start at" 39 i18n-labelText labelText="Start at"
diff --git a/client/src/app/shared/shared-moderation/server-blocklist.component.html b/client/src/app/shared/shared-moderation/server-blocklist.component.html
index 1a320e9a4..ba4336e2e 100644
--- a/client/src/app/shared/shared-moderation/server-blocklist.component.html
+++ b/client/src/app/shared/shared-moderation/server-blocklist.component.html
@@ -19,7 +19,7 @@
19 </a> 19 </a>
20 </div> 20 </div>
21 21
22 <div class="ml-auto"> 22 <div class="ms-auto">
23 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 23 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
24 </div> 24 </div>
25 </div> 25 </div>
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 9edfac388..8b483499a 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,6 +1,7 @@
1import { forkJoin } from 'rxjs' 1import { forkJoin } from 'rxjs'
2import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 2import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { prepareIcu } from '@app/helpers'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -63,9 +64,16 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
63 forkJoin(observables) 64 forkJoin(observables)
64 .subscribe({ 65 .subscribe({
65 next: () => { 66 next: () => {
66 const message = Array.isArray(this.usersToBan) 67 let message: string
67 ? $localize`${this.usersToBan.length} users banned.` 68
68 : $localize`User ${this.usersToBan.username} banned.` 69 if (Array.isArray(this.usersToBan)) {
70 message = prepareIcu($localize`{count, plural, =1 {1 user} other {{count} users}} banned.`)(
71 { count: this.usersToBan.length },
72 $localize`${this.usersToBan.length} users banned.`
73 )
74 } else {
75 message = $localize`User ${this.usersToBan.username} banned.`
76 }
69 77
70 this.notifier.success(message) 78 this.notifier.success(message)
71 79
@@ -79,7 +87,12 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
79 } 87 }
80 88
81 getModalTitle () { 89 getModalTitle () {
82 if (Array.isArray(this.usersToBan)) return $localize`Ban ${this.usersToBan.length} users` 90 if (Array.isArray(this.usersToBan)) {
91 return prepareIcu($localize`Ban {count, plural, =1 {1 user} other {{count} users}}`)(
92 { count: this.usersToBan.length },
93 $localize`Ban ${this.usersToBan.length} users`
94 )
95 }
83 96
84 return $localize`Ban "${this.usersToBan.username}"` 97 return $localize`Ban "${this.usersToBan.username}"`
85 } 98 }
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 787318c2c..c69a45c25 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
@@ -100,7 +100,8 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
100 return 100 return
101 } 101 }
102 102
103 const message = $localize`If you remove user ${user.username}, you won't be able to create another with the same username!` 103 // eslint-disable-next-line max-len
104 const message = $localize`If you remove this user, you won't be able to create another user or channel with <strong>${user.username}</strong> username!`
104 const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`) 105 const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`)
105 if (res === false) return 106 if (res === false) return
106 107
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 400913f02..e14473b89 100644
--- a/client/src/app/shared/shared-moderation/video-block.component.ts
+++ b/client/src/app/shared/shared-moderation/video-block.component.ts
@@ -1,5 +1,6 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { prepareIcu } from '@app/helpers'
3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { Video } from '@app/shared/shared-main' 5import { Video } from '@app/shared/shared-main'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -80,9 +81,10 @@ export class VideoBlockComponent extends FormReactive implements OnInit {
80 this.videoBlocklistService.blockVideo(options) 81 this.videoBlocklistService.blockVideo(options)
81 .subscribe({ 82 .subscribe({
82 next: () => { 83 next: () => {
83 const message = this.isMultiple 84 const message = prepareIcu($localize`{count, plural, =1 {Blocked {videoName}} other {Blocked {count} videos}}.`)(
84 ? $localize`Blocked ${this.videos.length} videos.` 85 { count: this.videos.length, videoName: this.getSingleVideo().name },
85 : $localize`Blocked ${this.getSingleVideo().name}` 86 $localize`Blocked ${this.videos.length} videos.`
87 )
86 88
87 this.notifier.success(message) 89 this.notifier.success(message)
88 this.hide() 90 this.hide()
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 67ca56516..a6b8576a1 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
@@ -171,9 +171,8 @@
171 </div> 171 </div>
172 </div> 172 </div>
173 173
174 <div class="form-group"> 174 <div class="form-group" *ngIf="isInVideoEmbedTab()">
175 <my-peertube-checkbox 175 <my-peertube-checkbox
176 *ngIf="isInVideoEmbedTab()"
177 inputName="onlyEmbedUrl" [(ngModel)]="customizations.onlyEmbedUrl" 176 inputName="onlyEmbedUrl" [(ngModel)]="customizations.onlyEmbedUrl"
178 i18n-labelText labelText="Only display embed URL" 177 i18n-labelText labelText="Only display embed URL"
179 ></my-peertube-checkbox> 178 ></my-peertube-checkbox>
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.scss b/client/src/app/shared/shared-share-modal/video-share.component.scss
index 44ebb13f4..abb78b09b 100644
--- a/client/src/app/shared/shared-share-modal/video-share.component.scss
+++ b/client/src/app/shared/shared-share-modal/video-share.component.scss
@@ -55,13 +55,6 @@ my-input-toggle-hidden {
55 } 55 }
56 } 56 }
57 57
58 .form-group {
59 margin-bottom: 0;
60 height: 34px;
61 display: flex;
62 align-items: center;
63 }
64
65 .video-caption-block { 58 .video-caption-block {
66 display: flex; 59 display: flex;
67 align-items: center; 60 align-items: center;
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.html b/client/src/app/shared/shared-user-settings/user-video-settings.component.html
index 446ade445..ccd7f8f4b 100644
--- a/client/src/app/shared/shared-user-settings/user-video-settings.component.html
+++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.html
@@ -1,5 +1,5 @@
1<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form"> 1<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
2 <div class="form-group form-group-select"> 2 <div class="form-group">
3 <div class="anchor" id="video-sensitive-content-policy"></div> <!-- video-sensitive-content-policy anchor --> 3 <div class="anchor" id="video-sensitive-content-policy"></div> <!-- video-sensitive-content-policy anchor -->
4 <label i18n for="nsfwPolicy">Default policy on videos containing sensitive content</label> 4 <label i18n for="nsfwPolicy">Default policy on videos containing sensitive content</label>
5 <my-help> 5 <my-help>
@@ -20,7 +20,7 @@
20 </div> 20 </div>
21 </div> 21 </div>
22 22
23 <div class="form-group form-group-select"> 23 <div class="form-group">
24 <div class="anchor" id="video-languages-subtitles"></div> <!-- video-languages-subtitles anchor --> 24 <div class="anchor" id="video-languages-subtitles"></div> <!-- video-languages-subtitles anchor -->
25 <label i18n for="videoLanguages">Only display videos in the following languages/subtitles</label> 25 <label i18n for="videoLanguages">Only display videos in the following languages/subtitles</label>
26 <my-help> 26 <my-help>
@@ -30,7 +30,7 @@
30 </my-help> 30 </my-help>
31 31
32 <div> 32 <div>
33 <my-select-languages formControlName="videoLanguages"></my-select-languages> 33 <my-select-languages [maxLanguages]="20" formControlName="videoLanguages"></my-select-languages>
34 </div> 34 </div>
35 </div> 35 </div>
36 36
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.scss b/client/src/app/shared/shared-user-settings/user-video-settings.component.scss
index c4f6020d4..a39e7cf1f 100644
--- a/client/src/app/shared/shared-user-settings/user-video-settings.component.scss
+++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.scss
@@ -15,8 +15,6 @@ input[type=submit] {
15 15
16.peertube-select-container { 16.peertube-select-container {
17 @include peertube-select-container(340px); 17 @include peertube-select-container(340px);
18
19 margin-bottom: 30px;
20} 18}
21 19
22my-select-languages { 20my-select-languages {
@@ -24,7 +22,3 @@ my-select-languages {
24 22
25 display: block; 23 display: block;
26} 24}
27
28.form-group-select {
29 margin-bottom: 30px;
30}
diff --git a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.html b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.html
index a00c3d1c7..656d1beb3 100644
--- a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.html
+++ b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.html
@@ -1,5 +1,5 @@
1<form novalidate [formGroup]="form" (ngSubmit)="formValidated()"> 1<form novalidate [formGroup]="form" (ngSubmit)="formValidated()">
2 <div class="form-group mb-2"> 2 <div class="form-group">
3 <input type="email" 3 <input type="email"
4 formControlName="text" 4 formControlName="text"
5 class="form-control" 5 class="form-control"
diff --git a/client/src/app/shared/shared-video-live/live-stream-information.component.html b/client/src/app/shared/shared-video-live/live-stream-information.component.html
index 01e305938..28396915a 100644
--- a/client/src/app/shared/shared-video-live/live-stream-information.component.html
+++ b/client/src/app/shared/shared-video-live/live-stream-information.component.html
@@ -7,8 +7,8 @@
7 7
8 <div class="modal-body" *ngIf="live"> 8 <div class="modal-body" *ngIf="live">
9 <div> 9 <div>
10 <div class="badge badge-info" *ngIf="live.permanentLive" i18n>Permanent/Recurring live</div> 10 <div class="pt-badge badge-blue" *ngIf="live.permanentLive" i18n>Permanent/Recurring live</div>
11 <div class="badge badge-info" *ngIf="live.saveReplay" i18n>Replay will be saved</div> 11 <div class="pt-badge badge-blue" *ngIf="live.saveReplay" i18n>Replay will be saved</div>
12 </div> 12 </div>
13 13
14 <div class="alert alert-info"> 14 <div class="alert alert-info">
@@ -36,8 +36,8 @@
36 <label i18n>Latest live sessions</label> 36 <label i18n>Latest live sessions</label>
37 37
38 <div class="journal-session" *ngFor="let session of latestLiveSessions"> 38 <div class="journal-session" *ngFor="let session of latestLiveSessions">
39 <span i18n class="badge badge-success" *ngIf="!getErrorLabel(session)">Success</span> 39 <span i18n class="pt-badge badge-success" *ngIf="!getErrorLabel(session)">Success</span>
40 <span class="badge badge-danger" *ngIf="getErrorLabel(session)">{{ getErrorLabel(session) }}</span> 40 <span class="pt-badge badge-danger" *ngIf="getErrorLabel(session)">{{ getErrorLabel(session) }}</span>
41 41
42 <span i18n>Started on {{ session.startDate | date:'medium' }}</span> 42 <span i18n>Started on {{ session.startDate | date:'medium' }}</span>
43 <span i18n *ngIf="session.endDate">Ended on {{ session.endDate | date:'medium' }}</span> 43 <span i18n *ngIf="session.endDate">Ended on {{ session.endDate | date:'medium' }}</span>
diff --git a/client/src/app/shared/shared-video-live/live-stream-information.component.scss b/client/src/app/shared/shared-video-live/live-stream-information.component.scss
index 9c8ad12bd..fc0b1cea2 100644
--- a/client/src/app/shared/shared-video-live/live-stream-information.component.scss
+++ b/client/src/app/shared/shared-video-live/live-stream-information.component.scss
@@ -13,15 +13,16 @@ p-autocomplete {
13 margin: 1rem 0; 13 margin: 1rem 0;
14} 14}
15 15
16.badge { 16.pt-badge {
17 @include margin-right(5px);
18
17 font-size: 13px; 19 font-size: 13px;
18 margin-right: 5px;
19} 20}
20 21
21.journal-session { 22.journal-session {
22 margin-bottom: 5px; 23 margin-bottom: 5px;
23 24
24 span:not(.badge, :last-child)::after { 25 span:not(.pt-badge, :last-child)::after {
25 margin: 3px; 26 margin: 3px;
26 content: '•'; 27 content: '•';
27 } 28 }
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.html b/client/src/app/shared/shared-video-miniature/video-download.component.html
index b50544057..9123d2a5a 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.html
@@ -28,14 +28,15 @@
28 28
29 <ng-template ngbNavContent> 29 <ng-template ngbNavContent>
30 <div class="nav-content"> 30 <div class="nav-content">
31 <div class="input-group input-group-sm"> 31 <div class="input-group">
32 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> 32 <input #urlInput (click)="urlInput.select()" type="text" class="form-control readonly" readonly [value]="getLink()" />
33 33
34 <div class="input-group-append" *ngIf="!isConfidentialVideo()"> 34 <button
35 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> 35 *ngIf="!isConfidentialVideo()" type="button" class="btn btn-outline-secondary"
36 <span class="glyphicon glyphicon-duplicate"></span> 36 [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()"
37 </button> 37 >
38 </div> 38 <span class="glyphicon glyphicon-duplicate"></span>
39 </button>
39 </div> 40 </div>
40 </div> 41 </div>
41 </ng-template> 42 </ng-template>
@@ -53,13 +54,15 @@
53 54
54 <ng-template ngbNavContent> 55 <ng-template ngbNavContent>
55 <div class="nav-content"> 56 <div class="nav-content">
56 <div class="input-group input-group-sm"> 57 <div class="input-group">
57 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> 58 <input #urlInput (click)="urlInput.select()" type="text" class="form-control readonly" readonly [value]="getLink()" />
58 <div class="input-group-append" *ngIf="!isConfidentialVideo()"> 59
59 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> 60 <button
60 <span class="glyphicon glyphicon-duplicate"></span> 61 *ngIf="!isConfidentialVideo()" type="button" class="btn btn-outline-secondary"
61 </button> 62 [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()"
62 </div> 63 >
64 <span class="glyphicon glyphicon-duplicate"></span>
65 </button>
63 </div> 66 </div>
64 </div> 67 </div>
65 </ng-template> 68 </ng-template>
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.scss b/client/src/app/shared/shared-video-miniature/video-download.component.scss
index bd42f4813..04d5eb560 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.scss
@@ -5,6 +5,13 @@
5 margin-top: 30px; 5 margin-top: 30px;
6} 6}
7 7
8.input-group > input {
9 @include peertube-input-text(auto);
10
11 font-size: 14px;
12 padding: 0 5px;
13}
14
8.advanced-filters-button { 15.advanced-filters-button {
9 display: flex; 16 display: flex;
10 justify-content: center; 17 justify-content: center;
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 a07b8b5ee..fe7a59bdb 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
@@ -44,6 +44,7 @@
44 [searchable]="false" 44 [searchable]="false"
45 > 45 >
46 <ng-option i18n value="-publishedAt">Sort by <strong>"Recently Added"</strong></ng-option> 46 <ng-option i18n value="-publishedAt">Sort by <strong>"Recently Added"</strong></ng-option>
47 <ng-option i18n value="-originallyPublishedAt">Sort by <strong>"Original Publication Date"</strong></ng-option>
47 48
48 <ng-option i18n *ngIf="isTrendingSortEnabled('most-viewed')" value="-trending">Sort by <strong>"Recent Views"</strong></ng-option> 49 <ng-option i18n *ngIf="isTrendingSortEnabled('most-viewed')" value="-trending">Sort by <strong>"Recent Views"</strong></ng-option>
49 <ng-option i18n *ngIf="isTrendingSortEnabled('hot')" value="-hot">Sort by <strong>"Hot"</strong></ng-option> 50 <ng-option i18n *ngIf="isTrendingSortEnabled('hot')" value="-hot">Sort by <strong>"Hot"</strong></ng-option>
diff --git a/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss b/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss
index 8cb1ff5b8..6a968ed5c 100644
--- a/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss
@@ -101,7 +101,7 @@
101} 101}
102 102
103.sort { 103.sort {
104 min-width: 200px; 104 min-width: 250px;
105 max-width: 300px; 105 max-width: 300px;
106 height: min-content; 106 height: min-content;
107 107
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 42c472579..534a78b3f 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
@@ -175,7 +175,7 @@ export class VideoMiniatureComponent implements OnInit {
175 175
176 if (video.scheduledUpdate) { 176 if (video.scheduledUpdate) {
177 const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId) 177 const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId)
178 return $localize`Publication scheduled on ` + updateAt 178 return $localize`Publication scheduled on ${updateAt}`
179 } 179 }
180 180
181 if (video.state.id === VideoState.TRANSCODING_FAILED) { 181 if (video.state.id === VideoState.TRANSCODING_FAILED) {
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.html b/client/src/app/shared/shared-video-miniature/videos-list.component.html
index 2b554517f..c220f61f1 100644
--- a/client/src/app/shared/shared-video-miniature/videos-list.component.html
+++ b/client/src/app/shared/shared-video-miniature/videos-list.component.html
@@ -12,15 +12,15 @@
12 12
13 <div class="action-block"> 13 <div class="action-block">
14 <ng-container *ngFor="let action of headerActions"> 14 <ng-container *ngFor="let action of headerActions">
15 <a *ngIf="action.routerLink" class="ml-2" [routerLink]="action.routerLink" routerLinkActive="active"> 15 <a *ngIf="action.routerLink" class="ms-2" [routerLink]="action.routerLink" routerLinkActive="active">
16 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container> 16 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container>
17 </a> 17 </a>
18 18
19 <a *ngIf="!action.routerLink && !action.href && action.click" class="ml-2" (click)="action.click($event)" (key.enter)="action.click($event)"> 19 <a *ngIf="!action.routerLink && !action.href && action.click" class="ms-2" (click)="action.click($event)" (key.enter)="action.click($event)">
20 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container> 20 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container>
21 </a> 21 </a>
22 22
23 <a *ngIf="!action.routerLink && action.href && action.click" class="ml-2" (click)="action.click($event)" (key.enter)="action.click($event)" [href]="action.href"> 23 <a *ngIf="!action.routerLink && action.href && action.click" class="ms-2" (click)="action.click($event)" (key.enter)="action.click($event)" [href]="action.href">
24 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container> 24 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container>
25 </a> 25 </a>
26 26
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 2400a4c25..f58d5f7f6 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,7 @@
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="badge badge-yellow">Private</span> 24 <span *ngIf="isVideoPrivate()" class="pt-badge badge-yellow">Private</span>
25 </div> 25 </div>
26 26
27 <span class="video-miniature-created-at-views"> 27 <span class="video-miniature-created-at-views">
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
index fbf67e892..c0cf2d1da 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
@@ -91,9 +91,8 @@ my-video-thumbnail,
91 padding-right: 5px; 91 padding-right: 5px;
92 } 92 }
93 93
94 .badge { 94 .pt-badge {
95 @include peertube-badge; 95 @include margin-right(5px);
96 margin-right: 5px;
97 } 96 }
98 } 97 }
99 98