aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.html2
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html2
-rw-r--r--client/src/app/+accounts/account-about/account-about.component.html15
-rw-r--r--client/src/app/+accounts/account-about/account-about.component.scss12
-rw-r--r--client/src/app/+accounts/account-about/account-about.component.ts40
-rw-r--r--client/src/app/+accounts/account-search/account-search.component.ts7
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.html43
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.scss172
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.ts47
-rw-r--r--client/src/app/+accounts/account-videos/account-videos.component.ts10
-rw-r--r--client/src/app/+accounts/accounts-routing.module.ts16
-rw-r--r--client/src/app/+accounts/accounts.component.html116
-rw-r--r--client/src/app/+accounts/accounts.component.scss184
-rw-r--r--client/src/app/+accounts/accounts.component.ts121
-rw-r--r--client/src/app/+accounts/accounts.module.ts4
-rw-r--r--client/src/app/+admin/admin.module.ts2
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts3
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.html6
-rw-r--r--client/src/app/+admin/plugins/shared/plugin-list.component.scss2
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html2
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.scss8
-rw-r--r--client/src/app/+login/login.component.html2
-rw-r--r--client/src/app/+login/login.component.ts7
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts8
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.html2
-rw-r--r--client/src/app/+my-account/my-account.module.ts6
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts70
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html21
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss7
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts7
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts48
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss3
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts4
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.html4
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.scss45
-rw-r--r--client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html2
-rw-r--r--client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss88
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html6
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss46
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html14
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss69
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.html12
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.scss94
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.ts14
-rw-r--r--client/src/app/+search/search.component.html14
-rw-r--r--client/src/app/+search/search.component.scss234
-rw-r--r--client/src/app/+video-channels/video-channel-about/video-channel-about.component.html22
-rw-r--r--client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss12
-rw-r--r--client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts43
-rw-r--r--client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html10
-rw-r--r--client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss28
-rw-r--r--client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts9
-rw-r--r--client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts25
-rw-r--r--client/src/app/+video-channels/video-channels-routing.module.ts10
-rw-r--r--client/src/app/+video-channels/video-channels.component.html147
-rw-r--r--client/src/app/+video-channels/video-channels.component.scss327
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts62
-rw-r--r--client/src/app/+video-channels/video-channels.module.ts6
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts13
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss4
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts13
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts15
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts15
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.scss4
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comments.component.html15
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comments.component.ts1
-rw-r--r--client/src/app/+videos/+video-watch/modal/video-support.component.ts31
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html4
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss33
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts2
-rw-r--r--client/src/app/+videos/+video-watch/video-avatar-channel.component.html (renamed from client/src/app/shared/shared-main/account/video-avatar-channel.component.html)5
-rw-r--r--client/src/app/+videos/+video-watch/video-avatar-channel.component.scss (renamed from client/src/app/shared/shared-main/account/video-avatar-channel.component.scss)8
-rw-r--r--client/src/app/+videos/+video-watch/video-avatar-channel.component.ts (renamed from client/src/app/shared/shared-main/account/video-avatar-channel.component.ts)2
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.html5
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.scss59
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts62
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.module.ts9
-rw-r--r--client/src/app/+videos/video-list/overview/video-overview.component.html6
-rw-r--r--client/src/app/+videos/video-list/overview/video-overview.component.scss79
-rw-r--r--client/src/app/+videos/video-list/video-user-subscriptions.component.ts3
-rw-r--r--client/src/app/app.component.scss9
-rw-r--r--client/src/app/core/notification/peertube-socket.service.ts7
-rw-r--r--client/src/app/core/plugins/hooks.service.ts18
-rw-r--r--client/src/app/core/plugins/plugin.service.ts6
-rw-r--r--client/src/app/core/server/server.service.ts6
-rw-r--r--client/src/app/core/users/user.model.ts4
-rw-r--r--client/src/app/core/users/user.service.ts5
-rw-r--r--client/src/app/core/wrappers/screen.service.ts9
-rw-r--r--client/src/app/header/search-typeahead.component.html4
-rw-r--r--client/src/app/header/search-typeahead.component.scss2
-rw-r--r--client/src/app/menu/menu.component.scss325
-rw-r--r--client/src/app/menu/menu.component.ts5
-rw-r--r--client/src/app/menu/notification.component.scss3
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.html2
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts3
-rw-r--r--client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html41
-rw-r--r--client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss54
-rw-r--r--client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts (renamed from client/src/app/shared/shared-main/account/actor-avatar-info.component.ts)42
-rw-r--r--client/src/app/shared/shared-actor-image/actor-banner-edit.component.html34
-rw-r--r--client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss27
-rw-r--r--client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts76
-rw-r--r--client/src/app/shared/shared-actor-image/actor-image-edit.scss35
-rw-r--r--client/src/app/shared/shared-actor-image/index.ts1
-rw-r--r--client/src/app/shared/shared-actor-image/shared-actor-image.module.ts29
-rw-r--r--client/src/app/shared/shared-forms/input-toggle-hidden.component.html5
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.scss2
-rw-r--r--client/src/app/shared/shared-forms/select/select-options.component.ts9
-rw-r--r--client/src/app/shared/shared-instance/instance-about-accordion.component.scss2
-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.ts10
-rw-r--r--client/src/app/shared/shared-main/account/account.model.ts4
-rw-r--r--client/src/app/shared/shared-main/account/actor-avatar-info.component.html43
-rw-r--r--client/src/app/shared/shared-main/account/actor-avatar-info.component.scss86
-rw-r--r--client/src/app/shared/shared-main/account/actor.model.ts19
-rw-r--r--client/src/app/shared/shared-main/account/index.ts2
-rw-r--r--client/src/app/shared/shared-main/angular/autofocus.directive.ts12
-rw-r--r--client/src/app/shared/shared-main/angular/index.ts1
-rw-r--r--client/src/app/shared/shared-main/auth/auth-interceptor.service.ts4
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.html13
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.scss36
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.ts52
-rw-r--r--client/src/app/shared/shared-main/peertube-modal/index.ts1
-rw-r--r--client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts7
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts17
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts33
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html48
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.ts9
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.model.ts53
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.service.ts12
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts6
-rw-r--r--client/src/app/shared/shared-moderation/moderation.scss2
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/report.component.scss2
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.ts3
-rw-r--r--client/src/app/shared/shared-share-modal/video-share.component.ts4
-rw-r--r--client/src/app/shared/shared-support-modal/index.ts3
-rw-r--r--client/src/app/shared/shared-support-modal/shared-support-modal.module.ts24
-rw-r--r--client/src/app/shared/shared-support-modal/support-modal.component.html (renamed from client/src/app/+videos/+video-watch/modal/video-support.component.html)4
-rw-r--r--client/src/app/shared/shared-support-modal/support-modal.component.scss (renamed from client/src/app/+videos/+video-watch/modal/video-support.component.scss)0
-rw-r--r--client/src/app/shared/shared-support-modal/support-modal.component.ts40
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.html5
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.scss11
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.ts8
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.html149
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.scss37
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.ts38
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.html8
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.scss305
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.ts35
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.html3
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.scss42
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.ts3
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss119
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts1
-rw-r--r--client/src/assets/images/feather/cloud-download.svg2
-rw-r--r--client/src/assets/images/feather/subscriptions.svg19
-rw-r--r--client/src/assets/images/misc/language.svg8
-rw-r--r--client/src/assets/images/misc/npm.svg2
-rw-r--r--client/src/assets/images/misc/peertube-x.svg25
-rw-r--r--client/src/assets/images/misc/playlist-add.svg4
-rw-r--r--client/src/assets/images/misc/support.svg4
-rw-r--r--client/src/assets/images/misc/video-lang.svg2
-rw-r--r--client/src/assets/player/peertube-player-local-storage.ts50
-rw-r--r--client/src/assets/player/peertube-player-manager.ts16
-rw-r--r--client/src/assets/player/peertube-plugin.ts13
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts2
-rw-r--r--client/src/assets/player/utils.ts5
-rw-r--r--client/src/sass/application.scss183
-rw-r--r--client/src/sass/bootstrap.scss4
-rw-r--r--client/src/sass/classes.scss22
-rw-r--r--client/src/sass/include/_actor.scss92
-rw-r--r--client/src/sass/include/_miniature.scss172
-rw-r--r--client/src/sass/include/_mixins.scss223
-rw-r--r--client/src/sass/include/_variables.scss44
-rw-r--r--client/src/sass/ng-select.scss14
-rw-r--r--client/src/sass/player/context-menu.scss2
-rw-r--r--client/src/sass/player/peertube-skin.scss18
-rw-r--r--client/src/sass/primeng-custom.scss57
-rw-r--r--client/src/standalone/videos/embed.ts6
-rw-r--r--client/src/types/register-client-option.model.ts3
180 files changed, 3479 insertions, 2326 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 2cf890acf..e9139b503 100644
--- a/client/src/app/+about/about-follows/about-follows.component.html
+++ b/client/src/app/+about/about-follows/about-follows.component.html
@@ -1,7 +1,7 @@
1<div class="row"> 1<div class="row">
2 <h1 class="sr-only" i18n>Follows</h1> 2 <h1 class="sr-only" i18n>Follows</h1>
3 <div class="col-xl-6 col-md-12"> 3 <div class="col-xl-6 col-md-12">
4 <h2 i18n class="subtitle">Followers instances ({{ followersPagination.totalItems }})</h2> 4 <h2 i18n class="subtitle">Follower instances ({{ followersPagination.totalItems }})</h2>
5 5
6 <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">This instance does not have instances followers.</div> 6 <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">This instance does not have instances followers.</div>
7 7
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 d8794d602..1f372090e 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -83,7 +83,7 @@
83 fragment="business-model" 83 fragment="business-model"
84 #anchorLink 84 #anchorLink
85 (click)="onClickCopyLink(anchorLink)"> 85 (click)="onClickCopyLink(anchorLink)">
86 <h3 i18n class="section-title">How we will pay for this instance</h3> 86 <h3 i18n class="section-title">How we will pay for keeping our instance running</h3>
87 </a> 87 </a>
88 88
89 <div [innerHTML]="html.businessModel"></div> 89 <div [innerHTML]="html.businessModel"></div>
diff --git a/client/src/app/+accounts/account-about/account-about.component.html b/client/src/app/+accounts/account-about/account-about.component.html
deleted file mode 100644
index e9e0e4079..000000000
--- a/client/src/app/+accounts/account-about/account-about.component.html
+++ /dev/null
@@ -1,15 +0,0 @@
1<h1 class="sr-only" i18n>About</h1>
2<div class="margin-content">
3 <div *ngIf="account" class="row no-gutters">
4 <div class="block col-md-6 col-sm-12 pr-2">
5 <h2 i18n class="small-title">DESCRIPTION</h2>
6 <div class="content" [innerHtml]="getAccountDescription()"></div>
7 </div>
8
9 <div class="block col-md-6 col-sm-12">
10 <h2 i18n class="small-title">STATS</h2>
11
12 <div i18n class="content">Joined {{ account.createdAt | date }}</div>
13 </div>
14 </div>
15</div>
diff --git a/client/src/app/+accounts/account-about/account-about.component.scss b/client/src/app/+accounts/account-about/account-about.component.scss
deleted file mode 100644
index 5bcd4b561..000000000
--- a/client/src/app/+accounts/account-about/account-about.component.scss
+++ /dev/null
@@ -1,12 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4.block {
5 margin-bottom: 40px;
6
7 .small-title {
8 @include in-content-small-title;
9
10 margin-bottom: 20px;
11 }
12}
diff --git a/client/src/app/+accounts/account-about/account-about.component.ts b/client/src/app/+accounts/account-about/account-about.component.ts
deleted file mode 100644
index 6cf846d72..000000000
--- a/client/src/app/+accounts/account-about/account-about.component.ts
+++ /dev/null
@@ -1,40 +0,0 @@
1import { Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { MarkdownService } from '@app/core'
4import { Account, AccountService } from '@app/shared/shared-main'
5
6@Component({
7 selector: 'my-account-about',
8 templateUrl: './account-about.component.html',
9 styleUrls: [ './account-about.component.scss' ]
10})
11export class AccountAboutComponent implements OnInit, OnDestroy {
12 account: Account
13 descriptionHTML = ''
14
15 private accountSub: Subscription
16
17 constructor (
18 private accountService: AccountService,
19 private markdownService: MarkdownService
20 ) { }
21
22 ngOnInit () {
23 // Parent get the account for us
24 this.accountSub = this.accountService.accountLoaded
25 .subscribe(async account => {
26 this.account = account
27 this.descriptionHTML = await this.markdownService.textMarkdownToHTML(this.account.description, true)
28 })
29 }
30
31 ngOnDestroy () {
32 if (this.accountSub) this.accountSub.unsubscribe()
33 }
34
35 getAccountDescription () {
36 if (this.descriptionHTML) return this.descriptionHTML
37
38 return $localize`No description`
39 }
40}
diff --git a/client/src/app/+accounts/account-search/account-search.component.ts b/client/src/app/+accounts/account-search/account-search.component.ts
index dda4bf0c7..f54ab846a 100644
--- a/client/src/app/+accounts/account-search/account-search.component.ts
+++ b/client/src/app/+accounts/account-search/account-search.component.ts
@@ -64,9 +64,14 @@ export class AccountSearchComponent extends AbstractVideoList implements OnInit,
64 } 64 }
65 65
66 updateSearch (value: string) { 66 updateSearch (value: string) {
67 if (value === '') this.router.navigate(['../videos'], { relativeTo: this.route })
68 this.search = value 67 this.search = value
69 68
69 if (!this.search) {
70 this.router.navigate([ '../videos' ], { relativeTo: this.route })
71 return
72 }
73
74 this.videos = []
70 this.reloadVideos() 75 this.reloadVideos()
71 } 76 }
72 77
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 5dbb341d2..19a4b3c9c 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,33 +1,50 @@
1<h1 class="sr-only" i18n>Video channels</h1> 1<h1 class="sr-only" i18n>Video channels</h1>
2
2<div class="margin-content"> 3<div class="margin-content">
3 4
4 <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div> 5 <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div>
5 6
6 <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onChannelDataSubject.asObservable()"> 7 <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onChannelDataSubject.asObservable()">
7 <div class="section channel" *ngFor="let videoChannel of videoChannels"> 8 <div class="channel" *ngFor="let videoChannel of videoChannels">
8 <div class="section-title"> 9
9 <a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel"> 10 <div class="channel-avatar-row">
11 <a class="avatar-link" [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel">
10 <img [src]="videoChannel.avatarUrl" alt="Avatar" /> 12 <img [src]="videoChannel.avatarUrl" alt="Avatar" />
13 </a>
11 14
12 <h2 class="section-title">{{ videoChannel.displayName }}</h2> 15 <h2>
16 <a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel">
17 {{ videoChannel.displayName }}
18 </a>
19 </h2>
20
21 <div class="actor-counters">
13 <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> 22 <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
14 </a>
15 23
16 <my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button> 24 <span class="videos-count" *ngIf="getTotalVideosOf(videoChannel) !== undefined" i18n>
25 {getTotalVideosOf(videoChannel), plural, =1 {1 videos} other {{{ getTotalVideosOf(videoChannel) }} videos}}
26 </span>
27 </div>
28
29 <div class="description-html" [innerHTML]="getChannelDescription(videoChannel)"></div>
17 </div> 30 </div>
18 31
19 <div *ngIf="getVideosOf(videoChannel)" class="videos"> 32 <my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button>
20 <div class="no-results my-5" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel doesn't have any videos.</div> 33
34 <a i18n class="button-show-channel peertube-button-link orange-button-inverted" [routerLink]="getVideoChannelLink(videoChannel)">Show this channel</a>
35
36 <div class="videos">
37 <div class="no-results" i18n *ngIf="getTotalVideosOf(videoChannel) === 0">This channel doesn't have any videos.</div>
21 38
22 <my-video-miniature 39 <my-video-miniature
23 *ngFor="let video of getVideosOf(videoChannel)" 40 *ngFor="let video of getVideosOf(videoChannel)"
24 [video]="video" [user]="userMiniature" [displayVideoActions]="true" 41 [video]="video" [user]="userMiniature" [displayVideoActions]="true" [displayOptions]="miniatureDisplayOptions"
25 ></my-video-miniature> 42 ></my-video-miniature>
26 </div>
27 43
28 <a *ngIf="getVideosOf(videoChannel).length !== 0" class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)"> 44 <div *ngIf="getTotalVideosOf(videoChannel)" class="miniature-show-channel">
29 SHOW THIS CHANNEL 45 <a i18n [routerLink]="getVideoChannelLink(videoChannel)">SHOW THIS CHANNEL ></a>
30 </a> 46 </div>
47 </div>
31 </div> 48 </div>
32 </div> 49 </div>
33</div> 50</div>
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
index 4957e91d7..7e88802f3 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
@@ -3,37 +3,175 @@
3@import '_miniature'; 3@import '_miniature';
4 4
5.margin-content { 5.margin-content {
6 @include fluid-videos-miniature-layout; 6 @include grid-videos-miniature-margins;
7} 7}
8 8
9.section { 9.channel {
10 @include miniature-rows; 10 max-width: $max-channels-width;
11 background-color: pvar(--channelBackgroundColor);
12 padding: 30px;
11 13
12 padding-top: 0 !important; 14 margin: 30px 0;
13 15
14 .section-title { 16 display: grid;
17 grid-template-columns: 1fr auto;
18 grid-template-rows: auto auto;
19 column-gap: 15px;
20}
21
22.channel-avatar-row {
23 grid-column: 1;
24 grid-row: 1;
25
26 display: grid;
27 grid-template-columns: auto auto 1fr;
28 grid-template-rows: auto 1fr;
29
30 .avatar-link {
31 grid-column: 1;
32 grid-row: 1 / 3;
33 margin-right: 30px;
34 }
35
36 img {
37 @include channel-avatar(75px);
38 }
39
40 a {
41 color: pvar(--mainForegroundColor);
42 }
43
44 h2 {
45 grid-row: 1;
46 grid-column: 2;
47 font-size: 20px;
48 line-height: 1;
49 font-weight: $font-bold;
50 margin: 0;
51 }
52
53 .actor-counters {
54 grid-row: 1;
55 grid-column: 3;
56 color: pvar(--greyForegroundColor);
57 font-size: 16px;
58 display: flex;
15 align-items: center; 59 align-items: center;
60 margin-left: 15px;
16 } 61 }
17 62
18 .videos { 63 .actor-counters > *:not(:last-child)::after {
19 overflow: hidden; 64 content: '•';
65 margin: 0 10px;
66 color: pvar(--mainColor);
67 }
20 68
21 .no-results { 69 .description-html {
22 height: 50px; 70 grid-column: 2 / 4;
23 } 71 grid-row: 2;
72
73 max-height: 80px;
74 font-size: 16px;
75
76 @include fade-text(30px, pvar(--channelBackgroundColor));
77 }
78}
79
80my-subscribe-button {
81 grid-row: 1;
82 grid-column: 2;
83}
84
85.videos {
86 display: flex;
87 grid-column: 1 / 3;
88 grid-row: 2;
89 margin-top: 30px;
90
91 position: relative;
92 overflow: hidden;
93
94 my-video-miniature {
95 margin-right: 15px;
96 min-width: $video-thumbnail-medium-width;
97 max-width: $video-thumbnail-medium-width;
98 }
99
100 .no-results {
101 height: auto;
24 } 102 }
103}
25 104
26 my-video-miniature ::ng-deep my-video-actions-dropdown > my-action-dropdown { 105.miniature-show-channel {
27 // Fix our overflow 106 height: 100%;
28 position: absolute; 107 position: absolute;
108 right: 0;
109 background: linear-gradient(90deg, transparent 0, pvar(--channelBackgroundColor) 45px);
110 padding: ($video-thumbnail-medium-height / 2 - 10px) 15px 0 60px;
111 z-index: z(miniature) + 1;
112
113 a {
114 color: pvar(--mainColor);
115 font-size: 16px;
116 font-weight: $font-semibold;
29 } 117 }
30} 118}
31 119
120.button-show-channel {
121 display: none;
122}
123
32@media screen and (max-width: $mobile-view) { 124@media screen and (max-width: $mobile-view) {
33 .section { 125 .channel {
34 .section-title { 126 padding: 15px;
35 flex-direction: column; 127 }
36 align-items: normal; 128
129 .channel-avatar-row {
130 grid-template-columns: auto auto auto 1fr;
131
132 .avatar-link {
133 grid-row: 1 / 4;
134 }
135
136 h2 {
137 font-size: 16px;
37 } 138 }
139
140 .actor-counters {
141 margin: 0;
142 font-size: 13px;
143 grid-row: 2;
144 grid-column: 2 / 4;
145 }
146
147 .description-html {
148 grid-row: 3;
149 font-size: 14px;
150 }
151 }
152
153 .show-channel a {
154 @include peertube-button-link;
155 @include orange-button-inverted;
156 }
157
158 .videos {
159 display: none;
160 }
161
162 my-subscribe-button,
163 .button-show-channel {
164 grid-column: 1 / 4;
165 grid-row: 3;
166 margin-top: 15px;
167 }
168
169 my-subscribe-button {
170 justify-self: start;
171 }
172
173 .button-show-channel {
174 display: block;
175 justify-self: end;
38 } 176 }
39} 177}
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
index f2beb6689..0628c7a96 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
@@ -1,9 +1,10 @@
1import { from, Subject, Subscription } from 'rxjs' 1import { from, Subject, Subscription } from 'rxjs'
2import { concatMap, map, switchMap, tap } from 'rxjs/operators' 2import { concatMap, map, switchMap, tap } from 'rxjs/operators'
3import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ComponentPagination, hasMoreItems, ScreenService, User, UserService } from '@app/core' 4import { ComponentPagination, hasMoreItems, MarkdownService, ScreenService, User, UserService } from '@app/core'
5import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' 5import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
6import { NSFWPolicyType, VideoSortField } from '@shared/models' 6import { NSFWPolicyType, VideoSortField } from '@shared/models'
7import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
7 8
8@Component({ 9@Component({
9 selector: 'my-account-video-channels', 10 selector: 'my-account-video-channels',
@@ -13,7 +14,10 @@ import { NSFWPolicyType, VideoSortField } from '@shared/models'
13export class AccountVideoChannelsComponent implements OnInit, OnDestroy { 14export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
14 account: Account 15 account: Account
15 videoChannels: VideoChannel[] = [] 16 videoChannels: VideoChannel[] = []
16 videos: { [id: number]: Video[] } = {} 17
18 videos: { [id: number]: { total: number, videos: Video[] } } = {}
19
20 channelsDescriptionHTML: { [ id: number ]: string } = {}
17 21
18 channelPagination: ComponentPagination = { 22 channelPagination: ComponentPagination = {
19 currentPage: 1, 23 currentPage: 1,
@@ -23,7 +27,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
23 27
24 videosPagination: ComponentPagination = { 28 videosPagination: ComponentPagination = {
25 currentPage: 1, 29 currentPage: 1,
26 itemsPerPage: 12, 30 itemsPerPage: 5,
27 totalItems: null 31 totalItems: null
28 } 32 }
29 videosSort: VideoSortField = '-publishedAt' 33 videosSort: VideoSortField = '-publishedAt'
@@ -32,6 +36,16 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
32 36
33 userMiniature: User 37 userMiniature: User
34 nsfwPolicy: NSFWPolicyType 38 nsfwPolicy: NSFWPolicyType
39 miniatureDisplayOptions: MiniatureDisplayOptions = {
40 date: true,
41 views: true,
42 by: false,
43 avatar: false,
44 privacyLabel: false,
45 privacyText: false,
46 state: false,
47 blacklistInfo: false
48 }
35 49
36 private accountSub: Subscription 50 private accountSub: Subscription
37 51
@@ -39,7 +53,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
39 private accountService: AccountService, 53 private accountService: AccountService,
40 private videoChannelService: VideoChannelService, 54 private videoChannelService: VideoChannelService,
41 private videoService: VideoService, 55 private videoService: VideoService,
42 private screenService: ScreenService, 56 private markdown: MarkdownService,
43 private userService: UserService 57 private userService: UserService
44 ) { } 58 ) { }
45 59
@@ -78,23 +92,36 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
78 } 92 }
79 93
80 return this.videoService.getVideoChannelVideos(options) 94 return this.videoService.getVideoChannelVideos(options)
81 .pipe(map(data => ({ videoChannel, videos: data.data }))) 95 .pipe(map(data => ({ videoChannel, videos: data.data, total: data.total })))
82 }) 96 })
83 ) 97 )
84 .subscribe(({ videoChannel, videos }) => { 98 .subscribe(async ({ videoChannel, videos, total }) => {
99 this.channelsDescriptionHTML[videoChannel.id] = await this.markdown.textMarkdownToHTML(videoChannel.description)
100
85 this.videoChannels.push(videoChannel) 101 this.videoChannels.push(videoChannel)
86 102
87 this.videos[videoChannel.id] = videos 103 this.videos[videoChannel.id] = { videos, total }
88 104
89 this.onChannelDataSubject.next([ videoChannel ]) 105 this.onChannelDataSubject.next([ videoChannel ])
90 }) 106 })
91 } 107 }
92 108
93 getVideosOf (videoChannel: VideoChannel) { 109 getVideosOf (videoChannel: VideoChannel) {
94 const numberOfVideos = this.screenService.getNumberOfAvailableMiniatures() 110 const obj = this.videos[ videoChannel.id ]
111 if (!obj) return []
112
113 return obj.videos
114 }
115
116 getTotalVideosOf (videoChannel: VideoChannel) {
117 const obj = this.videos[ videoChannel.id ]
118 if (!obj) return undefined
119
120 return obj.total
121 }
95 122
96 // 2 rows 123 getChannelDescription (videoChannel: VideoChannel) {
97 return this.videos[ videoChannel.id ].slice(0, numberOfVideos * 2) 124 return this.channelsDescriptionHTML[videoChannel.id]
98 } 125 }
99 126
100 onNearOfBottom () { 127 onNearOfBottom () {
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts
index 484d60e25..75af45e90 100644
--- a/client/src/app/+accounts/account-videos/account-videos.component.ts
+++ b/client/src/app/+accounts/account-videos/account-videos.component.ts
@@ -16,6 +16,7 @@ import { VideoFilter } from '@shared/models'
16 ] 16 ]
17}) 17})
18export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { 18export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
19 // No value because we don't want a page title
19 titlePage: string 20 titlePage: string
20 loadOnInit = false 21 loadOnInit = false
21 loadUserVideoPreferences = true 22 loadUserVideoPreferences = true
@@ -77,11 +78,6 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
77 78
78 return this.videoService 79 return this.videoService
79 .getAccountVideos(options) 80 .getAccountVideos(options)
80 .pipe(
81 tap(({ total }) => {
82 this.titlePage = $localize`Published ${total} videos`
83 })
84 )
85 } 81 }
86 82
87 toggleModerationDisplay () { 83 toggleModerationDisplay () {
@@ -93,4 +89,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
93 generateSyndicationList () { 89 generateSyndicationList () {
94 this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id) 90 this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id)
95 } 91 }
92
93 displayAsRow () {
94 return this.screenService.isInMobileView()
95 }
96} 96}
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts
index 15937a67b..3bf0f7185 100644
--- a/client/src/app/+accounts/accounts-routing.module.ts
+++ b/client/src/app/+accounts/accounts-routing.module.ts
@@ -1,11 +1,10 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core' 3import { MetaGuard } from '@ngx-meta/core'
4import { AccountsComponent } from './accounts.component'
5import { AccountVideosComponent } from './account-videos/account-videos.component'
6import { AccountAboutComponent } from './account-about/account-about.component'
7import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
8import { AccountSearchComponent } from './account-search/account-search.component' 4import { AccountSearchComponent } from './account-search/account-search.component'
5import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
6import { AccountVideosComponent } from './account-videos/account-videos.component'
7import { AccountsComponent } from './accounts.component'
9 8
10const accountsRoutes: Routes = [ 9const accountsRoutes: Routes = [
11 { 10 {
@@ -32,15 +31,6 @@ const accountsRoutes: Routes = [
32 } 31 }
33 }, 32 },
34 { 33 {
35 path: 'about',
36 component: AccountAboutComponent,
37 data: {
38 meta: {
39 title: $localize`About account`
40 }
41 }
42 },
43 {
44 path: 'videos', 34 path: 'videos',
45 component: AccountVideosComponent, 35 component: AccountVideosComponent,
46 data: { 36 data: {
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 5bd7b0824..03d083bb6 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -1,57 +1,89 @@
1<div *ngIf="account" class="row"> 1<div *ngIf="account" class="root">
2 <div class="sub-menu"> 2 <div class="account-info">
3 3
4 <div class="actor"> 4 <div class="account-avatar-row">
5 <img [src]="account.avatarUrl" alt="Avatar" /> 5 <img class="account-avatar" [src]="account.avatarUrl" alt="Avatar" />
6 6
7 <div class="actor-info"> 7 <div>
8 <div class="actor-names"> 8 <div class="section-label" i18n>PEERTUBE ACCOUNT</div>
9 <div class="actor-display-name">{{ account.displayName }}</div> 9
10 <div class="actor-name"> 10 <div class="actor-info">
11 <span>{{ account.nameWithHost }}</span> 11 <div>
12 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" 12 <div class="actor-display-name">
13 class="btn btn-outline-secondary btn-sm copy-button" 13 <h1>{{ account.displayName }}</h1>
14 > 14
15 <span class="glyphicon glyphicon-copy"></span> 15 <my-user-moderation-dropdown
16 </button> 16 [prependActions]="prependModerationActions"
17 buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto"
18 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
19 ></my-user-moderation-dropdown>
20
21 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
22 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
23 <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
24 <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
25 <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
26 </div>
27
28 <div class="actor-handle">
29 <span>@{{ account.nameWithHost }}</span>
30 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
31 class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title
32 >
33 <span class="glyphicon glyphicon-duplicate"></span>
34 </button>
35 </div>
36
37 <div class="actor-counters">
38 <span i18n>{naiveAggregatedSubscribers(), plural, =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span>
39
40 <span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n>
41 {accountVideosCount, plural, =1 {1 videos} other {{{ accountVideosCount }} videos}}
42 </span>
43 </div>
17 </div> 44 </div>
18 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
19 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
20 <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
21 <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
22 <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
23
24 <my-user-moderation-dropdown
25 [prependActions]="prependModerationActions"
26 buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto"
27 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
28 ></my-user-moderation-dropdown>
29 </div>
30 <div class="actor-followers" [title]="accountFollowerTitle">
31 {{ subscribersDisplayFor(naiveAggregatedSubscribers) }}
32 </div> 45 </div>
33 </div> 46 </div>
47 </div>
34 48
35 <div class="right-buttons"> 49 <div class="description" [ngClass]="{ expanded: accountDescriptionExpanded }">
36 <a *ngIf="isAccountManageable && !isInSmallView" routerLink="/my-account" class="btn btn-outline-tertiary mr-2" i18n>Manage account</a> 50 <div class="description-html" [innerHTML]="accountDescriptionHTML"></div>
37 <my-subscribe-button *ngIf="videoChannels" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button> 51
38 </div> 52 <div class="created-at" i18n>Account created on {{ account.createdAt | date }}</div>
39 </div> 53 </div>
40 54
41 <div class="links w-100"> 55 <div *ngIf="hasShowMoreDescription()" class="show-more" role="button"
42 <ng-template #linkTemplate let-item="item"> 56 (click)="accountDescriptionExpanded = !accountDescriptionExpanded"
43 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> 57 title="Show the complete description" i18n-title i18n
44 </ng-template> 58 >
59 Show more...
60 </div>
45 61
46 <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> 62 <div class="buttons">
63 <a *ngIf="isManageable()" routerLink="/my-account" class="peertube-button-link orange-button" i18n>
64 Manage account
65 </a>
47 66
48 <simple-search-input (searchChanged)="searchChanged($event)" name="search-videos" i18n-placeholder placeholder="Search videos"></simple-search-input> 67 <my-subscribe-button *ngIf="hasVideoChannels() && !isManageable()" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button>
49 </div> 68 </div>
50 </div> 69 </div>
51 70
52 <div class="margin-content"> 71 <div class="links">
53 <router-outlet (activate)="onOutletLoaded($event)"></router-outlet> 72 <ng-template #linkTemplate let-item="item">
73 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
74 </ng-template>
75
76 <list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
77
78 <simple-search-input
79 [alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)"
80 (inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos"
81 i18n-iconTitle icon-title="Search account videos"
82 i18n-placeholder placeholder="Search account videos"
83 ></simple-search-input>
54 </div> 84 </div>
85
86 <router-outlet (activate)="onOutletLoaded($event)"></router-outlet>
55</div> 87</div>
56 88
57<ng-container *ngIf="prependModerationActions"> 89<ng-container *ngIf="prependModerationActions">
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index 40c6b6493..a836e84ce 100644
--- a/client/src/app/+accounts/accounts.component.scss
+++ b/client/src/app/+accounts/accounts.component.scss
@@ -1,48 +1,29 @@
1// Bootstrap grid utilities require functions, variables and mixins
2@import 'node_modules/bootstrap/scss/functions';
3@import 'node_modules/bootstrap/scss/variables';
4@import 'node_modules/bootstrap/scss/mixins';
5@import 'node_modules/bootstrap/scss/grid';
6
7@import '_variables'; 1@import '_variables';
8@import '_mixins'; 2@import '_mixins';
9 3@import '_actor';
10.sub-menu { 4@import '_miniature';
11 @include sub-menu-with-actor; 5
12 6.root {
13 .actor { 7 --myGlobalTopPadding: 60px;
14 width: 100%; 8 --myImgMargin: 30px;
15 } 9 --myFontSize: 16px;
10 --myGreyFontSize: 16px;
16} 11}
17 12
18.margin-content { 13.section-label {
19 // margin-content is required, but child views have their own margins 14 @include section-label-responsive;
20 // that match views outside the scope of accounts, so we only align
21 // them with the margins of .sub-menu when required.
22 margin: 0;
23} 15}
24 16
25.right-buttons { 17.links {
26 display: flex; 18 @include grid-videos-miniature-margins;
27 height: max-content;
28 margin-left: auto;
29 margin-top: 10px;
30
31 @include media-breakpoint-down(lg) {
32 flex-flow: column-reverse;
33 19
34 a { 20 display: flex;
35 margin-top: 0.25rem; 21 justify-content: space-between;
36 margin-right: 0 !important; 22 align-items: center;
37 } 23 max-width: $max-channels-width;
38 }
39
40 a {
41 @include peertube-button-outline;
42 }
43 24
44 my-subscribe-button { 25 simple-search-input {
45 min-height: 30px; 26 margin-left: auto;
46 } 27 }
47} 28}
48 29
@@ -60,39 +41,106 @@ my-user-moderation-dropdown,
60 41
61.copy-button { 42.copy-button {
62 border: none; 43 border: none;
63 padding: 5px; 44}
64 margin-top: -2px; 45
46.account-info {
47 @include grid-videos-miniature-margins(false, 15px);
48
49 display: grid;
50 grid-template-columns: 1fr min-content;
51 grid-template-rows: auto auto;
52
53 background-color: pvar(--submenuBackgroundColor);
54 margin-bottom: 45px;
55 padding-top: var(--myGlobalTopPadding);
56 font-size: var(--myFontSize);
57}
58
59.account-avatar-row {
60 @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
61}
62
63.description {
64 grid-column: 1 / 3;
65 max-width: 1000px;
66 word-break: break-word;
67}
68
69.created-at {
70 margin-top: 15px;
71 color: pvar(--greyForegroundColor);
72 padding-bottom: 60px;
73}
74
75.show-more {
76 @include show-more-description;
77
78 display: none;
79 text-align: center;
80}
81
82.buttons {
83 grid-column: 2;
84 grid-row: 1;
85
86 display: flex;
87 flex-wrap: wrap;
88 justify-content: flex-end;
89 align-content: flex-start;
90
91 > *:not(:last-child) {
92 margin-bottom: 15px;
93 }
94
95 > a {
96 white-space: nowrap;
97 }
98}
99
100@media screen and (max-width: $small-view) {
101 .root {
102 --myGlobalTopPadding: 45px;
103 --myChannelImgMargin: 15px;
104 }
105
106 .account-info {
107 display: block;
108 padding-bottom: 60px;
109 }
110
111 .description:not(.expanded) {
112 max-height: 70px;
113
114 @include fade-text(30px, pvar(--submenuBackgroundColor));
115 }
116
117 .show-more {
118 display: block;
119 }
120
121 .buttons {
122 justify-content: center;
123 }
65} 124}
66 125
67@media screen and (max-width: $mobile-view) { 126@media screen and (max-width: $mobile-view) {
68 .sub-menu { 127 .root {
69 .actor { 128 --myGlobalTopPadding: 15px;
70 flex-direction: column; 129 --myFontSize: 14px;
71 align-items: center; 130 --myGreyFontSize: 13px;
72 131 }
73 img, 132
74 .actor-info .actor-names .actor-display-name { 133 .account-info {
75 margin-right: 0; 134 display: block;
76 } 135 padding-bottom: 30px;
77 136 }
78 .actor-info { 137
79 .actor-names { 138 .links {
80 flex-direction: column; 139 margin: auto !important;
81 align-items: center; 140 width: min-content;
82 } 141 }
83 142
84 my-user-moderation-dropdown { 143 .show-more {
85 margin-left: 0; 144 margin-bottom: 30px;
86 }
87
88 .actor-followers {
89 text-align: center;
90 }
91 }
92
93 .right-buttons {
94 margin-left: 0;
95 }
96 }
97 } 145 }
98} 146}
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index e6a5a5d5e..fbd7380a9 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -2,11 +2,19 @@ import { Subscription } from 'rxjs'
2import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' 2import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
3import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' 3import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute } from '@angular/router' 4import { ActivatedRoute } from '@angular/router'
5import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core' 5import { AuthService, MarkdownService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core'
6import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' 6import {
7 Account,
8 AccountService,
9 DropdownAction,
10 ListOverflowItem,
11 VideoChannel,
12 VideoChannelService,
13 VideoService
14} from '@app/shared/shared-main'
7import { AccountReportComponent } from '@app/shared/shared-moderation' 15import { AccountReportComponent } from '@app/shared/shared-moderation'
8import { User, UserRight } from '@shared/models'
9import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 16import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
17import { User, UserRight } from '@shared/models'
10import { AccountSearchComponent } from './account-search/account-search.component' 18import { AccountSearchComponent } from './account-search/account-search.component'
11 19
12@Component({ 20@Component({
@@ -15,16 +23,23 @@ import { AccountSearchComponent } from './account-search/account-search.componen
15}) 23})
16export class AccountsComponent implements OnInit, OnDestroy { 24export class AccountsComponent implements OnInit, OnDestroy {
17 @ViewChild('accountReportModal') accountReportModal: AccountReportComponent 25 @ViewChild('accountReportModal') accountReportModal: AccountReportComponent
26
18 accountSearch: AccountSearchComponent 27 accountSearch: AccountSearchComponent
19 28
20 account: Account 29 account: Account
21 accountUser: User 30 accountUser: User
31
22 videoChannels: VideoChannel[] = [] 32 videoChannels: VideoChannel[] = []
33
23 links: ListOverflowItem[] = [] 34 links: ListOverflowItem[] = []
35 hideMenu = false
24 36
25 isAccountManageable = false
26 accountFollowerTitle = '' 37 accountFollowerTitle = ''
27 38
39 accountVideosCount: number
40 accountDescriptionHTML = ''
41 accountDescriptionExpanded = false
42
28 prependModerationActions: DropdownAction<any>[] 43 prependModerationActions: DropdownAction<any>[]
29 44
30 private routeSub: Subscription 45 private routeSub: Subscription
@@ -38,6 +53,8 @@ export class AccountsComponent implements OnInit, OnDestroy {
38 private restExtractor: RestExtractor, 53 private restExtractor: RestExtractor,
39 private redirectService: RedirectService, 54 private redirectService: RedirectService,
40 private authService: AuthService, 55 private authService: AuthService,
56 private videoService: VideoService,
57 private markdown: MarkdownService,
41 private screenService: ScreenService 58 private screenService: ScreenService
42 ) { 59 ) {
43 } 60 }
@@ -62,9 +79,8 @@ export class AccountsComponent implements OnInit, OnDestroy {
62 ) 79 )
63 80
64 this.links = [ 81 this.links = [
65 { label: $localize`VIDEO CHANNELS`, routerLink: 'video-channels' }, 82 { label: $localize`CHANNELS`, routerLink: 'video-channels' },
66 { label: $localize`VIDEOS`, routerLink: 'videos' }, 83 { label: $localize`VIDEOS`, routerLink: 'videos' }
67 { label: $localize`ABOUT`, routerLink: 'about' }
68 ] 84 ]
69 } 85 }
70 86
@@ -72,19 +88,29 @@ export class AccountsComponent implements OnInit, OnDestroy {
72 if (this.routeSub) this.routeSub.unsubscribe() 88 if (this.routeSub) this.routeSub.unsubscribe()
73 } 89 }
74 90
75 get naiveAggregatedSubscribers () { 91 naiveAggregatedSubscribers () {
76 return this.videoChannels.reduce( 92 return this.videoChannels.reduce(
77 (acc, val) => acc + val.followersCount, 93 (acc, val) => acc + val.followersCount,
78 this.account.followersCount // accumulator starts with the base number of subscribers the account has 94 this.account.followersCount // accumulator starts with the base number of subscribers the account has
79 ) 95 )
80 } 96 }
81 97
82 get isInSmallView () { 98 isUserLoggedIn () {
99 return this.authService.isLoggedIn()
100 }
101
102 isInSmallView () {
83 return this.screenService.isInSmallView() 103 return this.screenService.isInSmallView()
84 } 104 }
85 105
106 isManageable () {
107 if (!this.isUserLoggedIn()) return false
108
109 return this.account?.userId === this.authService.getUser().id
110 }
111
86 onUserChanged () { 112 onUserChanged () {
87 this.getUserIfNeeded(this.account) 113 this.loadUserIfNeeded(this.account)
88 } 114 }
89 115
90 onUserDeleted () { 116 onUserDeleted () {
@@ -113,40 +139,38 @@ export class AccountsComponent implements OnInit, OnDestroy {
113 if (this.accountSearch) this.accountSearch.updateSearch(search) 139 if (this.accountSearch) this.accountSearch.updateSearch(search)
114 } 140 }
115 141
116 private onAccount (account: Account) { 142 onSearchInputDisplayChanged (displayed: boolean) {
143 this.hideMenu = this.isInSmallView() && displayed
144 }
145
146 hasVideoChannels () {
147 return this.videoChannels.length !== 0
148 }
149
150 hasShowMoreDescription () {
151 return !this.accountDescriptionExpanded && this.accountDescriptionHTML.length > 100
152 }
153
154 private async onAccount (account: Account) {
155 this.accountFollowerTitle = $localize`${account.followersCount} direct account followers`
156
117 this.prependModerationActions = undefined 157 this.prependModerationActions = undefined
118 158
119 this.account = account 159 this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description)
120 160
121 if (this.authService.isLoggedIn()) { 161 // After the markdown renderer to avoid layout changes
122 this.authService.userInformationLoaded.subscribe( 162 this.account = account
123 () => {
124 this.isAccountManageable = this.account.userId && this.account.userId === this.authService.getUser().id
125
126 const followers = this.subscribersDisplayFor(account.followersCount)
127 this.accountFollowerTitle = $localize`${followers} direct account followers`
128
129 // It's not our account, we can report it
130 if (!this.isAccountManageable) {
131 this.prependModerationActions = [
132 {
133 label: $localize`Report this account`,
134 handler: () => this.showReportModal()
135 }
136 ]
137 }
138 }
139 )
140 }
141 163
142 this.getUserIfNeeded(account) 164 this.updateModerationActions()
165 this.loadUserIfNeeded(account)
166 this.loadAccountVideosCount()
143 } 167 }
144 168
145 private showReportModal () { 169 private showReportModal () {
146 this.accountReportModal.show() 170 this.accountReportModal.show()
147 } 171 }
148 172
149 private getUserIfNeeded (account: Account) { 173 private loadUserIfNeeded (account: Account) {
150 if (!account.userId || !this.authService.isLoggedIn()) return 174 if (!account.userId || !this.authService.isLoggedIn()) return
151 175
152 const user = this.authService.getUser() 176 const user = this.authService.getUser()
@@ -158,4 +182,33 @@ export class AccountsComponent implements OnInit, OnDestroy {
158 ) 182 )
159 } 183 }
160 } 184 }
185
186 private updateModerationActions () {
187 if (!this.authService.isLoggedIn()) return
188
189 this.authService.userInformationLoaded.subscribe(
190 () => {
191 if (this.isManageable()) return
192
193 // It's not our account, we can report it
194 this.prependModerationActions = [
195 {
196 label: $localize`Report this account`,
197 handler: () => this.showReportModal()
198 }
199 ]
200 }
201 )
202 }
203
204 private loadAccountVideosCount () {
205 this.videoService.getAccountVideos({
206 account: this.account,
207 videoPagination: {
208 currentPage: 1,
209 itemsPerPage: 0
210 },
211 sort: '-publishedAt'
212 }).subscribe(res => this.accountVideosCount = res.total)
213 }
161} 214}
diff --git a/client/src/app/+accounts/accounts.module.ts b/client/src/app/+accounts/accounts.module.ts
index 6da65cbc1..3354b4189 100644
--- a/client/src/app/+accounts/accounts.module.ts
+++ b/client/src/app/+accounts/accounts.module.ts
@@ -5,10 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedModerationModule } from '@app/shared/shared-moderation' 5import { SharedModerationModule } from '@app/shared/shared-moderation'
6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
8import { AccountAboutComponent } from './account-about/account-about.component' 8import { AccountSearchComponent } from './account-search/account-search.component'
9import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' 9import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
10import { AccountVideosComponent } from './account-videos/account-videos.component' 10import { AccountVideosComponent } from './account-videos/account-videos.component'
11import { AccountSearchComponent } from './account-search/account-search.component'
12import { AccountsRoutingModule } from './accounts-routing.module' 11import { AccountsRoutingModule } from './accounts-routing.module'
13import { AccountsComponent } from './accounts.component' 12import { AccountsComponent } from './accounts.component'
14 13
@@ -28,7 +27,6 @@ import { AccountsComponent } from './accounts.component'
28 AccountsComponent, 27 AccountsComponent,
29 AccountVideosComponent, 28 AccountVideosComponent,
30 AccountVideoChannelsComponent, 29 AccountVideoChannelsComponent,
31 AccountAboutComponent,
32 AccountSearchComponent 30 AccountSearchComponent
33 ], 31 ],
34 32
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index fd648a425..bac65c88e 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -3,6 +3,7 @@ import { SelectButtonModule } from 'primeng/selectbutton'
3import { TableModule } from 'primeng/table' 3import { TableModule } from 'primeng/table'
4import { NgModule } from '@angular/core' 4import { NgModule } from '@angular/core'
5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' 5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
6import { SharedActorImageModule } from '@app/shared/shared-actor-image'
6import { SharedFormModule } from '@app/shared/shared-forms' 7import { SharedFormModule } from '@app/shared/shared-forms'
7import { SharedGlobalIconModule } from '@app/shared/shared-icons' 8import { SharedGlobalIconModule } from '@app/shared/shared-icons'
8import { SharedMainModule } from '@app/shared/shared-main' 9import { SharedMainModule } from '@app/shared/shared-main'
@@ -49,6 +50,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
49 SharedGlobalIconModule, 50 SharedGlobalIconModule,
50 SharedAbuseListModule, 51 SharedAbuseListModule,
51 SharedVideoCommentModule, 52 SharedVideoCommentModule,
53 SharedActorImageModule,
52 54
53 TableModule, 55 TableModule,
54 SelectButtonModule, 56 SelectButtonModule,
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
index 82c371f4d..d6aca10e7 100644
--- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
@@ -164,7 +164,8 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
164 baseUrl: `${environment.originServerUrl}/videos/embed/${entry.video.uuid}`, 164 baseUrl: `${environment.originServerUrl}/videos/embed/${entry.video.uuid}`,
165 title: false, 165 title: false,
166 warningTitle: false 166 warningTitle: false
167 }) 167 }),
168 entry.video.name
168 ) 169 )
169 } 170 }
170 171
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 1b5fe45c6..8edf03a89 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
@@ -3,7 +3,7 @@
3</div> 3</div>
4 4
5<div class="search-bar"> 5<div class="search-bar">
6 <input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..."/> 6 <input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..." autofocus />
7</div> 7</div>
8 8
9<div class="alert alert-info" i18n *ngIf="pluginInstalled"> 9<div class="alert alert-info" i18n *ngIf="pluginInstalled">
@@ -20,8 +20,8 @@
20 <my-global-icon iconName="search"></my-global-icon> 20 <my-global-icon iconName="search"></my-global-icon>
21 21
22 <ng-container i18n> 22 <ng-container i18n>
23 {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}" 23 {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for {{ search }}"
24 </ng-container> 24 </ng-container>
25 </ng-container> 25 </ng-container>
26</div> 26</div>
27 27
diff --git a/client/src/app/+admin/plugins/shared/plugin-list.component.scss b/client/src/app/+admin/plugins/shared/plugin-list.component.scss
index 83030b7e0..f59a01b74 100644
--- a/client/src/app/+admin/plugins/shared/plugin-list.component.scss
+++ b/client/src/app/+admin/plugins/shared/plugin-list.component.scss
@@ -3,7 +3,7 @@
3 3
4.plugin { 4.plugin {
5 margin: 15px 0; 5 margin: 15px 0;
6 background-color: pvar(--submenuColor); 6 background-color: pvar(--submenuBackgroundColor);
7} 7}
8 8
9.first-row { 9.first-row {
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 243c6556a..5e92c0f36 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -72,7 +72,7 @@
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">
75 <my-actor-avatar-info [actor]="user.account"></my-actor-avatar-info> 75 <my-actor-avatar-edit [actor]="user.account" [editable]="false" [displaySubscribers]="false" [displayUsername]="false"></my-actor-avatar-edit>
76 </div> 76 </div>
77 </div> 77 </div>
78 78
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss
index aa87b8d6d..8b0ac8783 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.scss
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss
@@ -72,11 +72,3 @@ input[type=submit], button {
72 @include dashboard; 72 @include dashboard;
73 max-width: 900px; 73 max-width: 900px;
74} 74}
75
76my-actor-avatar-info ::ng-deep {
77 .actor-img-edit-container,
78 .actor-info-followers,
79 .actor-info-username {
80 display: none;
81 }
82}
diff --git a/client/src/app/+login/login.component.html b/client/src/app/+login/login.component.html
index 3171e5b0f..0167066a0 100644
--- a/client/src/app/+login/login.component.html
+++ b/client/src/app/+login/login.component.html
@@ -21,7 +21,7 @@
21 <label i18n for="username">User</label> 21 <label i18n for="username">User</label>
22 <input 22 <input
23 type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1" 23 type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1"
24 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" #usernameInput 24 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" autofocus
25 > 25 >
26 </div> 26 </div>
27 27
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts
index af747b7fa..d8ad49081 100644
--- a/client/src/app/+login/login.component.ts
+++ b/client/src/app/+login/login.component.ts
@@ -3,9 +3,9 @@ import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angula
3import { ActivatedRoute } from '@angular/router' 3import { ActivatedRoute } from '@angular/router'
4import { AuthService, Notifier, RedirectService, UserService } from '@app/core' 4import { AuthService, Notifier, RedirectService, UserService } from '@app/core'
5import { HooksService } from '@app/core/plugins/hooks.service' 5import { HooksService } from '@app/core/plugins/hooks.service'
6import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
7import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators' 6import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators'
8import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
8import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
10import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' 10import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
11 11
@@ -16,7 +16,6 @@ import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
16}) 16})
17 17
18export class LoginComponent extends FormReactive implements OnInit, AfterViewInit { 18export class LoginComponent extends FormReactive implements OnInit, AfterViewInit {
19 @ViewChild('usernameInput', { static: false }) usernameInput: ElementRef
20 @ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef 19 @ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef
21 20
22 accordion: NgbAccordion 21 accordion: NgbAccordion
@@ -91,10 +90,6 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
91 } 90 }
92 91
93 ngAfterViewInit () { 92 ngAfterViewInit () {
94 if (this.usernameInput) {
95 this.usernameInput.nativeElement.focus()
96 }
97
98 this.hooks.runAction('action:login.init', 'login') 93 this.hooks.runAction('action:login.init', 'login')
99 } 94 }
100 95
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 ad7497f45..c7e173038 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
@@ -42,7 +42,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
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`,
44 abuseNewMessage: $localize`An abuse report received a new message`, 44 abuseNewMessage: $localize`An abuse report received a new message`,
45 abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators` 45 abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`,
46 newPeerTubeVersion: $localize`A new PeerTube version is available`,
47 newPluginVersion: $localize`One of your plugin/theme has a new available version`
46 } 48 }
47 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] 49 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
48 50
@@ -51,7 +53,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
51 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, 53 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
52 newUserRegistration: UserRight.MANAGE_USERS, 54 newUserRegistration: UserRight.MANAGE_USERS,
53 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW, 55 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
54 autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION 56 autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION,
57 newPeerTubeVersion: UserRight.MANAGE_DEBUG,
58 newPluginVersion: UserRight.MANAGE_DEBUG
55 } 59 }
56 } 60 }
57 61
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 b0d2ec58d..48d06280b 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
@@ -3,7 +3,7 @@
3 <div class="form-group col-12 col-lg-4 col-xl-3"></div> 3 <div class="form-group col-12 col-lg-4 col-xl-3"></div>
4 4
5 <div class="form-group col-12 col-lg-8 col-xl-9"> 5 <div class="form-group col-12 col-lg-8 col-xl-9">
6 <my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-info> 6 <my-actor-avatar-edit [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-edit>
7 </div> 7 </div>
8</div> 8</div>
9 9
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 076864563..3df48d0aa 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -3,6 +3,7 @@ import { TableModule } from 'primeng/table'
3import { DragDropModule } from '@angular/cdk/drag-drop' 3import { DragDropModule } from '@angular/cdk/drag-drop'
4import { NgModule } from '@angular/core' 4import { NgModule } from '@angular/core'
5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' 5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
6import { SharedActorImageModule } from '@app/shared/shared-actor-image'
6import { SharedFormModule } from '@app/shared/shared-forms' 7import { SharedFormModule } from '@app/shared/shared-forms'
7import { SharedGlobalIconModule } from '@app/shared/shared-icons' 8import { SharedGlobalIconModule } from '@app/shared/shared-icons'
8import { SharedMainModule } from '@app/shared/shared-main' 9import { SharedMainModule } from '@app/shared/shared-main'
@@ -10,6 +11,7 @@ import { SharedModerationModule } from '@app/shared/shared-moderation'
10import { SharedShareModal } from '@app/shared/shared-share-modal' 11import { SharedShareModal } from '@app/shared/shared-share-modal'
11import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' 12import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings'
12import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' 13import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
14import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
13import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' 15import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
14import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' 16import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
15import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' 17import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
@@ -20,7 +22,6 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d
20import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' 22import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences'
21import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' 23import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component'
22import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' 24import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
23import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
24import { MyAccountComponent } from './my-account.component' 25import { MyAccountComponent } from './my-account.component'
25 26
26@NgModule({ 27@NgModule({
@@ -37,7 +38,8 @@ import { MyAccountComponent } from './my-account.component'
37 SharedUserInterfaceSettingsModule, 38 SharedUserInterfaceSettingsModule,
38 SharedGlobalIconModule, 39 SharedGlobalIconModule,
39 SharedAbuseListModule, 40 SharedAbuseListModule,
40 SharedShareModal 41 SharedShareModal,
42 SharedActorImageModule
41 ], 43 ],
42 44
43 declarations: [ 45 declarations: [
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
index a625493de..b3265210f 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
@@ -8,10 +8,12 @@ import {
8 VIDEO_CHANNEL_SUPPORT_VALIDATOR 8 VIDEO_CHANNEL_SUPPORT_VALIDATOR
9} from '@app/shared/form-validators/video-channel-validators' 9} from '@app/shared/form-validators/video-channel-validators'
10import { FormValidatorService } from '@app/shared/shared-forms' 10import { FormValidatorService } from '@app/shared/shared-forms'
11import { VideoChannelService } from '@app/shared/shared-main' 11import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
12import { VideoChannelCreate } from '@shared/models' 12import { VideoChannelCreate } from '@shared/models'
13import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 13import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
14import { MyVideoChannelEdit } from './my-video-channel-edit' 14import { MyVideoChannelEdit } from './my-video-channel-edit'
15import { switchMap } from 'rxjs/operators'
16import { of } from 'rxjs'
15 17
16@Component({ 18@Component({
17 templateUrl: './my-video-channel-edit.component.html', 19 templateUrl: './my-video-channel-edit.component.html',
@@ -19,6 +21,10 @@ import { MyVideoChannelEdit } from './my-video-channel-edit'
19}) 21})
20export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit { 22export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit {
21 error: string 23 error: string
24 videoChannel = new VideoChannel({})
25
26 private avatar: FormData
27 private banner: FormData
22 28
23 constructor ( 29 constructor (
24 protected formValidatorService: FormValidatorService, 30 protected formValidatorService: FormValidatorService,
@@ -50,23 +56,43 @@ export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements
50 support: body.support || null 56 support: body.support || null
51 } 57 }
52 58
53 this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe( 59 this.videoChannelService.createVideoChannel(videoChannelCreate)
54 () => { 60 .pipe(
55 this.authService.refreshUserInformation() 61 switchMap(() => this.uploadAvatar()),
62 switchMap(() => this.uploadBanner())
63 ).subscribe(
64 () => {
65 this.authService.refreshUserInformation()
66
67 this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`)
68 this.router.navigate(['/my-library', 'video-channels'])
69 },
56 70
57 this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`) 71 err => {
58 this.router.navigate([ '/my-library', 'video-channels' ]) 72 if (err.status === HttpStatusCode.CONFLICT_409) {
59 }, 73 this.error = $localize`This name already exists on this instance.`
74 return
75 }
60 76
61 err => { 77 this.error = err.message
62 if (err.status === HttpStatusCode.CONFLICT_409) {
63 this.error = $localize`This name already exists on this instance.`
64 return
65 } 78 }
79 )
80 }
81
82 onAvatarChange (formData: FormData) {
83 this.avatar = formData
84 }
85
86 onAvatarDelete () {
87 this.avatar = null
88 }
89
90 onBannerChange (formData: FormData) {
91 this.banner = formData
92 }
66 93
67 this.error = err.message 94 onBannerDelete () {
68 } 95 this.banner = null
69 )
70 } 96 }
71 97
72 isCreation () { 98 isCreation () {
@@ -76,4 +102,20 @@ export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements
76 getFormButtonTitle () { 102 getFormButtonTitle () {
77 return $localize`Create` 103 return $localize`Create`
78 } 104 }
105
106 getUsername () {
107 return this.form.value.name
108 }
109
110 private uploadAvatar () {
111 if (!this.avatar) return of(undefined)
112
113 return this.videoChannelService.changeVideoChannelImage(this.getUsername(), this.avatar, 'avatar')
114 }
115
116 private uploadBanner () {
117 if (!this.banner) return of(undefined)
118
119 return this.videoChannelService.changeVideoChannelImage(this.getUsername(), this.banner, 'banner')
120 }
79} 121}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
index 735f9e3ba..2910dffad 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
@@ -10,7 +10,7 @@
10 <ng-container *ngIf="!isCreation()"> 10 <ng-container *ngIf="!isCreation()">
11 <li class="breadcrumb-item active" i18n>Edit</li> 11 <li class="breadcrumb-item active" i18n>Edit</li>
12 <li class="breadcrumb-item active" aria-current="page"> 12 <li class="breadcrumb-item active" aria-current="page">
13 <a *ngIf="videoChannelToUpdate" [routerLink]="[ '/my-library/video-channels/update', videoChannelToUpdate?.nameWithHost ]">{{ videoChannelToUpdate?.displayName }}</a> 13 <a *ngIf="videoChannel" [routerLink]="[ '/my-library/video-channels/update', videoChannel?.nameWithHost ]">{{ videoChannel?.displayName }}</a>
14 </li> 14 </li>
15 </ng-container> 15 </ng-container>
16 </ol> 16 </ol>
@@ -23,10 +23,22 @@
23 <div class="form-row"> <!-- channel grid --> 23 <div class="form-row"> <!-- channel grid -->
24 <div class="form-group col-12 col-lg-4 col-xl-3"> 24 <div class="form-group col-12 col-lg-4 col-xl-3">
25 <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div> 25 <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
26 <div *ngIf="!isCreation() && videoChannelToUpdate" class="video-channel-title" i18n>CHANNEL</div> 26 <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div>
27 </div> 27 </div>
28 28
29 <div class="form-group col-12 col-lg-8 col-xl-9"> 29 <div class="form-group col-12 col-lg-8 col-xl-9">
30 <h6 i18n>Banner image of your channel</h6>
31
32 <my-actor-banner-edit
33 *ngIf="videoChannel" [previewImage]="isCreation()"
34 [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
35 ></my-actor-banner-edit>
36
37 <my-actor-avatar-edit
38 *ngIf="videoChannel" [previewImage]="isCreation()"
39 [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
40 [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
41 ></my-actor-avatar-edit>
30 42
31 <div class="form-group" *ngIf="isCreation()"> 43 <div class="form-group" *ngIf="isCreation()">
32 <label i18n for="name">Name</label> 44 <label i18n for="name">Name</label>
@@ -44,11 +56,6 @@
44 </div> 56 </div>
45 </div> 57 </div>
46 58
47 <my-actor-avatar-info
48 *ngIf="!isCreation() && videoChannelToUpdate"
49 [actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
50 ></my-actor-avatar-info>
51
52 <div class="form-group"> 59 <div class="form-group">
53 <label i18n for="display-name">Display name</label> 60 <label i18n for="display-name">Display name</label>
54 <input 61 <input
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
index 8f8af655c..22de103d1 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
@@ -10,11 +10,16 @@ label {
10 @include settings-big-title; 10 @include settings-big-title;
11} 11}
12 12
13my-actor-avatar-info { 13my-actor-avatar-edit,
14my-actor-banner-edit {
14 display: block; 15 display: block;
15 margin-bottom: 20px; 16 margin-bottom: 20px;
16} 17}
17 18
19my-actor-banner-edit {
20 max-width: 500px;
21}
22
18.input-group { 23.input-group {
19 @include peertube-input-group(fit-content); 24 @include peertube-input-group(fit-content);
20} 25}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
index 3e20a27ee..33bb90f14 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
@@ -2,8 +2,7 @@ import { FormReactive } from '@app/shared/shared-forms'
2import { VideoChannel } from '@app/shared/shared-main' 2import { VideoChannel } from '@app/shared/shared-main'
3 3
4export abstract class MyVideoChannelEdit extends FormReactive { 4export abstract class MyVideoChannelEdit extends FormReactive {
5 // We need it even in the create component because it's used in the edit template 5 videoChannel: VideoChannel
6 videoChannelToUpdate: VideoChannel
7 6
8 abstract isCreation (): boolean 7 abstract isCreation (): boolean
9 abstract getFormButtonTitle (): string 8 abstract getFormButtonTitle (): string
@@ -12,10 +11,6 @@ export abstract class MyVideoChannelEdit extends FormReactive {
12 return window.location.host 11 return window.location.host
13 } 12 }
14 13
15 // We need this method so angular does not complain in child template that doesn't need this
16 onAvatarChange (formData: FormData) { /* empty */ }
17 onAvatarDelete () { /* empty */ }
18
19 // Should be implemented by the child 14 // Should be implemented by the child
20 isBulkUpdateVideosDisplayed () { 15 isBulkUpdateVideosDisplayed () {
21 return false 16 return false
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
index 6cd1ff503..a29af176c 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
@@ -1,7 +1,9 @@
1import { Subscription } from 'rxjs' 1import { Subscription } from 'rxjs'
2import { HttpErrorResponse } from '@angular/common/http'
2import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, Notifier, ServerService } from '@app/core' 5import { AuthService, Notifier, ServerService } from '@app/core'
6import { uploadErrorHandler } from '@app/helpers'
5import { 7import {
6 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, 8 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
7 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, 9 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
@@ -11,8 +13,6 @@ import { FormValidatorService } from '@app/shared/shared-forms'
11import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 13import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
12import { ServerConfig, VideoChannelUpdate } from '@shared/models' 14import { ServerConfig, VideoChannelUpdate } from '@shared/models'
13import { MyVideoChannelEdit } from './my-video-channel-edit' 15import { MyVideoChannelEdit } from './my-video-channel-edit'
14import { HttpErrorResponse } from '@angular/common/http'
15import { uploadErrorHandler } from '@app/helpers'
16 16
17@Component({ 17@Component({
18 selector: 'my-video-channel-update', 18 selector: 'my-video-channel-update',
@@ -21,7 +21,7 @@ import { uploadErrorHandler } from '@app/helpers'
21}) 21})
22export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy { 22export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy {
23 error: string 23 error: string
24 videoChannelToUpdate: VideoChannel 24 videoChannel: VideoChannel
25 25
26 private paramsSub: Subscription 26 private paramsSub: Subscription
27 private oldSupportField: string 27 private oldSupportField: string
@@ -56,7 +56,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
56 56
57 this.videoChannelService.getVideoChannel(videoChannelId).subscribe( 57 this.videoChannelService.getVideoChannel(videoChannelId).subscribe(
58 videoChannelToUpdate => { 58 videoChannelToUpdate => {
59 this.videoChannelToUpdate = videoChannelToUpdate 59 this.videoChannel = videoChannelToUpdate
60 60
61 this.oldSupportField = videoChannelToUpdate.support 61 this.oldSupportField = videoChannelToUpdate.support
62 62
@@ -87,7 +87,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
87 bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false 87 bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false
88 } 88 }
89 89
90 this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( 90 this.videoChannelService.updateVideoChannel(this.videoChannel.name, videoChannelUpdate).subscribe(
91 () => { 91 () => {
92 this.authService.refreshUserInformation() 92 this.authService.refreshUserInformation()
93 93
@@ -101,12 +101,12 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
101 } 101 }
102 102
103 onAvatarChange (formData: FormData) { 103 onAvatarChange (formData: FormData) {
104 this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData) 104 this.videoChannelService.changeVideoChannelImage(this.videoChannel.name, formData, 'avatar')
105 .subscribe( 105 .subscribe(
106 data => { 106 data => {
107 this.notifier.success($localize`Avatar changed.`) 107 this.notifier.success($localize`Avatar changed.`)
108 108
109 this.videoChannelToUpdate.updateAvatar(data.avatar) 109 this.videoChannel.updateAvatar(data.avatar)
110 }, 110 },
111 111
112 (err: HttpErrorResponse) => uploadErrorHandler({ 112 (err: HttpErrorResponse) => uploadErrorHandler({
@@ -118,12 +118,42 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
118 } 118 }
119 119
120 onAvatarDelete () { 120 onAvatarDelete () {
121 this.videoChannelService.deleteVideoChannelAvatar(this.videoChannelToUpdate.name) 121 this.videoChannelService.deleteVideoChannelImage(this.videoChannel.name, 'avatar')
122 .subscribe( 122 .subscribe(
123 data => { 123 data => {
124 this.notifier.success($localize`Avatar deleted.`) 124 this.notifier.success($localize`Avatar deleted.`)
125 125
126 this.videoChannelToUpdate.resetAvatar() 126 this.videoChannel.resetAvatar()
127 },
128
129 err => this.notifier.error(err.message)
130 )
131 }
132
133 onBannerChange (formData: FormData) {
134 this.videoChannelService.changeVideoChannelImage(this.videoChannel.name, formData, 'banner')
135 .subscribe(
136 data => {
137 this.notifier.success($localize`Banner changed.`)
138
139 this.videoChannel.updateBanner(data.banner)
140 },
141
142 (err: HttpErrorResponse) => uploadErrorHandler({
143 err,
144 name: $localize`banner`,
145 notifier: this.notifier
146 })
147 )
148 }
149
150 onBannerDelete () {
151 this.videoChannelService.deleteVideoChannelImage(this.videoChannel.name, 'banner')
152 .subscribe(
153 data => {
154 this.notifier.success($localize`Banner deleted.`)
155
156 this.videoChannel.resetBanner()
127 }, 157 },
128 158
129 err => this.notifier.error(err.message) 159 err => this.notifier.error(err.message)
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss
index f2f42459f..8804fa95c 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss
@@ -17,10 +17,11 @@ input[type=text] {
17 17
18.video-channel { 18.video-channel {
19 @include row-blocks; 19 @include row-blocks;
20
20 padding-bottom: 0; 21 padding-bottom: 0;
21 22
22 img { 23 img {
23 @include avatar(80px); 24 @include channel-avatar(80px);
24 25
25 margin-right: 10px; 26 margin-right: 10px;
26 } 27 }
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
index 92b56db49..53557ca02 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
@@ -1,5 +1,6 @@
1import { ChartModule } from 'primeng/chart' 1import { ChartModule } from 'primeng/chart'
2import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
3import { SharedActorImageModule } from '@app/shared/shared-actor-image'
3import { SharedFormModule } from '@app/shared/shared-forms' 4import { SharedFormModule } from '@app/shared/shared-forms'
4import { SharedGlobalIconModule } from '@app/shared/shared-icons' 5import { SharedGlobalIconModule } from '@app/shared/shared-icons'
5import { SharedMainModule } from '@app/shared/shared-main' 6import { SharedMainModule } from '@app/shared/shared-main'
@@ -16,7 +17,8 @@ import { MyVideoChannelsComponent } from './my-video-channels.component'
16 17
17 SharedMainModule, 18 SharedMainModule,
18 SharedFormModule, 19 SharedFormModule,
19 SharedGlobalIconModule 20 SharedGlobalIconModule,
21 SharedActorImageModule
20 ], 22 ],
21 23
22 declarations: [ 24 declarations: [
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 c180161e7..9dec64645 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
@@ -4,7 +4,7 @@
4</h1> 4</h1>
5 5
6<div class="top-buttons"> 6<div class="top-buttons">
7 <div> 7 <div class="search-wrapper">
8 <div class="input-group has-feedback has-clear"> 8 <div class="input-group has-feedback has-clear">
9 <input 9 <input
10 type="text" name="history-search" id="history-search" i18n-placeholder placeholder="Search your history" 10 type="text" name="history-search" id="history-search" i18n-placeholder placeholder="Search your history"
@@ -15,7 +15,7 @@
15 </div> 15 </div>
16 </div> 16 </div>
17 17
18 <div class="history-switch ml-auto mr-3"> 18 <div class="history-switch">
19 <my-input-switch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></my-input-switch> 19 <my-input-switch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></my-input-switch>
20 <label i18n>Track watch history</label> 20 <label i18n>Track watch history</label>
21 </div> 21 </div>
diff --git a/client/src/app/+my-library/my-history/my-history.component.scss b/client/src/app/+my-library/my-history/my-history.component.scss
index 928a8a3da..af4a34b4b 100644
--- a/client/src/app/+my-library/my-history/my-history.component.scss
+++ b/client/src/app/+my-library/my-history/my-history.component.scss
@@ -11,16 +11,24 @@
11 11
12.top-buttons { 12.top-buttons {
13 margin-bottom: 30px; 13 margin-bottom: 30px;
14 display: flex; 14 display: grid;
15 grid-template-columns: 250px 1fr auto auto;
15 align-items: center; 16 align-items: center;
16 flex-wrap: wrap;
17 17
18 #history-search { 18 .search-wrapper {
19 @include peertube-input-text(250px); 19 grid-column: 1;
20
21 input {
22 @include peertube-input-text(250px);
23 }
20 } 24 }
21 25
22 .history-switch { 26 .history-switch {
27 grid-column: 3;
28
23 display: flex; 29 display: flex;
30 margin-left: auto;
31 margin-right: 15px;
24 32
25 label { 33 label {
26 margin: 0 0 0 5px; 34 margin: 0 0 0 5px;
@@ -31,6 +39,8 @@
31 } 39 }
32 40
33 .delete-history { 41 .delete-history {
42 grid-column: 4;
43
34 @include peertube-button; 44 @include peertube-button;
35 @include grey-button; 45 @include grey-button;
36 @include button-with-icon; 46 @include button-with-icon;
@@ -40,26 +50,27 @@
40} 50}
41 51
42.video { 52.video {
43 @include row-blocks; 53 @include row-blocks($column-responsive: false);
44
45 .my-video-miniature {
46 flex-grow: 1;
47 }
48} 54}
49 55
50@media screen and (max-width: $mobile-view) { 56@media screen and (max-width: $small-view) {
51 .top-buttons { 57 .top-buttons {
52 .history-switch label, .delete-history { 58 grid-template-columns: auto 1fr auto;
53 @include ellipsis; 59 row-gap: 20px;
54 }
55 60
56 .history-switch label { 61 .history-switch {
57 width: 60%; 62 grid-row: 1;
63 grid-column: 1;
64 margin: 0;
58 } 65 }
59 66
60 .delete-history { 67 .delete-history {
61 margin-left: auto; 68 grid-row: 1;
62 max-width: 32%; 69 grid-column: 3;
70 }
71
72 .search-wrapper {
73 grid-column: 1 / 4;
63 } 74 }
64 } 75 }
65} 76}
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
index 510b400c0..ff448ad87 100644
--- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
+++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
@@ -6,7 +6,7 @@
6 </span> 6 </span>
7</h1> 7</h1>
8 8
9<div class="video-subscriptions-header d-flex justify-content-between"> 9<div class="video-subscriptions-header">
10 <div class="has-feedback has-clear"> 10 <div class="has-feedback has-clear">
11 <input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch" 11 <input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch"
12 (ngModelChange)="onSubscriptionsSearchChanged()" /> 12 (ngModelChange)="onSubscriptionsSearchChanged()" />
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss
index 5ead45dd8..3c1a4d2ad 100644
--- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss
+++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss
@@ -9,40 +9,40 @@ input[type=text] {
9 @include row-blocks; 9 @include row-blocks;
10 10
11 img { 11 img {
12 @include avatar(80px); 12 @include channel-avatar(80px);
13 13
14 margin-right: 10px; 14 margin-right: 10px;
15 } 15 }
16}
16 17
17 .video-channel-info { 18.video-channel-info {
18 flex-grow: 1; 19 flex-grow: 1;
19 20
20 a.video-channel-names { 21 a.video-channel-names {
21 @include disable-default-a-behaviour; 22 @include disable-default-a-behaviour;
22 23
23 width: fit-content; 24 width: fit-content;
24 display: flex; 25 display: flex;
25 align-items: baseline; 26 align-items: baseline;
26 color: pvar(--mainForegroundColor); 27 color: pvar(--mainForegroundColor);
27 28
28 .video-channel-display-name { 29 .video-channel-display-name {
29 font-weight: $font-semibold; 30 font-weight: $font-semibold;
30 font-size: 18px; 31 font-size: 18px;
31 } 32 }
32 33
33 .video-channel-name { 34 .video-channel-name {
34 font-size: 14px; 35 font-size: 14px;
35 color: $grey-actor-name; 36 color: $grey-actor-name;
36 margin-left: 5px; 37 margin-left: 5px;
37 }
38 } 38 }
39 } 39 }
40}
40 41
41 .actor-owner { 42.actor-owner {
42 @include actor-owner; 43 @include actor-owner;
43 44
44 margin-top: 0; 45 margin-top: 0;
45 }
46} 46}
47 47
48.video-subscriptions-header { 48.video-subscriptions-header {
@@ -50,32 +50,22 @@ input[type=text] {
50} 50}
51 51
52@media screen and (max-width: $small-view) { 52@media screen and (max-width: $small-view) {
53 .video-channel { 53 .video-subscriptions-header input[type=text] {
54 .video-channel-info { 54 width: 100% !important;
55 padding-bottom: 10px;
56 text-align: center;
57
58 .video-channel-names {
59 flex-direction: column;
60 align-items: center !important;
61 margin: auto;
62 }
63 }
64
65 img {
66 margin-right: 0;
67 }
68 } 55 }
69}
70 56
71@media screen and (max-width: $mobile-view) { 57 .video-channel-info {
72 .video-subscriptions-header { 58 padding-bottom: 10px;
73 flex-direction: column; 59 text-align: center;
74 60
75 input[type=text] { 61 .video-channel-names {
76 width: 100% !important; 62 flex-direction: column;
63 align-items: center !important;
64 margin: auto;
77 } 65 }
78 } 66 }
79}
80
81 67
68 img {
69 margin-right: 0;
70 }
71}
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html
index a97b2b4fb..e7e3c17b3 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html
@@ -1,6 +1,6 @@
1<div class="row"> 1<div class="root">
2 2
3 <div class="playlist-info col-xs-12 col-md-5 col-xl-3"> 3 <div class="playlist-info">
4 <my-video-playlist-miniature 4 <my-video-playlist-miniature
5 *ngIf="playlist" [playlist]="playlist" [toManage]="false" [displayChannel]="true" 5 *ngIf="playlist" [playlist]="playlist" [toManage]="false" [displayChannel]="true"
6 [displayDescription]="true" [displayPrivacy]="true" 6 [displayDescription]="true" [displayPrivacy]="true"
@@ -20,7 +20,7 @@
20 20
21 </div> 21 </div>
22 22
23 <div class="playlist-elements col-xs-12 col-md-7 col-xl-9"> 23 <div class="playlist-elements">
24 <div class="no-results" *ngIf="pagination.totalItems === 0"> 24 <div class="no-results" *ngIf="pagination.totalItems === 0">
25 <div i18n>No videos in this playlist.</div> 25 <div i18n>No videos in this playlist.</div>
26 26
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss
index de7e1993f..0c68dedf6 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss
@@ -2,21 +2,25 @@
2@import '_mixins'; 2@import '_mixins';
3@import '_miniature'; 3@import '_miniature';
4 4
5.root {
6 display: grid;
7 grid-template-columns: auto 1fr;
8}
9
5.playlist-info { 10.playlist-info {
6 background-color: pvar(--submenuColor); 11 grid-column: 1;
7 margin-left: -$not-expanded-horizontal-margins; 12 background-color: pvar(--submenuBackgroundColor);
13 margin-left: calc(#{pvar(--horizontalMarginContent)} * -1);
8 margin-top: -$sub-menu-margin-bottom; 14 margin-top: -$sub-menu-margin-bottom;
9 15
10 padding: 10px; 16 padding: 15px;
11 17
12 display: flex; 18 display: flex;
13 flex-direction: column; 19 flex-direction: column;
14 justify-content: flex-start;
15 align-items: center;
16 20
17 /* fix ellipsis dots background color */ 21 /* fix ellipsis dots background color */
18 ::ng-deep .miniature-name::after { 22 ::ng-deep .miniature-name::after {
19 background-color: pvar(--submenuColor) !important; 23 background-color: pvar(--submenuBackgroundColor) !important;
20 } 24 }
21} 25}
22 26
@@ -59,15 +63,35 @@
59 transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); 63 transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
60} 64}
61 65
62@media screen and (max-width: $small-view) { 66.playlist-elements {
67 grid-column: 2;
68}
69
70my-video-playlist-miniature {
71 width: $video-thumbnail-width;
72}
73
74@include on-small-main-col {
75 my-video-playlist-miniature {
76 width: $video-thumbnail-medium-width;
77 }
78}
79
80@include on-mobile-main-col {
81 .root {
82 display: block;
83 }
84
63 .playlist-info { 85 .playlist-info {
64 width: 100vw; 86 width: calc(100% + (2 * var(--horizontalMarginContent)));
65 padding-top: 20px; 87 padding-top: 20px;
66 margin-left: calc(#{var(--expanded-horizontal-margin-content)} * -1); 88 margin-bottom: 10px;
67 } 89 }
68 90
69 .playlist-elements { 91 my-video-playlist-miniature,
70 padding: 0 !important; 92 .playlist-buttons {
93 margin-left: auto;
94 margin-right: auto;
71 } 95 }
72 96
73 ::ng-deep my-video-playlist-element-miniature { 97 ::ng-deep my-video-playlist-element-miniature {
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 afcf6a084..b88ea3db7 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,8 +1,6 @@
1<h1> 1<h1>
2 <span> 2 <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon>
3 <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>
4 <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span>
5 </span>
6</h1> 4</h1>
7 5
8<div class="video-playlists-header d-flex justify-content-between"> 6<div class="video-playlists-header d-flex justify-content-between">
@@ -21,10 +19,10 @@
21 19
22<div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> 20<div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
23 <div *ngFor="let playlist of videoPlaylists" class="video-playlist"> 21 <div *ngFor="let playlist of videoPlaylists" class="video-playlist">
24 <div class="miniature-wrapper"> 22 <my-video-playlist-miniature
25 <my-video-playlist-miniature [playlist]="playlist" [toManage]="true" [displayChannel]="true" [displayDescription]="true" [displayPrivacy]="true" 23 [playlist]="playlist" [toManage]="true" [displayChannel]="true"
26 ></my-video-playlist-miniature> 24 [displayDescription]="true" [displayPrivacy]="true" [displayAsRow]="true"
27 </div> 25 ></my-video-playlist-miniature>
28 26
29 <div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons"> 27 <div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons">
30 <my-delete-button label (click)="deleteVideoPlaylist(playlist)"></my-delete-button> 28 <my-delete-button label (click)="deleteVideoPlaylist(playlist)"></my-delete-button>
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss
index 2b7c88246..94187efd4 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss
@@ -1,6 +1,10 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4h1 {
5 display: flex;
6}
7
4.create-button { 8.create-button {
5 @include create-button; 9 @include create-button;
6} 10}
@@ -9,64 +13,45 @@ input[type=text] {
9 @include peertube-input-text(300px); 13 @include peertube-input-text(300px);
10} 14}
11 15
12::ng-deep .action-button {
13 &.action-button-delete {
14 margin-right: 10px;
15 }
16}
17
18.video-playlist { 16.video-playlist {
19 @include row-blocks; 17 @include row-blocks($column-responsive: false);
20 18}
21 .miniature-wrapper {
22 flex-grow: 1;
23
24 ::ng-deep .miniature {
25 display: flex;
26
27 .miniature-info {
28 margin-left: 10px;
29 width: auto;
30 }
31 }
32 }
33 19
34 .video-playlist-buttons { 20.video-playlist-buttons {
35 min-width: 190px; 21 display: flex;
36 height: max-content; 22 margin-left: 10px;
37 } 23 align-self: flex-end;
38} 24}
39 25
40.video-playlists-header { 26.video-playlists-header {
41 margin-bottom: 30px; 27 margin-bottom: 30px;
42} 28}
43 29
44@media screen and (max-width: $small-view) { 30my-video-playlist-miniature {
31 display: block;
32 flex-grow: 1;
33}
34
35my-delete-button {
36 margin-right: 10px;
37}
38
39@include on-small-main-col {
45 .video-playlists-header { 40 .video-playlists-header {
46 text-align: center; 41 text-align: center;
47 } 42 }
48 43
49 .video-playlist { 44 .video-playlist {
50 45 flex-wrap: wrap;
51 .video-playlist-buttons {
52 margin-top: 10px;
53 }
54 } 46 }
55 47
56 my-video-playlist-miniature ::ng-deep .miniature { 48 .video-playlist-buttons {
57 flex-direction: column; 49 margin-top: 10px;
58 50 margin-left: auto;
59 .miniature-info {
60 margin-left: 0 !important;
61 }
62
63 .miniature-name {
64 max-width: $video-thumbnail-width;
65 }
66 } 51 }
67} 52}
68 53
69@media screen and (max-width: $mobile-view) { 54@include on-mobile-main-col {
70 .video-playlists-header { 55 .video-playlists-header {
71 flex-direction: column; 56 flex-direction: column;
72 57
@@ -75,4 +60,8 @@ input[type=text] {
75 margin-bottom: 12px; 60 margin-bottom: 12px;
76 } 61 }
77 } 62 }
63
64 .action-button {
65 margin-left: 0;
66 }
78} 67}
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 5fa4c02ec..e9f436378 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
@@ -25,6 +25,17 @@
25 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> 25 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
26 <span class="sr-only" i18n>Clear filters</span> 26 <span class="sr-only" i18n>Clear filters</span>
27 </div> 27 </div>
28
29 <div class="peertube-select-container peertube-select-button">
30 <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control">
31 <option value="undefined" disabled>Sort by</option>
32 <option value="-publishedAt" i18n>Last published first</option>
33 <option value="-createdAt" i18n>Last created first</option>
34 <option value="-views" i18n>Most viewed first</option>
35 <option value="-likes" i18n>Most liked first</option>
36 <option value="-duration" i18n>Longest first</option>
37 </select>
38 </div>
28</div> 39</div>
29 40
30<my-videos-selection 41<my-videos-selection
@@ -34,7 +45,6 @@
34 [miniatureDisplayOptions]="miniatureDisplayOptions" 45 [miniatureDisplayOptions]="miniatureDisplayOptions"
35 [titlePage]="titlePage" 46 [titlePage]="titlePage"
36 [getVideosObservableFunction]="getVideosObservableFunction" 47 [getVideosObservableFunction]="getVideosObservableFunction"
37 [ownerDisplayType]="ownerDisplayType"
38 [user]="user" 48 [user]="user"
39 #videosSelection 49 #videosSelection
40> 50>
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.scss b/client/src/app/+my-library/my-videos/my-videos.component.scss
index 59fc5fe80..aaf21126b 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.scss
+++ b/client/src/app/+my-library/my-videos/my-videos.component.scss
@@ -5,6 +5,11 @@ input[type=text] {
5 @include peertube-input-text(300px); 5 @include peertube-input-text(300px);
6} 6}
7 7
8.peertube-select-container {
9 @include peertube-select-container(auto);
10 margin-left: 0.5rem;
11}
12
8h1 { 13h1 {
9 display: flex; 14 display: flex;
10 justify-content: space-between; 15 justify-content: space-between;
@@ -32,36 +37,9 @@ h1 {
32 } 37 }
33} 38}
34 39
35::ng-deep {
36 .video {
37 flex-wrap: wrap;
38 }
39
40 .action-button span {
41 white-space: nowrap;
42 }
43
44 .video-miniature {
45 &.display-as-row {
46 // width: min-content !important;
47 width: 100% !important;
48
49 .video-bottom .video-miniature-information {
50 width: max-content !important;
51 min-width: unset !important;
52 }
53 }
54
55 .video-bottom {
56 max-width: 350px;
57 }
58 }
59}
60
61.action-button { 40.action-button {
62 display: flex; 41 display: flex;
63 margin-left: 55px; 42 margin-left: 10px;
64 margin-top: 10px;
65 align-self: flex-end; 43 align-self: flex-end;
66} 44}
67 45
@@ -69,7 +47,7 @@ my-edit-button {
69 margin-right: 10px; 47 margin-right: 10px;
70} 48}
71 49
72@media screen and (max-width: $small-view) { 50@include on-small-main-col {
73 h1 { 51 h1 {
74 flex-direction: column; 52 flex-direction: column;
75 53
@@ -80,59 +58,25 @@ my-edit-button {
80 } 58 }
81 59
82 .action-button { 60 .action-button {
83 flex-direction: column; 61 margin-top: 10px;
84 align-self: center; 62 margin-left: auto;
85 align-items: center;
86 margin-left: 0px;
87 }
88
89 my-edit-button {
90 margin: 15px 0 5px 0;
91 width: 100%;
92 text-align: center;
93
94 ::ng-deep {
95 .action-button {
96 /* same width than a.video-thumbnail */
97 width: $video-thumbnail-width;
98 }
99 }
100 }
101
102 ::ng-deep {
103 .video-miniature {
104 align-items: center;
105
106 .video-bottom,
107 .video-bottom .video-miniature-information {
108 /* same width than a.video-thumbnail */
109 max-width: $video-thumbnail-width !important;
110 }
111 }
112 } 63 }
113} 64}
114 65
115// Adapt my-video-miniature on small screens with menu 66@include on-mobile-main-col {
116@media screen and (min-width: $small-view) and (max-width: #{breakpoint(lg) + ($not-expanded-horizontal-margins / 3) * 2}) {
117 :host-context(.main-col:not(.expanded)) {
118 ::ng-deep {
119 .video-miniature {
120 flex-direction: column;
121
122 .video-miniature-name {
123 max-width: $video-thumbnail-width;
124 }
125 }
126 }
127 }
128}
129
130@media screen and (max-width: $mobile-view) {
131 .videos-header { 67 .videos-header {
132 flex-direction: column; 68 flex-direction: column;
133 69
134 input[type=text] { 70 input[type=text] {
135 width: 100% !important; 71 width: 100%;
72 margin-bottom: 12px;
73 }
74 .peertube-select-container {
75 margin-left: 0;
136 } 76 }
137 } 77 }
78
79 .action-button {
80 margin-left: 0;
81 }
138} 82}
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 6a2a62608..356e158d6 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
@@ -2,12 +2,12 @@ import { concat, Observable, Subject } from 'rxjs'
2import { debounceTime, tap, toArray } from 'rxjs/operators' 2import { debounceTime, tap, toArray } 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, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User, UserService } 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 { immutableAssign } from '@app/helpers'
8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
9import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' 9import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
10import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' 10import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature'
11import { VideoSortField } from '@shared/models' 11import { VideoSortField } from '@shared/models'
12import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' 12import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component'
13 13
@@ -36,7 +36,6 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
36 state: true, 36 state: true,
37 blacklistInfo: true 37 blacklistInfo: true
38 } 38 }
39 ownerDisplayType: OwnerDisplayType = 'videoChannel'
40 39
41 videoActions: DropdownAction<{ video: Video }>[] = [] 40 videoActions: DropdownAction<{ video: Video }>[] = []
42 41
@@ -44,6 +43,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
44 videosSearch: string 43 videosSearch: string
45 videosSearchChanged = new Subject<string>() 44 videosSearchChanged = new Subject<string>()
46 getVideosObservableFunction = this.getVideosObservable.bind(this) 45 getVideosObservableFunction = this.getVideosObservable.bind(this)
46 sort: VideoSortField = '-publishedAt'
47 47
48 user: User 48 user: User
49 49
@@ -81,6 +81,10 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
81 this.videosSearchChanged.next() 81 this.videosSearchChanged.next()
82 } 82 }
83 83
84 onChangeSortColumn () {
85 this.videosSelection.reloadVideos()
86 }
87
84 disableForReuse () { 88 disableForReuse () {
85 this.videosSelection.disableForReuse() 89 this.videosSelection.disableForReuse()
86 } 90 }
@@ -89,10 +93,10 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
89 this.videosSelection.enabledForReuse() 93 this.videosSelection.enabledForReuse()
90 } 94 }
91 95
92 getVideosObservable (page: number, sort: VideoSortField) { 96 getVideosObservable (page: number) {
93 const newPagination = immutableAssign(this.pagination, { currentPage: page }) 97 const newPagination = immutableAssign(this.pagination, { currentPage: page })
94 98
95 return this.videoService.getMyVideos(newPagination, sort, this.videosSearch) 99 return this.videoService.getMyVideos(newPagination, this.sort, this.videosSearch)
96 .pipe( 100 .pipe(
97 tap(res => this.pagination.totalItems = res.total) 101 tap(res => this.pagination.totalItems = res.total)
98 ) 102 )
diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html
index 84be4fb14..65d4b6ecd 100644
--- a/client/src/app/+search/search.component.html
+++ b/client/src/app/+search/search.component.html
@@ -2,14 +2,12 @@
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 i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}} </span> 5 <span class="mr-1" i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}}</span>
6 6
7 <span i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span> 7 <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span>
8 <span i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span> 8 <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span>
9 9
10 <span *ngIf="currentSearch" i18n> 10 <span *ngIf="currentSearch" i18n>for <span class="search-value">{{ currentSearch }}</span></span>
11 for <span class="search-value">{{ currentSearch }}</span>
12 </span>
13 </div> 11 </div>
14 12
15 <div 13 <div
@@ -35,11 +33,11 @@
35 33
36 <ng-container *ngFor="let result of results"> 34 <ng-container *ngFor="let result of results">
37 <div *ngIf="isVideoChannel(result)" class="entry video-channel"> 35 <div *ngIf="isVideoChannel(result)" class="entry video-channel">
38 <a *ngIf="!isExternalChannelUrl()" [routerLink]="getChannelUrl(result)"> 36 <a class="link-avatar" *ngIf="!isExternalChannelUrl()" [routerLink]="getChannelUrl(result)">
39 <img [src]="result.avatarUrl" alt="Avatar" /> 37 <img [src]="result.avatarUrl" alt="Avatar" />
40 </a> 38 </a>
41 39
42 <a *ngIf="isExternalChannelUrl()" [href]="getChannelUrl(result)" target="_blank"> 40 <a class="link-avatar" *ngIf="isExternalChannelUrl()" [href]="getChannelUrl(result)" target="_blank">
43 <img [src]="result.avatarUrl" alt="Avatar" /> 41 <img [src]="result.avatarUrl" alt="Avatar" />
44 </a> 42 </a>
45 43
diff --git a/client/src/app/+search/search.component.scss b/client/src/app/+search/search.component.scss
index 64927fa4b..91c8272d7 100644
--- a/client/src/app/+search/search.component.scss
+++ b/client/src/app/+search/search.component.scss
@@ -1,159 +1,122 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4@mixin build-channel-img-size ($video-img-width) {
5 $image-size: min(130px, $video-img-width);
6 $margin-size: ($video-img-width - $image-size) / 2; // So we have the same width than the video miniature
7
8 @include channel-avatar($image-size);
9
10 margin: 0 $margin-size 0 $margin-size;
11}
12
4.search-result { 13.search-result {
5 padding: 40px; 14 padding: 40px;
15}
6 16
7 .results-header { 17.results-header {
8 font-size: 16px; 18 font-size: 16px;
9 padding-bottom: 20px; 19 padding-bottom: 20px;
10 margin-bottom: 30px; 20 margin-bottom: 30px;
11 border-bottom: 1px solid #DADADA; 21 border-bottom: 1px solid #DADADA;
12 22
13 .first-line { 23 .first-line {
14 display: flex; 24 display: flex;
15 flex-direction: row; 25 flex-direction: row;
16 26
17 .results-counter { 27 .results-counter {
18 flex-grow: 1; 28 flex-grow: 1;
19 29
20 .search-value { 30 .search-value {
21 font-weight: $font-semibold; 31 font-weight: $font-semibold;
22 }
23 } 32 }
33 }
24 34
25 .results-filter-button { 35 .results-filter-button {
26 cursor: pointer; 36 cursor: pointer;
27 37
28 .icon.icon-filter { 38 .icon.icon-filter {
29 @include icon(20px); 39 @include icon(20px);
30 40
31 position: relative; 41 position: relative;
32 top: -1px; 42 top: -1px;
33 margin-right: 5px; 43 margin-right: 5px;
34 background-image: url('../../assets/images/feather/filter.svg'); 44 background-image: url('../../assets/images/feather/filter.svg');
35 }
36 } 45 }
37 } 46 }
38 } 47 }
48}
39 49
40 .entry { 50.entry {
41 display: flex; 51 display: flex;
42 min-height: 130px; 52 margin-bottom: 40px;
43 padding-bottom: 20px; 53 max-width: 800px;
44 margin-bottom: 20px; 54}
45 55
46 &.video-channel { 56.video-channel {
47 img { 57 img {
48 $image-size: 130px; 58 @include build-channel-img-size($video-thumbnail-width);
49 $margin-size: ($video-thumbnail-width - $image-size) / 2; // So we have the same width than the video miniature 59 }
60}
50 61
51 @include avatar($image-size); 62.video-channel-info {
63 flex-grow: 1;
64 margin: 0 10px;
65 width: fit-content;
66}
52 67
53 margin: 0 ($margin-size + 10) 0 $margin-size; 68.video-channel-names {
54 } 69 @include disable-default-a-behaviour;
55 70
56 .video-channel-info { 71 display: flex;
57 flex-grow: 1; 72 align-items: baseline;
58 width: fit-content; 73 color: pvar(--mainForegroundColor);
59 74 width: fit-content;
60 .video-channel-names {
61 @include disable-default-a-behaviour;
62
63 display: flex;
64 align-items: baseline;
65 color: pvar(--mainForegroundColor);
66 width: fit-content;
67
68 .video-channel-display-name {
69 font-weight: $font-semibold;
70 font-size: 18px;
71 }
72
73 .video-channel-name {
74 font-size: 14px;
75 color: $grey-actor-name;
76 margin-left: 5px;
77 }
78 }
79 }
80 }
81 }
82} 75}
83 76
84@media screen and (min-width: $small-view) and (max-width: breakpoint(xl)) { 77.video-channel-display-name {
85 .video-channel-info .video-channel-names { 78 font-weight: $font-semibold;
86 flex-direction: column !important; 79 font-size: $video-miniature-row-name-font-size;
80}
87 81
88 .video-channel-name { 82.video-channel-name {
89 @include ellipsis; // Ellipsis and max-width on channel-name to not break screen 83 font-size: $video-miniature-row-info-font-size;
84 color: pvar(--greyForegroundColor);
85 margin-left: 5px;
86}
90 87
91 max-width: 250px; 88// Use the same breakpoints than in video-miniature
92 margin-left: 0 !important; 89@include on-small-main-col {
93 } 90 .video-channel {
94 } 91 display: grid;
92 grid-template-columns: auto 1fr;
93 grid-template-rows: auto auto;
95 94
96 :host-context(.main-col:not(.expanded)) { 95 .link-avatar {
97 // Override the min-width: 500px to not break screen 96 grid-column: 1;
98 ::ng-deep .video-miniature-information { 97 grid-row: 1 / -1;
99 min-width: 300px !important;
100 } 98 }
101 }
102}
103 99
104@media screen and (min-width: $small-view) and (max-width: breakpoint(lg)) { 100 img {
105 :host-context(.main-col:not(.expanded)) { 101 @include build-channel-img-size($video-thumbnail-medium-width);
106 .video-channel-info .video-channel-names {
107 .video-channel-name {
108 max-width: 160px;
109 }
110 } 102 }
103 }
111 104
112 // Override the min-width: 500px to not break screen 105 .video-channel-info {
113 ::ng-deep .video-miniature-information { 106 grid-column: 2;
114 min-width: $video-thumbnail-width !important; 107 grid-row: 1;
115 }
116 } 108 }
117 109
118 :host-context(.expanded) { 110 my-subscribe-button {
119 // Override the min-width: 500px to not break screen 111 grid-column: 2;
120 ::ng-deep .video-miniature-information { 112 grid-row: 2;
121 min-width: 300px !important; 113 align-self: end;
122 }
123 } 114 }
124} 115}
125 116
126@media screen and (max-width: $small-view) { 117@include on-mobile-main-col {
127 .search-result { 118 .video-channel img {
128 .entry.video-channel, 119 @include build-channel-img-size($video-thumbnail-small-width);
129 .entry.video {
130 flex-direction: column;
131 height: auto;
132 justify-content: center;
133 align-items: center;
134 text-align: center;
135
136 img {
137 margin: 0;
138 }
139
140 img {
141 margin: 0;
142 }
143
144 .video-channel-info .video-channel-names {
145 align-items: center;
146 flex-direction: column !important;
147
148 .video-channel-name {
149 margin-left: 0 !important;
150 }
151 }
152
153 my-subscribe-button {
154 margin-top: 5px;
155 }
156 }
157 } 120 }
158} 121}
159 122
@@ -164,28 +127,13 @@
164 .results-header { 127 .results-header {
165 font-size: 15px !important; 128 font-size: 15px !important;
166 } 129 }
130 }
167 131
168 .entry { 132 .video-channel-display-name {
169 &.video { 133 font-size: $video-miniature-row-mobile-name-font-size;
170 .video-info-name, 134 }
171 .video-info-account { 135
172 margin: auto; 136 .video-channel-name {
173 } 137 font-size: $video-miniature-row-mobile-info-font-size;
174
175 my-video-thumbnail {
176 margin-right: 0 !important;
177
178 ::ng-deep .video-thumbnail {
179 width: 100%;
180 height: auto;
181
182 img {
183 width: 100%;
184 height: auto;
185 }
186 }
187 }
188 }
189 }
190 } 138 }
191} 139}
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.html b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.html
deleted file mode 100644
index 8dff8ba91..000000000
--- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.html
+++ /dev/null
@@ -1,22 +0,0 @@
1<div class="margin-content">
2 <div *ngIf="videoChannel" class="row no-gutters">
3 <div class="description col-md-6 col-sm-12 pr-2">
4 <div class="block">
5 <div i18n class="small-title">DESCRIPTION</div>
6 <div class="content" [innerHtml]="getVideoChannelDescription()"></div>
7 </div>
8
9 <div class="block" *ngIf="supportHTML">
10 <div i18n class="small-title">SUPPORT THIS CHANNEL</div>
11 <div class="content" [innerHtml]="supportHTML"></div>
12 </div>
13 </div>
14
15 <div class="stats col-md-6 col-sm-12">
16 <div class="block">
17 <div i18n class="small-title">STATS</div>
18 <div i18n class="content">Created {{ videoChannel.createdAt | date }}</div>
19 </div>
20 </div>
21 </div>
22</div>
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss
deleted file mode 100644
index 5bcd4b561..000000000
--- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss
+++ /dev/null
@@ -1,12 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4.block {
5 margin-bottom: 40px;
6
7 .small-title {
8 @include in-content-small-title;
9
10 margin-bottom: 20px;
11 }
12}
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts
deleted file mode 100644
index 537c7d08e..000000000
--- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts
+++ /dev/null
@@ -1,43 +0,0 @@
1import { Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { MarkdownService } from '@app/core'
4import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
5
6@Component({
7 selector: 'my-video-channel-about',
8 templateUrl: './video-channel-about.component.html',
9 styleUrls: [ './video-channel-about.component.scss' ]
10})
11export class VideoChannelAboutComponent implements OnInit, OnDestroy {
12 videoChannel: VideoChannel
13 descriptionHTML = ''
14 supportHTML = ''
15
16 private videoChannelSub: Subscription
17
18 constructor (
19 private videoChannelService: VideoChannelService,
20 private markdownService: MarkdownService
21 ) { }
22
23 ngOnInit () {
24 // Parent get the video channel for us
25 this.videoChannelSub = this.videoChannelService.videoChannelLoaded
26 .subscribe(async videoChannel => {
27 this.videoChannel = videoChannel
28
29 this.descriptionHTML = await this.markdownService.textMarkdownToHTML(this.videoChannel.description)
30 this.supportHTML = await this.markdownService.enhancedMarkdownToHTML(this.videoChannel.support)
31 })
32 }
33
34 ngOnDestroy () {
35 if (this.videoChannelSub) this.videoChannelSub.unsubscribe()
36 }
37
38 getVideoChannelDescription () {
39 if (this.descriptionHTML) return this.descriptionHTML
40
41 return $localize`No description`
42 }
43}
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html
index 03770ceec..b69d1682a 100644
--- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html
+++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html
@@ -1,13 +1,13 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <div i18n class="title-page title-page-single"> 2 <div i18n class="title-page title-page-single" *ngIf="pagination.totalItems">
3 Created {{ pagination.totalItems }} playlists 3 Created {pagination.totalItems, plural, =1 {1 playlist} other {{{ pagination.totalItems }} playlists}}
4 </div> 4 </div>
5 5
6 <div i18n class="no-results" *ngIf="pagination.totalItems === 0">This channel does not have playlists.</div> 6 <div i18n class="no-results" *ngIf="pagination.totalItems === 0">This channel does not have playlists.</div>
7 7
8 <div class="video-playlist" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()"> 8 <div class="playlists" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()">
9 <div *ngFor="let playlist of videoPlaylists" class="playlist-miniature-container"> 9 <div *ngFor="let playlist of videoPlaylists" class="playlist-wrapper">
10 <my-video-playlist-miniature [playlist]="playlist" [toManage]="false"></my-video-playlist-miniature> 10 <my-video-playlist-miniature [playlist]="playlist" [toManage]="false" [displayAsRow]="displayAsRow()"></my-video-playlist-miniature>
11 </div> 11 </div>
12 </div> 12 </div>
13</div> 13</div>
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss
index cb2931858..acd2e409e 100644
--- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss
+++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss
@@ -1,14 +1,32 @@
1.title-page { 1@import '_variables';
2 margin-top: 0; 2@import '_mixins';
3} 3@import '_miniature';
4 4
5.video-playlist { 5.playlists {
6 display: flex; 6 display: flex;
7 flex-wrap: wrap; 7 flex-wrap: wrap;
8 justify-content: center; 8 justify-content: center;
9 9
10 .playlist-miniature-container { 10 .playlist-wrapper {
11 margin-right: 15px; 11 margin-right: 15px;
12 margin-bottom: 30px; 12 margin-bottom: 30px;
13 } 13 }
14} 14}
15
16.margin-content {
17 @include grid-videos-miniature-layout;
18}
19
20@media screen and (max-width: $mobile-view) {
21 .title-page {
22 display: block;
23 text-align: center;
24 }
25
26 .playlists {
27 justify-content: left;
28
29 margin-left: pvar(--horizontalMarginContent) !important;
30 margin-right: pvar(--horizontalMarginContent) !important;
31 }
32}
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
index 8b507c626..14465bb8d 100644
--- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
+++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
@@ -1,6 +1,6 @@
1import { Subject, Subscription } from 'rxjs' 1import { Subject, Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core' 2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ComponentPagination, hasMoreItems } from '@app/core' 3import { ComponentPagination, hasMoreItems, ScreenService } from '@app/core'
4import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 4import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
5import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 5import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
6 6
@@ -25,7 +25,8 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy {
25 25
26 constructor ( 26 constructor (
27 private videoPlaylistService: VideoPlaylistService, 27 private videoPlaylistService: VideoPlaylistService,
28 private videoChannelService: VideoChannelService 28 private videoChannelService: VideoChannelService,
29 private screenService: ScreenService
29 ) {} 30 ) {}
30 31
31 ngOnInit () { 32 ngOnInit () {
@@ -48,6 +49,10 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy {
48 this.loadVideoPlaylists() 49 this.loadVideoPlaylists()
49 } 50 }
50 51
52 displayAsRow () {
53 return this.screenService.isInMobileView()
54 }
55
51 private loadVideoPlaylists () { 56 private loadVideoPlaylists () {
52 this.videoPlaylistService.listChannelPlaylists(this.videoChannel, this.pagination) 57 this.videoPlaylistService.listChannelPlaylists(this.videoChannel, this.pagination)
53 .subscribe(res => { 58 .subscribe(res => {
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
index 803651505..d83fc1324 100644
--- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
+++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
@@ -5,7 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core' 5import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
6import { immutableAssign } from '@app/helpers' 6import { immutableAssign } from '@app/helpers'
7import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' 7import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
8import { AbstractVideoList } from '@app/shared/shared-video-miniature' 8import { AbstractVideoList, MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
9import { VideoFilter } from '@shared/models' 9import { VideoFilter } from '@shared/models'
10 10
11@Component({ 11@Component({
@@ -16,12 +16,24 @@ import { VideoFilter } from '@shared/models'
16 ] 16 ]
17}) 17})
18export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { 18export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
19 // No value because we don't want a page title
19 titlePage: string 20 titlePage: string
20 loadOnInit = false 21 loadOnInit = false
21 loadUserVideoPreferences = true 22 loadUserVideoPreferences = true
22 23
23 filter: VideoFilter = null 24 filter: VideoFilter = null
24 25
26 displayOptions: MiniatureDisplayOptions = {
27 date: true,
28 views: true,
29 by: false,
30 avatar: false,
31 privacyLabel: true,
32 privacyText: false,
33 state: false,
34 blacklistInfo: false
35 }
36
25 private videoChannel: VideoChannel 37 private videoChannel: VideoChannel
26 private videoChannelSub: Subscription 38 private videoChannelSub: Subscription
27 39
@@ -83,13 +95,6 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
83 95
84 return this.videoService 96 return this.videoService
85 .getVideoChannelVideos(options) 97 .getVideoChannelVideos(options)
86 .pipe(
87 tap(({ total }) => {
88 this.titlePage = total === 1
89 ? $localize`Published 1 video`
90 : $localize`Published ${total} videos`
91 })
92 )
93 } 98 }
94 99
95 generateSyndicationList () { 100 generateSyndicationList () {
@@ -101,4 +106,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
101 106
102 this.reloadVideos() 107 this.reloadVideos()
103 } 108 }
109
110 displayAsRow () {
111 return this.screenService.isInMobileView()
112 }
104} 113}
diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts
index f8c32f14e..fcaad8934 100644
--- a/client/src/app/+video-channels/video-channels-routing.module.ts
+++ b/client/src/app/+video-channels/video-channels-routing.module.ts
@@ -1,7 +1,6 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core' 3import { MetaGuard } from '@ngx-meta/core'
4import { VideoChannelAboutComponent } from './video-channel-about/video-channel-about.component'
5import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' 4import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
6import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' 5import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
7import { VideoChannelsComponent } from './video-channels.component' 6import { VideoChannelsComponent } from './video-channels.component'
@@ -38,15 +37,6 @@ const videoChannelsRoutes: Routes = [
38 title: $localize`Video channel playlists` 37 title: $localize`Video channel playlists`
39 } 38 }
40 } 39 }
41 },
42 {
43 path: 'about',
44 component: VideoChannelAboutComponent,
45 data: {
46 meta: {
47 title: $localize`About video channel`
48 }
49 }
50 } 40 }
51 ] 41 ]
52 } 42 }
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index 4b0d12b6e..1312a1b3c 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -1,50 +1,129 @@
1<div *ngIf="videoChannel" class="row"> 1<div class="root" *ngIf="videoChannel">
2 <div class="sub-menu"> 2 <div class="banner" *ngIf="videoChannel.bannerUrl">
3 3 <img [src]="videoChannel.bannerUrl" alt="Channel banner">
4 <div class="actor"> 4 </div>
5 <img [src]="videoChannel.avatarUrl" alt="Avatar" /> 5
6 6 <div class="channel-info">
7 <div class="actor-info"> 7
8 <div class="actor-names"> 8 <ng-template #buttonsTemplate>
9 <div class="actor-display-name">{{ videoChannel.displayName }}</div> 9 <a *ngIf="isManageable()" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n>
10 <div class="actor-name"> 10 Manage channel
11 <span>{{ videoChannel.nameWithHost }}</span> 11 </a>
12 <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()" 12
13 class="btn btn-outline-secondary btn-sm copy-button" 13 <my-subscribe-button *ngIf="!isManageable()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
14 > 14
15 <span class="glyphicon glyphicon-copy"></span> 15 <button *ngIf="videoChannel.support" (click)="showSupportModal()" class="support-button peertube-button orange-button-inverted">
16 </button> 16 <my-global-icon iconName="support" aria-hidden="true"></my-global-icon>
17 <span class="icon-text" i18n>Support</span>
18 </button>
19 </ng-template>
20
21 <ng-template #ownerTemplate>
22 <div class="owner-block">
23 <div class="avatar-row">
24 <a [routerLink]="getAccountUrl()" title="View account" i18n-title>
25 <img class="account-avatar" [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
26 </a>
27
28 <div class="actor-info">
29 <h4>
30 <a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ videoChannel.ownerAccount.displayName }}</a>
31 </h4>
32
33 <div class="actor-handle">@{{ videoChannel.ownerBy }}</div>
17 </div> 34 </div>
18 </div> 35 </div>
19 36
20 <div class="right-buttons"> 37 <div class="owner-description">
21 <a *ngIf="isChannelManageable && !isInSmallView" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n> 38 <div class="description-html" [innerHTML]="ownerDescriptionHTML"></div>
22 Manage channel
23 </a>
24 <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
25 </div> 39 </div>
26 40
27 <div class="actor-lower"> 41 <a class="view-account short" [routerLink]="getAccountUrl()" i18n>
28 <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> 42 View account
43 </a>
29 44
30 <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner"> 45 <a class="view-account complete" [routerLink]="getAccountUrl()" i18n>
31 <span class="d-inline-flex"><span i18n class="d-none d-sm-block mr-1">Created by</span>{{ videoChannel.ownerBy }}</span> 46 View owner account
32 <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> 47 </a>
33 </a> 48 </div>
49 </ng-template>
50
51 <div class="channel-avatar-row">
52 <img class="channel-avatar" [src]="videoChannel.avatarUrl" alt="Avatar" />
53
54 <div>
55 <div class="section-label" i18n>VIDEO CHANNEL</div>
56
57 <div class="actor-info">
58 <div>
59 <div class="actor-display-name">
60 <h1>{{ videoChannel.displayName }}</h1>
61 </div>
62
63 <div class="actor-handle">
64 <span>@{{ videoChannel.nameWithHost }}</span>
65 <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()"
66 class="btn btn-outline-secondary btn-sm copy-button" title="Copy channel handle" i18n-title
67 >
68 <span class="glyphicon glyphicon-duplicate"></span>
69 </button>
70 </div>
71
72 <div class="actor-counters">
73 <span i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span>
74
75 <span class="videos-count" *ngIf="channelVideosCount !== undefined" i18n>
76 {channelVideosCount, plural, =1 {1 videos} other {{{ channelVideosCount }} videos}}
77 </span>
78 </div>
79 </div>
80
81 <div class="channel-buttons right">
82 <ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template>
83 </div>
34 </div> 84 </div>
35 </div> 85 </div>
36 </div> 86 </div>
37 87
38 <div class="links w-100"> 88 <div class="channel-description" [ngClass]="{ expanded: channelDescriptionExpanded }">
39 <ng-template #linkTemplate let-item="item"> 89 <div class="description-html" [innerHTML]="channelDescriptionHTML"></div>
40 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
41 </ng-template>
42 90
43 <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> 91 <div class="created-at" i18n>Channel created on {{ videoChannel.createdAt | date }}</div>
44 </div> 92 </div>
93
94 <div *ngIf="hasShowMoreDescription()" class="show-more" role="button"
95 (click)="channelDescriptionExpanded = !channelDescriptionExpanded"
96 title="Show the complete description" i18n-title i18n
97 >
98 Show more...
99 </div>
100
101 <div class="channel-buttons bottom">
102 <ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template>
103 </div>
104
105 <div class="owner-card">
106 <div class="section-label" i18n>OWNER ACCOUNT</div>
107
108 <ng-template *ngTemplateOutlet="ownerTemplate"></ng-template>
109 </div>
110 </div>
111
112 <div class="bottom-owner">
113 <div class="section-label" i18n>OWNER ACCOUNT</div>
114
115 <ng-template *ngTemplateOutlet="ownerTemplate"></ng-template>
45 </div> 116 </div>
46 117
47 <div class="margin-content"> 118 <div class="links">
48 <router-outlet></router-outlet> 119 <ng-template #linkTemplate let-item="item">
120 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
121 </ng-template>
122
123 <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
49 </div> 124 </div>
125
126 <router-outlet></router-outlet>
50</div> 127</div>
128
129<my-support-modal #supportModal [videoChannel]="videoChannel"></my-support-modal>
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss
index 22f21dcc6..b19b4c81b 100644
--- a/client/src/app/+video-channels/video-channels.component.scss
+++ b/client/src/app/+video-channels/video-channels.component.scss
@@ -1,89 +1,308 @@
1// Bootstrap grid utilities require functions, variables and mixins
2@import 'node_modules/bootstrap/scss/functions';
3@import 'node_modules/bootstrap/scss/variables';
4@import 'node_modules/bootstrap/scss/mixins';
5@import 'node_modules/bootstrap/scss/grid';
6
7@import '_variables'; 1@import '_variables';
8@import '_mixins'; 2@import '_mixins';
3@import '_actor';
4@import '_miniature';
9 5
10.sub-menu { 6.root {
11 @include sub-menu-with-actor; 7 --myGlobalTopPadding: 60px;
8 --myChannelImgMargin: 30px;
9 --myFontSize: 16px;
10 --myGreyChannelFontSize: 16px;
11 --myGreyOwnerFontSize: 14px;
12}
12 13
13 .actor, .actor-info { 14.banner {
14 width: 100%; 15 @include block-ratio('img', $banner-inverted-ratio);
15 } 16}
16 17
17 .actor-info { 18.section-label {
18 display: grid !important; 19 @include section-label-responsive;
19 grid-template-columns: 1fr auto; 20}
20 grid-template-rows: 1fr auto / 1fr auto;
21 grid-template-areas: "name buttons" "lower buttons";
22 21
23 @include media-breakpoint-down(lg) { 22.links {
24 grid-template-areas: "name name" "lower buttons"; 23 @include grid-videos-miniature-margins;
25 } 24}
25
26.actor-info {
27 min-width: 1px;
28 width: 100%;
29
30 > h4,
31 > .actor-handle {
32 @include ellipsis;
26 } 33 }
34}
35
36.channel-info {
37 @include grid-videos-miniature-margins(false, 15px);
38
39 display: grid;
40 grid-template-columns: 1fr auto;
41 grid-template-rows: auto auto;
42
43 background-color: pvar(--channelBackgroundColor);
44 margin-bottom: 45px;
45 padding-top: var(--myGlobalTopPadding);
46 font-size: var(--myFontSize);
47}
48
49.channel-avatar-row {
50 @include avatar-row-responsive(var(--myChannelImgMargin), var(--myGreyChannelFontSize));
51}
52
53.support-button {
54 @include button-with-icon(21px, 0, -1px);
55}
56
57.channel-description {
58 grid-column: 1;
59 word-break: break-word;
60}
61
62.show-more {
63 @include show-more-description;
64
65 display: none;
66}
67
68.channel-buttons {
69 display: flex;
70 flex-wrap: wrap;
27 71
28 .actor-names { 72 > *:not(:last-child) {
29 grid-area: name; 73 margin-right: 15px;
30 } 74 }
75}
76
77.channel-buttons.right {
78 margin-left: 45px;
79}
80
81// Only used by mobile
82.channel-buttons.bottom {
83 display: none;
84}
85
86.created-at {
87 margin-top: 15px;
88 color: pvar(--greyForegroundColor);
89 padding-bottom: 60px;
90}
91
92.owner-card {
93 margin-left: 105px;
94 grid-column: 2;
95 // Takes all the column
96 grid-row: 1 / 3;
97 place-self: end;
98}
99
100// Only used on mobile
101.bottom-owner {
102 display: none;
103}
104
105.owner-block {
106 background-color: pvar(--mainBackgroundColor);
107 padding: 30px;
108 width: 300px;
109 font-size: var(--myFontSize);
31 110
32 .actor-name { 111 .avatar-row {
33 flex-grow: 1; 112 display: flex;
113 margin-bottom: 15px;
34 114
35 .copy-button { 115 img {
36 border: none; 116 @include avatar(48px);
37 padding: 5px;
38 margin-top: -2px;
39 } 117 }
118
119 .actor-info {
120 margin-left: 15px;
121 }
122
123 h4 {
124 font-size: 18px;
125 margin: 0;
126
127 a {
128 color: pvar(--mainForegroundColor);
129 }
130 }
131
132 .actor-handle {
133 font-size: var(--myGreyOwnerFontSize);
134 color: pvar(--greyForegroundColor);
135 }
136 }
137
138 .owner-description {
139 max-height: 140px;
140 word-break: break-word;
141
142 @include fade-text(120px, pvar(--mainBackgroundColor));
40 } 143 }
41} 144}
42 145
43.margin-content { 146.view-account.short {
44 // margin-content is required, but child views have their own margins 147 @include peertube-button-link;
45 // that match views outside the scope of accounts, so we only align 148 @include orange-button-inverted;
46 // them with the margins of .sub-menu when required. 149
47 margin: 0; 150 margin-top: 30px;
151}
152
153.view-account.complete {
154 display: none;
48} 155}
49 156
50.right-buttons { 157.copy-button {
51 display: flex; 158 border: none;
52 height: max-content; 159}
53 margin-left: auto; 160
54 margin-top: 10px; 161@media screen and (max-width: 1400px) {
162 // Takes all the row width
163 .channel-avatar-row {
164 grid-column: 1 / 3;
165 }
166
167 .owner-card {
168 grid-row: 2;
169 margin-left: 60px;
170 }
171}
172
173@media screen and (max-width: 1100px) {
174 .root {
175 --myGlobalTopPadding: 45px;
176 --myChannelImgMargin: 15px;
177 }
178
179 .channel-info {
180 display: flex;
181 flex-direction: column;
182 margin-bottom: 0;
183 }
184
185 .channel-description:not(.expanded) {
186 max-height: 70px;
187
188 @include fade-text(30px, pvar(--channelBackgroundColor));
189 }
190
191 .show-more {
192 display: inline-block;
193 }
194
195 .channel-buttons.bottom {
196 display: flex;
197 justify-content: center;
198 margin-bottom: 30px;
199 }
200
201 .channel-buttons.right {
202 display: none;
203 }
204
205 .owner-card {
206 display: none;
207 }
208
209 .bottom-owner {
210 display: block;
211 width: 100%;
212 border-bottom: 2px solid $separator-border-color;
213 padding: var(--myGlobalTopPadding) 45px;
214 margin-bottom: 60px;
215 }
55 216
56 grid-row: buttons-start / span buttons-end; 217 .owner-block {
57 grid-column: buttons-start; 218 display: grid;
219 width: 100%;
220 padding: 0;
58 221
59 @include media-breakpoint-down(lg) { 222 .avatar-row {
60 flex-flow: column-reverse; 223 grid-column: 1;
224 margin-right: 30px;
225 }
226
227 .owner-description {
228 grid-column: 2;
229 max-height: 70px;
230
231 @include fade-text(30px, pvar(--mainBackgroundColor));
232 }
61 233
62 a { 234 .view-account {
63 margin-top: 0.25rem; 235 grid-column: 2;
64 margin-right: 0 !important;
65 } 236 }
66 } 237 }
67 238
68 a { 239 .view-account.complete {
69 @include peertube-button-outline; 240 display: block;
70 line-height: 1.8; 241 text-align: right;
242 margin-top: 10px;
243 color: pvar(--mainColor);
71 } 244 }
72 245
73 my-subscribe-button { 246 .view-account.short {
74 height: min-content; 247 display: none;
75 } 248 }
76} 249}
77 250
78@media screen and (max-width: $mobile-view) { 251@media screen and (max-width: $mobile-view) {
79 .sub-menu { 252 .root {
80 .actor { 253 --myGlobalTopPadding: 15px;
81 flex-direction: column; 254 --myFontSize: 14px;
255 --myGreyChannelFontSize: 13px;
256 --myGreyOwnerFontSize: 13px;
257 }
258
259 .links {
260 margin: auto !important;
261 width: min-content;
262 }
263
264 .show-more {
265 margin-bottom: 30px;
266 }
267
268 .bottom-owner {
269 padding: 15px;
270 margin-bottom: 30px;
82 271
83 .actor-info .actor-names { 272 .section-label {
273 display: none;
274 }
275 }
276
277 .owner-block {
278 display: block;
279
280 .avatar-row {
281 display: flex;
282 flex-direction: row-reverse;
283 margin: 0;
284
285 h4 {
286 font-size: 16px;
287 }
288
289 .actor-info {
290 display: flex;
84 flex-direction: column; 291 flex-direction: column;
85 align-items: normal; 292 align-items: flex-end;
293 justify-content: flex-end;
294 margin-top: -5px;
295 }
296
297 img {
298 @include channel-avatar(64px);
299
300 margin: -30px 0 0 15px;
86 } 301 }
87 } 302 }
303
304 .owner-description {
305 display: none;
306 }
88 } 307 }
89} 308}
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index bb601e227..41fdb5e79 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -3,8 +3,9 @@ import { Subscription } from 'rxjs'
3import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators' 3import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators'
4import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' 4import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
5import { ActivatedRoute } from '@angular/router' 5import { ActivatedRoute } from '@angular/router'
6import { AuthService, Notifier, RestExtractor, ScreenService } from '@app/core' 6import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core'
7import { ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' 7import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
8import { SupportModalComponent } from '@app/shared/shared-support-modal'
8import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 9import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
9import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 10import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
10 11
@@ -14,12 +15,18 @@ import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
14}) 15})
15export class VideoChannelsComponent implements OnInit, OnDestroy { 16export class VideoChannelsComponent implements OnInit, OnDestroy {
16 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent 17 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
18 @ViewChild('supportModal') supportModal: SupportModalComponent
17 19
18 videoChannel: VideoChannel 20 videoChannel: VideoChannel
19 hotkeys: Hotkey[] 21 hotkeys: Hotkey[]
20 links: ListOverflowItem[] = [] 22 links: ListOverflowItem[] = []
21 isChannelManageable = false 23 isChannelManageable = false
22 24
25 channelVideosCount: number
26 ownerDescriptionHTML = ''
27 channelDescriptionHTML = ''
28 channelDescriptionExpanded = false
29
23 private routeSub: Subscription 30 private routeSub: Subscription
24 31
25 constructor ( 32 constructor (
@@ -27,9 +34,11 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
27 private notifier: Notifier, 34 private notifier: Notifier,
28 private authService: AuthService, 35 private authService: AuthService,
29 private videoChannelService: VideoChannelService, 36 private videoChannelService: VideoChannelService,
37 private videoService: VideoService,
30 private restExtractor: RestExtractor, 38 private restExtractor: RestExtractor,
31 private hotkeysService: HotkeysService, 39 private hotkeysService: HotkeysService,
32 private screenService: ScreenService 40 private screenService: ScreenService,
41 private markdown: MarkdownService
33 ) { } 42 ) { }
34 43
35 ngOnInit () { 44 ngOnInit () {
@@ -43,16 +52,14 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
43 HttpStatusCode.NOT_FOUND_404 52 HttpStatusCode.NOT_FOUND_404
44 ])) 53 ]))
45 ) 54 )
46 .subscribe(videoChannel => { 55 .subscribe(async videoChannel => {
56 this.channelDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.description)
57 this.ownerDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.ownerAccount.description)
58
59 // After the markdown renderer to avoid layout changes
47 this.videoChannel = videoChannel 60 this.videoChannel = videoChannel
48 61
49 if (this.authService.isLoggedIn()) { 62 this.loadChannelVideosCount()
50 this.authService.userInformationLoaded
51 .subscribe(() => {
52 const channelUserId = this.videoChannel.ownerAccount.userId
53 this.isChannelManageable = channelUserId && channelUserId === this.authService.getUser().id
54 })
55 }
56 }) 63 })
57 64
58 this.hotkeys = [ 65 this.hotkeys = [
@@ -67,8 +74,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
67 74
68 this.links = [ 75 this.links = [
69 { label: $localize`VIDEOS`, routerLink: 'videos' }, 76 { label: $localize`VIDEOS`, routerLink: 'videos' },
70 { label: $localize`VIDEO PLAYLISTS`, routerLink: 'video-playlists' }, 77 { label: $localize`PLAYLISTS`, routerLink: 'video-playlists' }
71 { label: $localize`ABOUT`, routerLink: 'about' }
72 ] 78 ]
73 } 79 }
74 80
@@ -79,7 +85,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
79 if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys) 85 if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys)
80 } 86 }
81 87
82 get isInSmallView () { 88 isInSmallView () {
83 return this.screenService.isInSmallView() 89 return this.screenService.isInSmallView()
84 } 90 }
85 91
@@ -87,12 +93,36 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
87 return this.authService.isLoggedIn() 93 return this.authService.isLoggedIn()
88 } 94 }
89 95
90 get isManageable () { 96 isManageable () {
91 if (!this.isUserLoggedIn()) return false 97 if (!this.isUserLoggedIn()) return false
92 return this.videoChannel.ownerAccount.userId === this.authService.getUser().id 98
99 return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id
93 } 100 }
94 101
95 activateCopiedMessage () { 102 activateCopiedMessage () {
96 this.notifier.success($localize`Username copied`) 103 this.notifier.success($localize`Username copied`)
97 } 104 }
105
106 hasShowMoreDescription () {
107 return !this.channelDescriptionExpanded && this.channelDescriptionHTML.length > 100
108 }
109
110 showSupportModal () {
111 this.supportModal.show()
112 }
113
114 getAccountUrl () {
115 return [ '/accounts', this.videoChannel.ownerBy ]
116 }
117
118 private loadChannelVideosCount () {
119 this.videoService.getVideoChannelVideos({
120 videoChannel: this.videoChannel,
121 videoPagination: {
122 currentPage: 1,
123 itemsPerPage: 0
124 },
125 sort: '-publishedAt'
126 }).subscribe(res => this.channelVideosCount = res.total)
127 }
98} 128}
diff --git a/client/src/app/+video-channels/video-channels.module.ts b/client/src/app/+video-channels/video-channels.module.ts
index 05236ff85..408f86225 100644
--- a/client/src/app/+video-channels/video-channels.module.ts
+++ b/client/src/app/+video-channels/video-channels.module.ts
@@ -2,10 +2,10 @@ import { NgModule } from '@angular/core'
2import { SharedFormModule } from '@app/shared/shared-forms' 2import { SharedFormModule } from '@app/shared/shared-forms'
3import { SharedGlobalIconModule } from '@app/shared/shared-icons' 3import { SharedGlobalIconModule } from '@app/shared/shared-icons'
4import { SharedMainModule } from '@app/shared/shared-main' 4import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedSupportModal } from '@app/shared/shared-support-modal'
5import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
6import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
7import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' 8import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
8import { VideoChannelAboutComponent } from './video-channel-about/video-channel-about.component'
9import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' 9import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
10import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' 10import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
11import { VideoChannelsRoutingModule } from './video-channels-routing.module' 11import { VideoChannelsRoutingModule } from './video-channels-routing.module'
@@ -20,13 +20,13 @@ import { VideoChannelsComponent } from './video-channels.component'
20 SharedVideoPlaylistModule, 20 SharedVideoPlaylistModule,
21 SharedVideoMiniatureModule, 21 SharedVideoMiniatureModule,
22 SharedUserSubscriptionModule, 22 SharedUserSubscriptionModule,
23 SharedGlobalIconModule 23 SharedGlobalIconModule,
24 SharedSupportModal
24 ], 25 ],
25 26
26 declarations: [ 27 declarations: [
27 VideoChannelsComponent, 28 VideoChannelsComponent,
28 VideoChannelVideosComponent, 29 VideoChannelVideosComponent,
29 VideoChannelAboutComponent,
30 VideoChannelPlaylistsComponent 30 VideoChannelPlaylistsComponent
31 ], 31 ],
32 32
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
index 8780ca567..8e035b6bb 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
@@ -1,8 +1,8 @@
1 1
2import { forkJoin } from 'rxjs' 2import { forkJoin } from 'rxjs'
3import { Component, EventEmitter, OnInit, Output } from '@angular/core' 3import { AfterViewChecked, AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'
4import { Router } from '@angular/router' 4import { Router } from '@angular/router'
5import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' 5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
6import { scrollToTop } from '@app/helpers' 6import { scrollToTop } from '@app/helpers'
7import { FormValidatorService } from '@app/shared/shared-forms' 7import { FormValidatorService } from '@app/shared/shared-forms'
8import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 8import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
@@ -19,7 +19,7 @@ import { VideoSend } from './video-send'
19 './video-send.scss' 19 './video-send.scss'
20 ] 20 ]
21}) 21})
22export class VideoGoLiveComponent extends VideoSend implements OnInit, CanComponentDeactivate { 22export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate {
23 @Output() firstStepDone = new EventEmitter<string>() 23 @Output() firstStepDone = new EventEmitter<string>()
24 @Output() firstStepError = new EventEmitter<void>() 24 @Output() firstStepError = new EventEmitter<void>()
25 25
@@ -41,7 +41,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon
41 protected videoService: VideoService, 41 protected videoService: VideoService,
42 protected videoCaptionService: VideoCaptionService, 42 protected videoCaptionService: VideoCaptionService,
43 private liveVideoService: LiveVideoService, 43 private liveVideoService: LiveVideoService,
44 private router: Router 44 private router: Router,
45 private hooks: HooksService
45 ) { 46 ) {
46 super() 47 super()
47 } 48 }
@@ -50,6 +51,10 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon
50 super.ngOnInit() 51 super.ngOnInit()
51 } 52 }
52 53
54 ngAfterViewInit () {
55 this.hooks.runAction('action:go-live.init', 'video-edit')
56 }
57
53 canDeactivate () { 58 canDeactivate () {
54 return { canDeactivate: true } 59 return { canDeactivate: true }
55 } 60 }
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss
index 1fef74994..dd87641fc 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss
@@ -3,8 +3,8 @@
3 3
4.first-step-block { 4.first-step-block {
5 .torrent-or-magnet { 5 .torrent-or-magnet {
6 @include divider($color: pvar(--inputPlaceholderColor), $background: pvar(--submenuColor)); 6 @include divider($color: pvar(--inputPlaceholderColor), $background: pvar(--submenuBackgroundColor));
7 7
8 &[data-content] { 8 &[data-content] {
9 margin: 1.5rem 0; 9 margin: 1.5rem 0;
10 } 10 }
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
index 01087e525..3aae24732 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -1,6 +1,6 @@
1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { AfterViewInit, Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' 3import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
4import { scrollToTop } from '@app/helpers' 4import { scrollToTop } from '@app/helpers'
5import { FormValidatorService } from '@app/shared/shared-forms' 5import { FormValidatorService } from '@app/shared/shared-forms'
6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
@@ -18,7 +18,7 @@ import { VideoSend } from './video-send'
18 './video-send.scss' 18 './video-send.scss'
19 ] 19 ]
20}) 20})
21export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { 21export class VideoImportTorrentComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate {
22 @Output() firstStepDone = new EventEmitter<string>() 22 @Output() firstStepDone = new EventEmitter<string>()
23 @Output() firstStepError = new EventEmitter<void>() 23 @Output() firstStepError = new EventEmitter<void>()
24 @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement> 24 @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
@@ -43,7 +43,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
43 protected videoService: VideoService, 43 protected videoService: VideoService,
44 protected videoCaptionService: VideoCaptionService, 44 protected videoCaptionService: VideoCaptionService,
45 private router: Router, 45 private router: Router,
46 private videoImportService: VideoImportService 46 private videoImportService: VideoImportService,
47 private hooks: HooksService
47 ) { 48 ) {
48 super() 49 super()
49 } 50 }
@@ -52,6 +53,10 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
52 super.ngOnInit() 53 super.ngOnInit()
53 } 54 }
54 55
56 ngAfterViewInit () {
57 this.hooks.runAction('action:video-torrent-import.init', 'video-edit')
58 }
59
55 canDeactivate () { 60 canDeactivate () {
56 return { canDeactivate: true } 61 return { canDeactivate: true }
57 } 62 }
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
index c447c179d..7a9fe369f 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
@@ -1,7 +1,7 @@
1import { map, switchMap } from 'rxjs/operators' 1import { map, switchMap } from 'rxjs/operators'
2import { Component, EventEmitter, OnInit, Output } from '@angular/core' 2import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' 4import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
5import { getAbsoluteAPIUrl, scrollToTop } from '@app/helpers' 5import { getAbsoluteAPIUrl, scrollToTop } from '@app/helpers'
6import { FormValidatorService } from '@app/shared/shared-forms' 6import { FormValidatorService } from '@app/shared/shared-forms'
7import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 7import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
@@ -18,7 +18,7 @@ import { VideoSend } from './video-send'
18 './video-send.scss' 18 './video-send.scss'
19 ] 19 ]
20}) 20})
21export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { 21export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate {
22 @Output() firstStepDone = new EventEmitter<string>() 22 @Output() firstStepDone = new EventEmitter<string>()
23 @Output() firstStepError = new EventEmitter<void>() 23 @Output() firstStepError = new EventEmitter<void>()
24 24
@@ -42,8 +42,9 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
42 protected videoService: VideoService, 42 protected videoService: VideoService,
43 protected videoCaptionService: VideoCaptionService, 43 protected videoCaptionService: VideoCaptionService,
44 private router: Router, 44 private router: Router,
45 private videoImportService: VideoImportService 45 private videoImportService: VideoImportService,
46 ) { 46 private hooks: HooksService
47 ) {
47 super() 48 super()
48 } 49 }
49 50
@@ -51,6 +52,10 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
51 super.ngOnInit() 52 super.ngOnInit()
52 } 53 }
53 54
55 ngAfterViewInit () {
56 this.hooks.runAction('action:video-url-import.init', 'video-edit')
57 }
58
54 canDeactivate () { 59 canDeactivate () {
55 return { canDeactivate: true } 60 return { canDeactivate: true }
56 } 61 }
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
index ca21b61cd..effb37077 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
@@ -1,15 +1,15 @@
1import { Subscription } from 'rxjs' 1import { Subscription } from 'rxjs'
2import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' 2import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http'
3import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' 3import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
4import { Router } from '@angular/router' 4import { Router } from '@angular/router'
5import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core' 5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core'
6import { scrollToTop, uploadErrorHandler } from '@app/helpers' 6import { scrollToTop, uploadErrorHandler } from '@app/helpers'
7import { FormValidatorService } from '@app/shared/shared-forms' 7import { FormValidatorService } from '@app/shared/shared-forms'
8import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 8import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
9import { LoadingBarService } from '@ngx-loading-bar/core' 9import { LoadingBarService } from '@ngx-loading-bar/core'
10import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
10import { VideoPrivacy } from '@shared/models' 11import { VideoPrivacy } from '@shared/models'
11import { VideoSend } from './video-send' 12import { VideoSend } from './video-send'
12import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
13 13
14@Component({ 14@Component({
15 selector: 'my-video-upload', 15 selector: 'my-video-upload',
@@ -20,7 +20,7 @@ import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
20 './video-send.scss' 20 './video-send.scss'
21 ] 21 ]
22}) 22})
23export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { 23export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate {
24 @Output() firstStepDone = new EventEmitter<string>() 24 @Output() firstStepDone = new EventEmitter<string>()
25 @Output() firstStepError = new EventEmitter<void>() 25 @Output() firstStepError = new EventEmitter<void>()
26 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> 26 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
@@ -60,7 +60,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
60 protected videoService: VideoService, 60 protected videoService: VideoService,
61 protected videoCaptionService: VideoCaptionService, 61 protected videoCaptionService: VideoCaptionService,
62 private userService: UserService, 62 private userService: UserService,
63 private router: Router 63 private router: Router,
64 private hooks: HooksService
64 ) { 65 ) {
65 super() 66 super()
66 } 67 }
@@ -79,6 +80,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
79 }) 80 })
80 } 81 }
81 82
83 ngAfterViewInit () {
84 this.hooks.runAction('action:video-upload.init', 'video-edit')
85 }
86
82 ngOnDestroy () { 87 ngOnDestroy () {
83 if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe() 88 if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe()
84 } 89 }
diff --git a/client/src/app/+videos/+video-edit/video-add.component.scss b/client/src/app/+videos/+video-edit/video-add.component.scss
index 5db9e823d..1ebee946b 100644
--- a/client/src/app/+videos/+video-edit/video-add.component.scss
+++ b/client/src/app/+videos/+video-edit/video-add.component.scss
@@ -67,7 +67,7 @@ $nav-link-height: 40px;
67 &.active { 67 &.active {
68 border-color: $border-color; 68 border-color: $border-color;
69 border-bottom-color: transparent; 69 border-bottom-color: transparent;
70 background-color: pvar(--submenuColor) !important; 70 background-color: pvar(--submenuBackgroundColor) !important;
71 71
72 span { 72 span {
73 border-bottom-color: pvar(--mainColor); 73 border-bottom-color: pvar(--mainColor);
@@ -84,7 +84,7 @@ $nav-link-height: 40px;
84 border: $border-width $border-type $border-color; 84 border: $border-width $border-type $border-color;
85 border-top: transparent; 85 border-top: transparent;
86 86
87 background-color: pvar(--submenuColor); 87 background-color: pvar(--submenuBackgroundColor);
88 border-bottom-left-radius: 3px; 88 border-bottom-left-radius: 3px;
89 border-bottom-right-radius: 3px; 89 border-bottom-right-radius: 3px;
90 width: 100%; 90 width: 100%;
diff --git a/client/src/app/+videos/+video-watch/comment/video-comments.component.html b/client/src/app/+videos/+video-watch/comment/video-comments.component.html
index 4a6426d30..9e6fde2e0 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comments.component.html
+++ b/client/src/app/+videos/+video-watch/comment/video-comments.component.html
@@ -1,12 +1,7 @@
1<div> 1<div>
2 <div class="title-block"> 2 <div class="title-block">
3 <h2 class="title-page title-page-single"> 3 <h2 class="title-page title-page-single">
4 <ng-container *ngIf="totalNotDeletedComments > 0; then hasComments; else noComments"></ng-container> 4 {totalNotDeletedComments, plural, =0 {Comments} =1 {1 Comment} other {{{totalNotDeletedComments}} Comments}}
5 <ng-template #hasComments>
6 <ng-container i18n *ngIf="totalNotDeletedComments === 1; else manyComments">1 Comment</ng-container>
7 <ng-template i18n #manyComments>{{ totalNotDeletedComments }} Comments</ng-template>
8 </ng-template>
9 <ng-template i18n #noComments>Comments</ng-template>
10 </h2> 5 </h2>
11 6
12 <my-feed [syndicationItems]="syndicationItems"></my-feed> 7 <my-feed [syndicationItems]="syndicationItems"></my-feed>
@@ -79,15 +74,17 @@
79 <span class="glyphicon glyphicon-menu-down"></span> 74 <span class="glyphicon glyphicon-menu-down"></span>
80 75
81 <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container> 76 <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container>
77
82 <ng-template #hasAuthorComments> 78 <ng-template #hasAuthorComments>
83 <ng-container *ngIf="comment.totalReplies !== comment.totalRepliesFromVideoAuthor; else onlyAuthorComments" i18n> 79 <ng-container *ngIf="comment.totalReplies !== comment.totalRepliesFromVideoAuthor; else onlyAuthorComments" i18n>
84 View {{ comment.totalReplies }} replies from {{ video?.account?.displayName || 'the author' }} and others 80 View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}} from {{ video?.account?.displayName || 'the author' }} and others
85 </ng-container> 81 </ng-container>
86 <ng-template i18n #onlyAuthorComments> 82 <ng-template i18n #onlyAuthorComments>
87 View {{ comment.totalReplies }} replies from {{ video?.account?.displayName || 'the author' }} 83 View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}} from {{ video?.account?.displayName || 'the author' }}
88 </ng-template> 84 </ng-template>
89 </ng-template> 85 </ng-template>
90 <ng-template i18n #noAuthorComments>View {{ comment.totalReplies }} replies</ng-template> 86
87 <ng-template i18n #noAuthorComments>View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}}</ng-template>
91 88
92 <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader> 89 <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader>
93 </div> 90 </div>
diff --git a/client/src/app/+videos/+video-watch/comment/video-comments.component.ts b/client/src/app/+videos/+video-watch/comment/video-comments.component.ts
index d36dd9e34..210236b61 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comments.component.ts
+++ b/client/src/app/+videos/+video-watch/comment/video-comments.component.ts
@@ -5,7 +5,6 @@ import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifie
5import { HooksService } from '@app/core/plugins/hooks.service' 5import { HooksService } from '@app/core/plugins/hooks.service'
6import { Syndication, VideoDetails } from '@app/shared/shared-main' 6import { Syndication, VideoDetails } from '@app/shared/shared-main'
7import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment' 7import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment'
8import { ThisReceiver } from '@angular/compiler'
9 8
10@Component({ 9@Component({
11 selector: 'my-video-comments', 10 selector: 'my-video-comments',
diff --git a/client/src/app/+videos/+video-watch/modal/video-support.component.ts b/client/src/app/+videos/+video-watch/modal/video-support.component.ts
deleted file mode 100644
index bd5290a72..000000000
--- a/client/src/app/+videos/+video-watch/modal/video-support.component.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import { Component, Input, ViewChild } from '@angular/core'
2import { MarkdownService } from '@app/core'
3import { VideoDetails } from '@app/shared/shared-main'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5
6@Component({
7 selector: 'my-video-support',
8 templateUrl: './video-support.component.html',
9 styleUrls: [ './video-support.component.scss' ]
10})
11export class VideoSupportComponent {
12 @Input() video: VideoDetails = null
13
14 @ViewChild('modal', { static: true }) modal: NgbModal
15
16 videoHTMLSupport = ''
17
18 constructor (
19 private markdownService: MarkdownService,
20 private modalService: NgbModal
21 ) { }
22
23 show () {
24 const modalRef = this.modalService.open(this.modal, { centered: true })
25
26 this.markdownService.enhancedMarkdownToHTML(this.video.support)
27 .then(r => this.videoHTMLSupport = r)
28
29 return modalRef
30 }
31}
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html
index 3c7c679b8..e0e9f92e7 100644
--- a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html
+++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html
@@ -1,4 +1,4 @@
1<div class="other-videos"> 1<div class="other-videos" [ngClass]="{ 'display-as-row': displayAsRow }">
2 <ng-container *ngIf="hasVideos$ | async"> 2 <ng-container *ngIf="hasVideos$ | async">
3 <div class="title-page-container"> 3 <div class="title-page-container">
4 <h2 i18n class="title-page title-page-single"> 4 <h2 i18n class="title-page title-page-single">
@@ -14,7 +14,7 @@
14 14
15 <ng-container *ngFor="let video of (videos$ | async); let i = index; let length = count"> 15 <ng-container *ngFor="let video of (videos$ | async); let i = index; let length = count">
16 <my-video-miniature 16 <my-video-miniature
17 [displayOptions]="displayOptions" [video]="video" [user]="userMiniature" 17 [displayOptions]="displayOptions" [video]="video" [user]="userMiniature" [displayAsRow]="displayAsRow"
18 (videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()" (videoAccountMuted)="onVideoRemoved()"> 18 (videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()" (videoAccountMuted)="onVideoRemoved()">
19 </my-video-miniature> 19 </my-video-miniature>
20 20
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss
index b278c9654..c9fae6f27 100644
--- a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss
+++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss
@@ -1,3 +1,6 @@
1@import '_variables';
2@import '_mixins';
3
1.title-page-container { 4.title-page-container {
2 display: flex; 5 display: flex;
3 justify-content: space-between; 6 justify-content: space-between;
@@ -11,6 +14,10 @@
11 } 14 }
12} 15}
13 16
17.title-page {
18 margin-top: 0;
19}
20
14.title-page-autoplay { 21.title-page-autoplay {
15 display: flex; 22 display: flex;
16 width: max-content; 23 width: max-content;
@@ -29,3 +36,29 @@
29hr { 36hr {
30 margin-top: 0; 37 margin-top: 0;
31} 38}
39
40my-video-miniature {
41 display: block;
42}
43
44.other-videos:not(.display-as-row) my-video-miniature {
45 min-width: $video-thumbnail-medium-width;
46 max-width: $video-thumbnail-medium-width;
47}
48
49.display-as-row {
50 my-video-miniature {
51 margin-bottom: 20px;
52 }
53
54 hr {
55 display: none;
56 }
57
58 @media screen and (max-width: $mobile-view) {
59 my-video-miniature {
60 margin-bottom: 10px;
61 }
62 }
63}
64
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts
index a1c8e0661..89b9c01b6 100644
--- a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts
+++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts
@@ -16,6 +16,8 @@ import { RecommendedVideosStore } from './recommended-videos.store'
16export class RecommendedVideosComponent implements OnInit, OnChanges { 16export class RecommendedVideosComponent implements OnInit, OnChanges {
17 @Input() inputRecommendation: RecommendationInfo 17 @Input() inputRecommendation: RecommendationInfo
18 @Input() playlist: VideoPlaylist 18 @Input() playlist: VideoPlaylist
19 @Input() displayAsRow: boolean
20
19 @Output() gotRecommendations = new EventEmitter<Video[]>() 21 @Output() gotRecommendations = new EventEmitter<Video[]>()
20 22
21 autoPlayNextVideo: boolean 23 autoPlayNextVideo: boolean
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html
index 310cc926f..5058f05dd 100644
--- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.html
+++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html
@@ -1,8 +1,9 @@
1<div class="wrapper" [ngClass]="'avatar-' + size"> 1<div class="wrapper" [ngClass]="'avatar-' + size">
2 <ng-container *ngIf="!isChannelAvatarNull() && !genericChannel"> 2 <ng-container *ngIf="!isChannelAvatarNull() && !genericChannel">
3 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> 3 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
4 <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" /> 4 <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" class="channel-avatar" />
5 </a> 5 </a>
6
6 <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle"> 7 <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle">
7 <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" /> 8 <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" />
8 </a> 9 </a>
@@ -14,7 +15,7 @@
14 </a> 15 </a>
15 16
16 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> 17 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
17 <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" /> 18 <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" class="channel-avatar" />
18 </a> 19 </a>
19 </ng-container> 20 </ng-container>
20 21
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss b/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss
index 37709fce6..4998e85fa 100644
--- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss
+++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss
@@ -25,8 +25,12 @@
25 position: absolute; 25 position: absolute;
26 top:50%; 26 top:50%;
27 left:50%; 27 left:50%;
28 border-radius: 50%; 28 transform: translate(-50%,-50%);
29 transform: translate(-50%,-50%) 29 border-radius: 5px;
30
31 &:not(.channel-avatar) {
32 border-radius: 50%;
33 }
30 } 34 }
31 35
32 a:nth-of-type(2) img { 36 a:nth-of-type(2) img {
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts
index 440e2b522..0b6e796df 100644
--- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts
+++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { Video } from '../video/video.model' 2import { Video } from '@app/shared/shared-main/video'
3 3
4@Component({ 4@Component({
5 selector: 'my-video-avatar-channel', 5 selector: 'my-video-avatar-channel',
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 b17f898ce..99103c2c3 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch.component.html
@@ -142,7 +142,7 @@
142 <ng-container *ngIf="isUserLoggedIn()"> 142 <ng-container *ngIf="isUserLoggedIn()">
143 <my-video-actions-dropdown 143 <my-video-actions-dropdown
144 placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" [videoCaptions]="videoCaptions" 144 placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" [videoCaptions]="videoCaptions"
145 [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()" (modalOpened)="onModalOpened()" 145 [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()"
146 ></my-video-actions-dropdown> 146 ></my-video-actions-dropdown>
147 </ng-container> 147 </ng-container>
148 </div> 148 </div>
@@ -289,6 +289,7 @@
289 </div> 289 </div>
290 290
291 <my-recommended-videos 291 <my-recommended-videos
292 [displayAsRow]="displayOtherVideosAsRow()"
292 [inputRecommendation]="{ uuid: video.uuid, tags: video.tags }" 293 [inputRecommendation]="{ uuid: video.uuid, tags: video.tags }"
293 [playlist]="playlist" 294 [playlist]="playlist"
294 (gotRecommendations)="onRecommendations($event)" 295 (gotRecommendations)="onRecommendations($event)"
@@ -313,6 +314,6 @@
313</div> 314</div>
314 315
315<ng-container *ngIf="video !== null"> 316<ng-container *ngIf="video !== null">
316 <my-video-support #videoSupportModal [video]="video"></my-video-support> 317 <my-support-modal #supportModal [video]="video"></my-support-modal>
317 <my-video-share #videoShareModal [video]="video" [videoCaptions]="videoCaptions" [playlist]="playlist"></my-video-share> 318 <my-video-share #videoShareModal [video]="video" [videoCaptions]="videoCaptions" [playlist]="playlist"></my-video-share>
318</ng-container> 319</ng-container>
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.scss b/client/src/app/+videos/+video-watch/video-watch.component.scss
index 555126cbc..2e566e3fb 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/+videos/+video-watch/video-watch.component.scss
@@ -3,7 +3,7 @@
3@import '_bootstrap-variables'; 3@import '_bootstrap-variables';
4@import '_miniature'; 4@import '_miniature';
5 5
6$player-factor: 1.7; // 16/9 6$player-factor: #{16/9};
7$video-info-margin-left: 44px; 7$video-info-margin-left: 44px;
8 8
9@function getPlayerHeight($width){ 9@function getPlayerHeight($width){
@@ -179,12 +179,6 @@ $video-info-margin-left: 44px;
179 &:hover { 179 &:hover {
180 opacity: 0.8; 180 opacity: 0.8;
181 } 181 }
182
183 img {
184 @include avatar(18px);
185
186 margin: -2px 5px 0 0;
187 }
188 } 182 }
189 183
190 .video-info-channel-left { 184 .video-info-channel-left {
@@ -413,37 +407,12 @@ $video-info-margin-left: 44px;
413 } 407 }
414 } 408 }
415 } 409 }
410}
416 411
417 ::ng-deep .other-videos { 412my-recommended-videos {
418 padding-left: 15px; 413 display: block;
419 min-width: $video-miniature-width; 414 padding-left: 15px;
420 415 min-width: 250px;
421 @media screen and (min-width: 1800px - (3* $video-miniature-width)) {
422 width: min-content;
423 }
424
425 .title-page {
426 margin: 0 !important;
427 }
428
429 .video-miniature {
430 display: flex;
431 width: max-content;
432 height: 100%;
433 padding-bottom: 20px;
434 flex-wrap: wrap;
435 }
436
437 .video-bottom {
438 @media screen and (max-width: 1800px - (3* $video-miniature-width)) {
439 margin-left: 1rem;
440 }
441 @media screen and (max-width: 500px) {
442 margin-left: 0;
443 margin-top: .5rem;
444 }
445 }
446 }
447} 416}
448 417
449my-video-comments { 418my-video-comments {
@@ -537,6 +506,7 @@ my-video-comments {
537 } 506 }
538} 507}
539 508
509// Use the same breakpoint than in the typescript component to display the other video miniatures as row
540@media screen and (max-width: 1100px) { 510@media screen and (max-width: 1100px) {
541 #video-wrapper { 511 #video-wrapper {
542 flex-direction: column; 512 flex-direction: column;
@@ -549,15 +519,10 @@ my-video-comments {
549 519
550 .video-bottom { 520 .video-bottom {
551 flex-direction: column; 521 flex-direction: column;
522 }
552 523
553 ::ng-deep .other-videos { 524 my-recommended-videos {
554 padding-left: 0 !important; 525 padding-left: 0;
555
556 ::ng-deep .video-miniature {
557 flex-direction: row;
558 width: auto;
559 }
560 }
561 } 526 }
562} 527}
563 528
@@ -579,10 +544,6 @@ my-video-comments {
579 } 544 }
580 } 545 }
581 546
582 ::ng-deep .other-videos .video-miniature {
583 flex-direction: column;
584 }
585
586 .privacy-concerns { 547 .privacy-concerns {
587 width: 100%; 548 width: 100%;
588 } 549 }
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 7a98cab3b..de5fb4ed0 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -21,6 +21,7 @@ import { RedirectService } from '@app/core/routing/redirect.service'
21import { isXPercentInViewport, scrollToTop } from '@app/helpers' 21import { isXPercentInViewport, scrollToTop } from '@app/helpers'
22import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' 22import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
23import { VideoShareComponent } from '@app/shared/shared-share-modal' 23import { VideoShareComponent } from '@app/shared/shared-share-modal'
24import { SupportModalComponent } from '@app/shared/shared-support-modal'
24import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 25import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
25import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' 26import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature'
26import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 27import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
@@ -28,7 +29,12 @@ import { MetaService } from '@ngx-meta/core'
28import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' 29import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
29import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 30import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
30import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' 31import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
31import { getStoredP2PEnabled, getStoredTheater } from '../../../assets/player/peertube-player-local-storage' 32import {
33 cleanupVideoWatch,
34 getStoredP2PEnabled,
35 getStoredTheater,
36 getStoredVideoWatchHistory
37} from '../../../assets/player/peertube-player-local-storage'
32import { 38import {
33 CustomizationOptions, 39 CustomizationOptions,
34 P2PMediaLoaderOptions, 40 P2PMediaLoaderOptions,
@@ -39,7 +45,6 @@ import {
39} from '../../../assets/player/peertube-player-manager' 45} from '../../../assets/player/peertube-player-manager'
40import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' 46import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils'
41import { environment } from '../../../environments/environment' 47import { environment } from '../../../environments/environment'
42import { VideoSupportComponent } from './modal/video-support.component'
43import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' 48import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
44 49
45type URLOptions = CustomizationOptions & { playerMode: PlayerMode } 50type URLOptions = CustomizationOptions & { playerMode: PlayerMode }
@@ -54,7 +59,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
54 59
55 @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent 60 @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent
56 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent 61 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
57 @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent 62 @ViewChild('supportModal') supportModal: SupportModalComponent
58 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent 63 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
59 @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent 64 @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent
60 65
@@ -195,6 +200,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
195 this.theaterEnabled = getStoredTheater() 200 this.theaterEnabled = getStoredTheater()
196 201
197 this.hooks.runAction('action:video-watch.init', 'video-watch') 202 this.hooks.runAction('action:video-watch.init', 'video-watch')
203
204 setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI
198 } 205 }
199 206
200 ngOnDestroy () { 207 ngOnDestroy () {
@@ -277,23 +284,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
277 } 284 }
278 285
279 showSupportModal () { 286 showSupportModal () {
280 // Check video was playing before opening support modal 287 this.supportModal.show()
281 const isVideoPlaying = this.isPlaying()
282
283 this.pausePlayer()
284
285 const modalRef = this.videoSupportModal.show()
286
287 modalRef.result.then(() => {
288 if (isVideoPlaying) {
289 this.resumePlayer()
290 }
291 })
292 } 288 }
293 289
294 showShareModal () { 290 showShareModal () {
295 this.pausePlayer()
296
297 this.videoShareModal.show(this.currentTime, this.videoWatchPlaylist.currentPlaylistPosition) 291 this.videoShareModal.show(this.currentTime, this.videoWatchPlaylist.currentPlaylistPosition)
298 } 292 }
299 293
@@ -316,10 +310,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
316 } 310 }
317 } 311 }
318 312
319 onModalOpened () {
320 this.pausePlayer()
321 }
322
323 onVideoRemoved () { 313 onVideoRemoved () {
324 this.redirectService.redirectToHomepage() 314 this.redirectService.redirectToHomepage()
325 } 315 }
@@ -396,6 +386,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
396 this.loadVideo(videoId) 386 this.loadVideo(videoId)
397 } 387 }
398 388
389 displayOtherVideosAsRow () {
390 // Use the same value as in the SASS file
391 return this.screenService.getWindowInnerWidth() <= 1100
392 }
393
399 private loadVideo (videoId: string) { 394 private loadVideo (videoId: string) {
400 // Video did not change 395 // Video did not change
401 if (this.video && this.video.uuid === videoId) return 396 if (this.video && this.video.uuid === videoId) return
@@ -768,9 +763,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
768 const getStartTime = () => { 763 const getStartTime = () => {
769 const byUrl = urlOptions.startTime !== undefined 764 const byUrl = urlOptions.startTime !== undefined
770 const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined) 765 const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined)
766 const byLocalStorage = getStoredVideoWatchHistory(video.uuid)
771 767
772 if (byUrl) return timeToInt(urlOptions.startTime) 768 if (byUrl) return timeToInt(urlOptions.startTime)
773 if (byHistory) return video.userHistory.currentTime 769 if (byHistory) return video.userHistory.currentTime
770 if (byLocalStorage) return byLocalStorage.duration
774 771
775 return 0 772 return 0
776 } 773 }
@@ -815,6 +812,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
815 ? this.videoService.getVideoViewUrl(video.uuid) 812 ? this.videoService.getVideoViewUrl(video.uuid)
816 : null, 813 : null,
817 embedUrl: video.embedUrl, 814 embedUrl: video.embedUrl,
815 embedTitle: video.name,
818 816
819 isLive: video.isLive, 817 isLive: video.isLive,
820 818
@@ -827,7 +825,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
827 825
828 serverUrl: environment.apiUrl, 826 serverUrl: environment.apiUrl,
829 827
830 videoCaptions: playerCaptions 828 videoCaptions: playerCaptions,
829
830 videoUUID: video.uuid
831 }, 831 },
832 832
833 webtorrent: { 833 webtorrent: {
@@ -867,24 +867,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
867 return { playerMode: mode, playerOptions: options } 867 return { playerMode: mode, playerOptions: options }
868 } 868 }
869 869
870 private pausePlayer () {
871 if (!this.player) return
872
873 this.player.pause()
874 }
875
876 private resumePlayer () {
877 if (!this.player) return
878
879 this.player.play()
880 }
881
882 private isPlaying () {
883 if (!this.player) return
884
885 return !this.player.paused()
886 }
887
888 private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { 870 private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) {
889 if (!this.liveVideosSub) { 871 if (!this.liveVideosSub) {
890 this.liveVideosSub = this.buildLiveEventsSubscription() 872 this.liveVideosSub = this.buildLiveEventsSubscription()
diff --git a/client/src/app/+videos/+video-watch/video-watch.module.ts b/client/src/app/+videos/+video-watch/video-watch.module.ts
index fbda9b9c4..3e9f3822e 100644
--- a/client/src/app/+videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.module.ts
@@ -4,6 +4,7 @@ import { SharedGlobalIconModule } from '@app/shared/shared-icons'
4import { SharedMainModule } from '@app/shared/shared-main' 4import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedModerationModule } from '@app/shared/shared-moderation' 5import { SharedModerationModule } from '@app/shared/shared-moderation'
6import { SharedShareModal } from '@app/shared/shared-share-modal' 6import { SharedShareModal } from '@app/shared/shared-share-modal'
7import { SharedSupportModal } from '@app/shared/shared-support-modal'
7import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 8import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
8import { SharedVideoModule } from '@app/shared/shared-video' 9import { SharedVideoModule } from '@app/shared/shared-video'
9import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' 10import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
@@ -13,9 +14,9 @@ import { VideoCommentService } from '../../shared/shared-video-comment/video-com
13import { VideoCommentAddComponent } from './comment/video-comment-add.component' 14import { VideoCommentAddComponent } from './comment/video-comment-add.component'
14import { VideoCommentComponent } from './comment/video-comment.component' 15import { VideoCommentComponent } from './comment/video-comment.component'
15import { VideoCommentsComponent } from './comment/video-comments.component' 16import { VideoCommentsComponent } from './comment/video-comments.component'
16import { VideoSupportComponent } from './modal/video-support.component'
17import { RecommendationsModule } from './recommendations/recommendations.module' 17import { RecommendationsModule } from './recommendations/recommendations.module'
18import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive' 18import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive'
19import { VideoAvatarChannelComponent } from './video-avatar-channel.component'
19import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' 20import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
20import { VideoWatchRoutingModule } from './video-watch-routing.module' 21import { VideoWatchRoutingModule } from './video-watch-routing.module'
21import { VideoWatchComponent } from './video-watch.component' 22import { VideoWatchComponent } from './video-watch.component'
@@ -34,18 +35,20 @@ import { VideoWatchComponent } from './video-watch.component'
34 SharedGlobalIconModule, 35 SharedGlobalIconModule,
35 SharedVideoCommentModule, 36 SharedVideoCommentModule,
36 SharedShareModal, 37 SharedShareModal,
37 SharedVideoModule 38 SharedVideoModule,
39 SharedSupportModal
38 ], 40 ],
39 41
40 declarations: [ 42 declarations: [
41 VideoWatchComponent, 43 VideoWatchComponent,
42 VideoWatchPlaylistComponent, 44 VideoWatchPlaylistComponent,
43 45
44 VideoSupportComponent,
45 VideoCommentsComponent, 46 VideoCommentsComponent,
46 VideoCommentAddComponent, 47 VideoCommentAddComponent,
47 VideoCommentComponent, 48 VideoCommentComponent,
48 49
50 VideoAvatarChannelComponent,
51
49 TimestampRouteTransformerDirective, 52 TimestampRouteTransformerDirective,
50 TimestampRouteTransformerDirective 53 TimestampRouteTransformerDirective
51 ], 54 ],
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 ca986c634..639a96c43 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
@@ -14,7 +14,7 @@
14 </h1> 14 </h1>
15 15
16 <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> 16 <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)">
17 <my-video-miniature [video]="video" [fitWidth]="true" [user]="userMiniature" [displayVideoActions]="true"> 17 <my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true">
18 </my-video-miniature> 18 </my-video-miniature>
19 </div> 19 </div>
20 </div> 20 </div>
@@ -25,7 +25,7 @@
25 </h2> 25 </h2>
26 26
27 <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> 27 <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)">
28 <my-video-miniature [video]="video" [fitWidth]="true" [user]="userMiniature" [displayVideoActions]="true"> 28 <my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true">
29 </my-video-miniature> 29 </my-video-miniature>
30 </div> 30 </div>
31 </div> 31 </div>
@@ -40,7 +40,7 @@
40 </div> 40 </div>
41 41
42 <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> 42 <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)">
43 <my-video-miniature [video]="video" [fitWidth]="true" [user]="userMiniature" [displayVideoActions]="true"> 43 <my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true">
44 </my-video-miniature> 44 </my-video-miniature>
45 </div> 45 </div>
46 </div> 46 </div>
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.scss b/client/src/app/+videos/video-list/overview/video-overview.component.scss
index c1d10188a..ec73c628c 100644
--- a/client/src/app/+videos/video-list/overview/video-overview.component.scss
+++ b/client/src/app/+videos/video-list/overview/video-overview.component.scss
@@ -8,9 +8,84 @@
8} 8}
9 9
10.margin-content { 10.margin-content {
11 @include fluid-videos-miniature-layout; 11 @include grid-videos-miniature-layout;
12} 12}
13 13
14.section { 14.section {
15 @include miniature-rows; 15 &:first-child {
16 padding-top: 30px;
17
18 .section-title {
19 border-top: none !important;
20 }
21 }
22
23 .section-title {
24 font-size: 24px;
25 font-weight: $font-semibold;
26 padding-top: 15px;
27 margin-bottom: 15px;
28 display: flex;
29 justify-content: space-between;
30
31 &:not(h2) {
32 border-top: 1px solid $separator-border-color;
33 }
34
35 a {
36 &:hover, &:focus:not(.focus-visible), &:active {
37 text-decoration: none;
38 outline: none;
39 }
40
41 color: pvar(--mainForegroundColor);
42 }
43 }
44
45 &.channel {
46 .section-title {
47 a {
48 display: flex;
49 width: fit-content;
50 align-items: center;
51
52 img {
53 @include channel-avatar(28px);
54
55 margin-right: 8px;
56 }
57 }
58
59 .followers {
60 color: pvar(--greyForegroundColor);
61 font-weight: normal;
62 font-size: 14px;
63 margin-left: 10px;
64 position: relative;
65 top: 2px;
66 }
67 }
68 }
69
70 .show-more {
71 position: relative;
72 top: -5px;
73 display: inline-block;
74 font-size: 16px;
75 text-transform: uppercase;
76 color: pvar(--greyForegroundColor);
77 margin-bottom: 10px;
78 font-weight: $font-semibold;
79 text-decoration: none;
80 }
81
82 @media screen and (max-width: $mobile-view) {
83 max-height: initial;
84 overflow: initial;
85
86 .section-title {
87 font-size: 17px;
88 margin-left: 10px;
89 }
90 }
16} 91}
diff --git a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts
index e352a2b2c..6aabb93a5 100644
--- a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts
+++ b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts
@@ -7,7 +7,7 @@ import { HooksService } from '@app/core/plugins/hooks.service'
7import { immutableAssign } from '@app/helpers' 7import { immutableAssign } from '@app/helpers'
8import { VideoService } from '@app/shared/shared-main' 8import { VideoService } from '@app/shared/shared-main'
9import { UserSubscriptionService } from '@app/shared/shared-user-subscription' 9import { UserSubscriptionService } from '@app/shared/shared-user-subscription'
10import { AbstractVideoList, OwnerDisplayType } from '@app/shared/shared-video-miniature' 10import { AbstractVideoList } from '@app/shared/shared-video-miniature'
11import { FeedFormat, VideoSortField } from '@shared/models' 11import { FeedFormat, VideoSortField } from '@shared/models'
12import { environment } from '../../../environments/environment' 12import { environment } from '../../../environments/environment'
13import { copyToClipboard } from '../../../root-helpers/utils' 13import { copyToClipboard } from '../../../root-helpers/utils'
@@ -20,7 +20,6 @@ import { copyToClipboard } from '../../../root-helpers/utils'
20export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy { 20export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy {
21 titlePage: string 21 titlePage: string
22 sort = '-publishedAt' as VideoSortField 22 sort = '-publishedAt' as VideoSortField
23 ownerDisplayType: OwnerDisplayType = 'auto'
24 groupByDate = true 23 groupByDate = true
25 24
26 constructor ( 25 constructor (
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss
index e8447719a..42293e412 100644
--- a/client/src/app/app.component.scss
+++ b/client/src/app/app.component.scss
@@ -43,6 +43,10 @@ $assets-path: '../assets';
43 background-color: pvar(--mainForegroundColor); 43 background-color: pvar(--mainForegroundColor);
44 mask-image: url('#{$assets-path}/images/misc/menu.svg'); 44 mask-image: url('#{$assets-path}/images/misc/menu.svg');
45 margin: 0 18px 0 20px; 45 margin: 0 18px 0 20px;
46
47 @media screen and (max-width: $mobile-view) {
48 margin: 0 10px;
49 }
46 } 50 }
47 } 51 }
48 52
@@ -71,16 +75,11 @@ $assets-path: '../assets';
71 } 75 }
72 76
73 @media screen and (max-width: $mobile-view) { 77 @media screen and (max-width: $mobile-view) {
74 width: 70px;
75 78
76 .peertube-title { 79 .peertube-title {
77 display: none; 80 display: none;
78 } 81 }
79 } 82 }
80
81 @media screen and (max-width: 350px) {
82 flex: auto;
83 }
84 } 83 }
85 84
86 .header-right { 85 .header-right {
diff --git a/client/src/app/core/notification/peertube-socket.service.ts b/client/src/app/core/notification/peertube-socket.service.ts
index bc3f7b893..eab1c63f2 100644
--- a/client/src/app/core/notification/peertube-socket.service.ts
+++ b/client/src/app/core/notification/peertube-socket.service.ts
@@ -58,12 +58,11 @@ export class PeerTubeSocket {
58 this.notificationSocket = this.io(environment.apiUrl + '/user-notifications', { 58 this.notificationSocket = this.io(environment.apiUrl + '/user-notifications', {
59 query: { accessToken: this.auth.getAccessToken() } 59 query: { accessToken: this.auth.getAccessToken() }
60 }) 60 })
61
62 this.notificationSocket.on('new-notification', (n: UserNotificationServer) => {
63 this.ngZone.run(() => this.dispatchNotificationEvent('new', n))
64 })
65 }) 61 })
66 62
63 this.notificationSocket.on('new-notification', (n: UserNotificationServer) => {
64 this.ngZone.run(() => this.dispatchNotificationEvent('new', n))
65 })
67 } 66 }
68 67
69 private async initLiveVideosSocket () { 68 private async initLiveVideosSocket () {
diff --git a/client/src/app/core/plugins/hooks.service.ts b/client/src/app/core/plugins/hooks.service.ts
index ec47aa48c..ddde198d2 100644
--- a/client/src/app/core/plugins/hooks.service.ts
+++ b/client/src/app/core/plugins/hooks.service.ts
@@ -3,13 +3,29 @@ import { mergeMap, switchMap } from 'rxjs/operators'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { PluginService } from '@app/core/plugins/plugin.service' 4import { PluginService } from '@app/core/plugins/plugin.service'
5import { ClientActionHookName, ClientFilterHookName, PluginClientScope } from '@shared/models' 5import { ClientActionHookName, ClientFilterHookName, PluginClientScope } from '@shared/models'
6import { AuthService, AuthStatus } from '../auth'
6 7
7type RawFunction<U, T> = (params: U) => T 8type RawFunction<U, T> = (params: U) => T
8type ObservableFunction<U, T> = RawFunction<U, Observable<T>> 9type ObservableFunction<U, T> = RawFunction<U, Observable<T>>
9 10
10@Injectable() 11@Injectable()
11export class HooksService { 12export class HooksService {
12 constructor (private pluginService: PluginService) { } 13 constructor (
14 private authService: AuthService,
15 private pluginService: PluginService
16 ) {
17 // Run auth hooks
18 this.authService.userInformationLoaded
19 .subscribe(() => this.runAction('action:auth-user.information-loaded', 'common', { user: this.authService.getUser() }))
20
21 this.authService.loginChangedSource.subscribe(obj => {
22 if (obj === AuthStatus.LoggedIn) {
23 this.runAction('action:auth-user.logged-in', 'common')
24 } else if (obj === AuthStatus.LoggedOut) {
25 this.runAction('action:auth-user.logged-out', 'common')
26 }
27 })
28 }
13 29
14 wrapObsFun 30 wrapObsFun
15 <P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName> 31 <P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName>
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index b755fda2c..54dba5e17 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -235,6 +235,12 @@ export class PluginService implements ClientHook {
235 .toPromise() 235 .toPromise()
236 }, 236 },
237 237
238 getServerConfig: () => {
239 return this.server.getConfig()
240 .pipe(catchError(res => this.restExtractor.handleError(res)))
241 .toPromise()
242 },
243
238 isLoggedIn: () => { 244 isLoggedIn: () => {
239 return this.authService.isLoggedIn() 245 return this.authService.isLoggedIn()
240 }, 246 },
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 11288fc54..906191ae1 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -98,6 +98,12 @@ export class ServerService {
98 extensions: [] 98 extensions: []
99 } 99 }
100 }, 100 },
101 banner: {
102 file: {
103 size: { max: 0 },
104 extensions: []
105 }
106 },
101 video: { 107 video: {
102 image: { 108 image: {
103 size: { max: 0 }, 109 size: { max: 0 },
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts
index 15a4f7f82..8aaaa238d 100644
--- a/client/src/app/core/users/user.model.ts
+++ b/client/src/app/core/users/user.model.ts
@@ -1,7 +1,7 @@
1import { Account } from '@app/shared/shared-main/account/account.model' 1import { Account } from '@app/shared/shared-main/account/account.model'
2import { hasUserRight } from '@shared/core-utils/users' 2import { hasUserRight } from '@shared/core-utils/users'
3import { 3import {
4 Avatar, 4 ActorImage,
5 NSFWPolicyType, 5 NSFWPolicyType,
6 User as UserServerModel, 6 User as UserServerModel,
7 UserAdminFlag, 7 UserAdminFlag,
@@ -131,7 +131,7 @@ export class User implements UserServerModel {
131 } 131 }
132 } 132 }
133 133
134 updateAccountAvatar (newAccountAvatar?: Avatar) { 134 updateAccountAvatar (newAccountAvatar?: ActorImage) {
135 if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar) 135 if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar)
136 else this.account.resetAvatar() 136 else this.account.resetAvatar()
137 } 137 }
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts
index 33cc1f668..3de83152c 100644
--- a/client/src/app/core/users/user.service.ts
+++ b/client/src/app/core/users/user.service.ts
@@ -7,8 +7,7 @@ import { AuthService } from '@app/core/auth'
7import { getBytes } from '@root-helpers/bytes' 7import { getBytes } from '@root-helpers/bytes'
8import { UserLocalStorageKeys } from '@root-helpers/users' 8import { UserLocalStorageKeys } from '@root-helpers/users'
9import { 9import {
10 Avatar, 10 ActorImage,
11 NSFWPolicyType,
12 ResultList, 11 ResultList,
13 User as UserServerModel, 12 User as UserServerModel,
14 UserCreate, 13 UserCreate,
@@ -136,7 +135,7 @@ export class UserService {
136 changeAvatar (avatarForm: FormData) { 135 changeAvatar (avatarForm: FormData) {
137 const url = UserService.BASE_USERS_URL + 'me/avatar/pick' 136 const url = UserService.BASE_USERS_URL + 'me/avatar/pick'
138 137
139 return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm) 138 return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm)
140 .pipe(catchError(err => this.restExtractor.handleError(err))) 139 .pipe(catchError(err => this.restExtractor.handleError(err)))
141 } 140 }
142 141
diff --git a/client/src/app/core/wrappers/screen.service.ts b/client/src/app/core/wrappers/screen.service.ts
index a085e5bdc..c133b5fe9 100644
--- a/client/src/app/core/wrappers/screen.service.ts
+++ b/client/src/app/core/wrappers/screen.service.ts
@@ -38,11 +38,10 @@ export class ScreenService {
38 38
39 let numberOfVideos = 1 39 let numberOfVideos = 1
40 40
41 if (screenWidth > 1850) numberOfVideos = 7 41 if (screenWidth > 1850) numberOfVideos = 5
42 else if (screenWidth > 1600) numberOfVideos = 6 42 else if (screenWidth > 1600) numberOfVideos = 4
43 else if (screenWidth > 1370) numberOfVideos = 5 43 else if (screenWidth > 1370) numberOfVideos = 3
44 else if (screenWidth > 1100) numberOfVideos = 4 44 else if (screenWidth > 1100) numberOfVideos = 2
45 else if (screenWidth > 850) numberOfVideos = 3
46 45
47 return numberOfVideos 46 return numberOfVideos
48 } 47 }
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html
index 03e86b8e6..f84086b4a 100644
--- a/client/src/app/header/search-typeahead.component.html
+++ b/client/src/app/header/search-typeahead.component.html
@@ -34,7 +34,8 @@
34 34
35 <!-- search instructions, when search input is empty --> 35 <!-- search instructions, when search input is empty -->
36 <div *ngIf="areInstructionsDisplayed()" id="typeahead-instructions" class="overflow-hidden"> 36 <div *ngIf="areInstructionsDisplayed()" id="typeahead-instructions" class="overflow-hidden">
37 <div class="d-flex justify-content-between"> 37 <span class="text-muted" i18n>Your query will be matched against video names or descriptions, channel names.</span>
38 <div class="d-flex justify-content-between mt-3">
38 <label class="small-title" i18n>ADVANCED SEARCH</label> 39 <label class="small-title" i18n>ADVANCED SEARCH</label>
39 <div class="advanced-search-status c-help"> 40 <div class="advanced-search-status c-help">
40 <span [ngClass]="canSearchAnyURI ? 'text-success' : 'text-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' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.">
@@ -55,7 +56,6 @@
55 <em>UUID</em> <span class="text-muted" i18n>will list the matching video</span> 56 <em>UUID</em> <span class="text-muted" i18n>will list the matching video</span>
56 </li> 57 </li>
57 </ul> 58 </ul>
58 <span class="text-muted" i18n>Any other input will return matching video or channel names.</span>
59 </div> 59 </div>
60 </div> 60 </div>
61 61
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss
index f8d68e986..a60aa38d6 100644
--- a/client/src/app/header/search-typeahead.component.scss
+++ b/client/src/app/header/search-typeahead.component.scss
@@ -86,7 +86,7 @@ li.suggestion {
86 flex: 1; 86 flex: 1;
87 87
88 input { 88 input {
89 width: unset; 89 width: 70px;
90 } 90 }
91 } 91 }
92 92
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index 2ea66e57d..aa247d268 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -3,6 +3,7 @@
3 3
4$menu-link-icon-size: 22px; 4$menu-link-icon-size: 22px;
5$menu-link-icon-margin-right: 18px; 5$menu-link-icon-margin-right: 18px;
6$footer-links-base-opacity: .8;
6 7
7@mixin menu-link { 8@mixin menu-link {
8 display: flex; 9 display: flex;
@@ -91,168 +92,168 @@ menu {
91 align-items: center; 92 align-items: center;
92 justify-content: left; 93 justify-content: left;
93 94
94 .logged-in-more { 95 my-notification {
95 $main-radius: 25px; 96 margin-left: auto;
97 margin-right: 15px;
98 }
99 }
100}
96 101
97 flex: 1; 102.logged-in-more {
98 margin-left: 13px; 103 $main-radius: 25px;
99 border-radius: $main-radius;
100 transition: all .1s ease-in-out;
101 cursor: pointer;
102 104
103 *, & { 105 flex: 1;
104 line-height: 1; 106 margin-left: 13px;
105 } 107 border-radius: $main-radius;
108 transition: all .1s ease-in-out;
109 cursor: pointer;
106 110
107 &.show { 111 *, & {
108 background-color: rgba(255, 255, 255, 0.20); 112 line-height: 1;
109 box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325); 113 }
110 }
111 114
112 @mixin display-hints($is-mobile: false) { 115 &.show {
113 background-color: rgba(255, 255, 255, 0.15); 116 background-color: rgba(255, 255, 255, 0.20);
114 117 box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325);
115 @if $is-mobile { 118 }
116 .dropdown-toggle-indicator { 119
117 display: inherit !important; 120 @mixin display-hints($is-mobile: false) {
118 } 121 background-color: rgba(255, 255, 255, 0.15);
119 .dropdown-toggle:first-child {
120 padding-right: 30px !important;
121 }
122 }
123 }
124 122
125 &:hover { 123 @if $is-mobile {
126 @include display-hints; 124 .dropdown-toggle-indicator {
125 display: inherit !important;
126 }
127 .dropdown-toggle:first-child {
128 padding-right: 30px !important;
127 } 129 }
130 }
131 }
128 132
129 /* smartphones and touchscreens */ 133 &:hover {
130 @media (hover: none) and (pointer: coarse) { 134 @include display-hints;
131 @include display-hints($is-mobile: true); 135 }
132 136
133 /* fill space when on mobile */ 137 /* smartphones and touchscreens */
134 max-width: calc(100% - 80px); 138 @media (hover: none) and (pointer: coarse) {
135 .dropdown-toggle { 139 @include display-hints($is-mobile: true);
136 max-width: 100%;
137 }
138 .logged-in-info {
139 max-width: calc(100% - 45px) !important;
140 }
141 140
142 } 141 /* fill space when on mobile */
142 max-width: calc(100% - 80px);
143 .dropdown-toggle {
144 max-width: 100%;
145 }
146 .logged-in-info {
147 max-width: calc(100% - 45px) !important;
148 }
143 149
144 .dropdown-toggle-indicator { 150 }
145 position: relative;
146 width: 0;
147 display: none;
148
149 span {
150 position: absolute;
151 right: -35px;
152 top: -8px;
153 color: grey;
154 width: $main-radius;
155 }
156 }
157 151
158 .dropdown-toggle { 152 .dropdown-toggle-indicator {
159 &::after { 153 position: relative;
160 border: none; 154 width: 0;
161 } 155 display: none;
162 }
163 156
164 .dropdown-toggle:first-child { 157 span {
165 display: flex; 158 position: absolute;
166 align-items: center; 159 right: -35px;
167 padding: 5px 7px; 160 top: -8px;
168 border-radius: $main-radius; 161 color: grey;
169 } 162 width: $main-radius;
163 }
164 }
170 165
171 img { 166 .dropdown-toggle {
172 @include avatar(34px); 167 &::after {
168 border: none;
169 }
170 }
173 171
174 margin-right: 10px; 172 .dropdown-toggle:first-child {
175 } 173 display: flex;
174 align-items: center;
175 padding: 5px 7px;
176 border-radius: $main-radius;
177 }
178
179 img {
180 @include avatar(34px);
176 181
177 .logged-in-info { 182 margin-right: 10px;
178 max-width: 105px; 183 }
184}
179 185
180 flex-grow: 1; 186.logged-in-info {
187 max-width: 105px;
181 188
182 .logged-in-display-name, 189 flex-grow: 1;
183 .logged-in-username {
184 @include ellipsis;
185 }
186 190
187 .logged-in-display-name { 191 .logged-in-display-name,
188 font-size: 16px; 192 .logged-in-username {
189 font-weight: $font-semibold; 193 @include ellipsis;
190 color: pvar(--menuForegroundColor); 194 }
191 195
192 @include disable-default-a-behaviour; 196 .logged-in-display-name {
193 } 197 font-size: 16px;
198 font-weight: $font-semibold;
199 color: pvar(--menuForegroundColor);
194 200
195 .logged-in-username { 201 @include disable-default-a-behaviour;
196 font-size: 13px; 202 }
197 color: #C6C6C6;
198 margin-top: 3px;
199 }
200 }
201 }
202 203
203 my-notification { 204 .logged-in-username {
204 margin-left: auto; 205 font-size: 13px;
205 margin-right: 15px; 206 color: #C6C6C6;
206 } 207 margin-top: 3px;
207 } 208 }
209}
208 210
209 .logged-in-menu { 211.logged-in-menu {
210 display: flex; 212 display: flex;
211 flex-direction: column; 213 flex-direction: column;
212 align-items: flex-start; 214 align-items: flex-start;
213 border-top: 1px solid var(--greyForegroundColor); 215 border-top: 1px solid var(--greyForegroundColor);
214 line-height: $line-height-normal; 216 line-height: $line-height-normal;
215 217
216 a { 218 a {
217 @include menu-link; 219 @include menu-link;
218 @include disable-default-a-behaviour; 220 @include disable-default-a-behaviour;
219 221
220 $icon-size: 13px; 222 $icon-size: 13px;
221 $additional-margin: ($menu-link-icon-size - $icon-size) / 2; 223 $additional-margin: ($menu-link-icon-size - $icon-size) / 2;
222 224
223 font-size: 14px; 225 font-size: 14px;
224 width: 100%; 226 width: 100%;
225 min-height: 35px; 227 min-height: 35px;
226 228
227 my-global-icon { 229 my-global-icon {
228 width: $icon-size; 230 width: $icon-size;
229 height: $icon-size; 231 height: $icon-size;
230 232
231 // Keep aligned with other icons 233 // Keep aligned with other icons
232 margin-left: $additional-margin; 234 margin-left: $additional-margin;
233 235
234 &[iconName="channel"] { 236 &[iconName="channel"] {
235 margin-top: -2px; 237 margin-top: -2px;
236 }
237 } 238 }
239 }
238 240
239 &.active, 241 &.active,
240 &:hover, 242 &:hover,
241 &:focus-visible { 243 &:focus-visible {
242 my-global-icon { 244 my-global-icon {
243 @include apply-svg-color(var(--menuForegroundColor)); 245 @include apply-svg-color(var(--menuForegroundColor));
244 }
245 } 246 }
247 }
246 248
247 &.active { 249 &.active {
248 $border-left-width: 4px; 250 $border-left-width: 4px;
249 251
250 font-weight: $font-semibold; 252 font-weight: $font-semibold;
251 border-left: $border-left-width solid var(--mainColor); 253 border-left: $border-left-width solid var(--mainColor);
252 254
253 my-global-icon { 255 my-global-icon {
254 margin-left: $additional-margin - $border-left-width; 256 margin-left: $additional-margin - $border-left-width;
255 }
256 } 257 }
257 } 258 }
258 } 259 }
@@ -333,50 +334,48 @@ menu {
333 flex-direction: column; 334 flex-direction: column;
334 padding: 0 $menu-lateral-padding; 335 padding: 0 $menu-lateral-padding;
335 } 336 }
337}
336 338
337 $footer-links-base-opacity: .8; 339.footer-links {
338 340 &, > div {
339 .footer-links { 341 display: flex;
340 &, > div { 342 flex-wrap: wrap;
341 display: flex; 343 }
342 flex-wrap: wrap;
343 }
344 344
345 a, span[role=button] { 345 a, span[role=button] {
346 display: inline-block; 346 display: inline-block;
347 text-decoration: none; 347 text-decoration: none;
348 color: pvar(--menuForegroundColor); 348 color: pvar(--menuForegroundColor);
349 opacity: $footer-links-base-opacity; 349 opacity: $footer-links-base-opacity;
350 white-space: nowrap;
351 font-size: 90%;
352 font-weight: 500;
353 line-height: 1.4rem;
354 margin-right: 8px;
355
356 &.inline-global-icon {
357 display: inline-flex;
358 align-items: center;
350 white-space: nowrap; 359 white-space: nowrap;
351 font-size: 90%; 360 height: 1.4rem;
352 font-weight: 500; 361
353 line-height: 1.4rem; 362 my-global-icon {
354 margin-right: 8px; 363 @include apply-svg-color(pvar(--menuForegroundColor));
355 364
356 &.inline-global-icon { 365 display: flex;
357 display: inline-flex; 366 width: auto;
358 align-items: center; 367 height: 90%;
359 white-space: nowrap; 368 margin-right: .2rem;
360 height: 1.4rem;
361
362 my-global-icon {
363 @include apply-svg-color(pvar(--menuForegroundColor));
364
365 display: flex;
366 width: auto;
367 height: 90%;
368 margin-right: .2rem;
369 }
370 } 369 }
371 } 370 }
372 } 371 }
372}
373 373
374 .footer-copyleft small a { 374.footer-copyleft small a {
375 @include disable-default-a-behaviour; 375 @include disable-default-a-behaviour;
376 376
377 color: pvar(--menuForegroundColor); 377 color: pvar(--menuForegroundColor);
378 opacity: $footer-links-base-opacity - .2; 378 opacity: $footer-links-base-opacity - .2;
379 }
380} 379}
381 380
382.dropdown { 381.dropdown {
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts
index ed20d9c01..9b6b7cda5 100644
--- a/client/src/app/menu/menu.component.ts
+++ b/client/src/app/menu/menu.component.ts
@@ -10,6 +10,7 @@ import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
10import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' 10import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
11import { ServerConfig, UserRight, VideoConstant } from '@shared/models' 11import { ServerConfig, UserRight, VideoConstant } from '@shared/models'
12import { NgbDropdown, NgbDropdownConfig } from '@ng-bootstrap/ng-bootstrap' 12import { NgbDropdown, NgbDropdownConfig } from '@ng-bootstrap/ng-bootstrap'
13import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service'
13 14
14const logger = debug('peertube:menu:MenuComponent') 15const logger = debug('peertube:menu:MenuComponent')
15 16
@@ -54,6 +55,7 @@ export class MenuComponent implements OnInit {
54 private hotkeysService: HotkeysService, 55 private hotkeysService: HotkeysService,
55 private screenService: ScreenService, 56 private screenService: ScreenService,
56 private menuService: MenuService, 57 private menuService: MenuService,
58 private modalService: PeertubeModalService,
57 private dropdownConfig: NgbDropdownConfig, 59 private dropdownConfig: NgbDropdownConfig,
58 private router: Router 60 private router: Router
59 ) { 61 ) {
@@ -130,6 +132,9 @@ export class MenuComponent implements OnInit {
130 this.authService.userInformationLoaded 132 this.authService.userInformationLoaded
131 .subscribe(() => this.buildUserLanguages()) 133 .subscribe(() => this.buildUserLanguages())
132 }) 134 })
135
136 this.modalService.openQuickSettingsSubject
137 .subscribe(() => this.openQuickSettings())
133 } 138 }
134 139
135 isRegistrationAllowed () { 140 isRegistrationAllowed () {
diff --git a/client/src/app/menu/notification.component.scss b/client/src/app/menu/notification.component.scss
index 40feb9e66..c65787779 100644
--- a/client/src/app/menu/notification.component.scss
+++ b/client/src/app/menu/notification.component.scss
@@ -1,6 +1,9 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.content {
5 scrollbar-color: auto;
6}
4 7
5.notification-inbox-popover { 8.notification-inbox-popover {
6 padding: 10px; 9 padding: 10px;
diff --git a/client/src/app/modal/instance-config-warning-modal.component.html b/client/src/app/modal/instance-config-warning-modal.component.html
index 5a8adf726..498adfeff 100644
--- a/client/src/app/modal/instance-config-warning-modal.component.html
+++ b/client/src/app/modal/instance-config-warning-modal.component.html
@@ -15,7 +15,7 @@
15 15
16 <li i18n *ngIf="!about.instance.administrator">Who you are</li> 16 <li i18n *ngIf="!about.instance.administrator">Who you are</li>
17 <li i18n *ngIf="!about.instance.maintenanceLifetime">How long you plan to maintain your instance</li> 17 <li i18n *ngIf="!about.instance.maintenanceLifetime">How long you plan to maintain your instance</li>
18 <li i18n *ngIf="!about.instance.businessModel">How you plan to pay your instance</li> 18 <li i18n *ngIf="!about.instance.businessModel">How you plan to pay for keeping your instance running</li>
19 19
20 <li i18n *ngIf="!about.instance.moderationInformation">How you will moderate your instance</li> 20 <li i18n *ngIf="!about.instance.moderationInformation">How you will moderate your instance</li>
21 <li i18n *ngIf="!about.instance.terms">Instance terms</li> 21 <li i18n *ngIf="!about.instance.terms">Instance terms</li>
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
index e34836a18..eeb9f128b 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
@@ -117,7 +117,8 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
117 warningTitle: false, 117 warningTitle: false,
118 startTime: abuse.video.startAt, 118 startTime: abuse.video.startAt,
119 stopTime: abuse.video.endAt 119 stopTime: abuse.video.endAt
120 }) 120 }),
121 abuse.video.name
121 ) 122 )
122 } 123 }
123 124
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html
new file mode 100644
index 000000000..0829263f4
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html
@@ -0,0 +1,41 @@
1<div class="actor" *ngIf="actor">
2 <div class="d-flex">
3 <img [ngClass]="{ channel: isChannel() }" [src]="preview || actor.avatarUrl" alt="Avatar" />
4
5 <div class="actor-img-edit-container">
6
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>
9 <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label>
10 <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
11 </div>
12
13 <div
14 *ngIf="editable && hasAvatar()" class="actor-img-edit-button"
15 #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right"
16 >
17 <my-global-icon iconName="edit"></my-global-icon>
18 <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label>
19 </div>
20
21 </div>
22 </div>
23
24 <div class="actor-info">
25 <div class="actor-info-display-name">{{ actor.displayName }}</div>
26 <div *ngIf="displayUsername" class="actor-info-username">{{ actor.name }}</div>
27 <div *ngIf="displaySubscribers" i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
28 </div>
29</div>
30
31<ng-template #avatarEditContent>
32 <div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body">
33 <my-global-icon iconName="upload"></my-global-icon>
34 <span for="avatarfile" i18n>Upload a new avatar</span>
35 <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
36 </div>
37 <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()">
38 <my-global-icon iconName="delete"></my-global-icon>
39 <span i18n>Remove avatar</span>
40 </div>
41</ng-template>
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss
new file mode 100644
index 000000000..8b0172315
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss
@@ -0,0 +1,54 @@
1@import '_variables';
2@import '_mixins';
3
4.actor {
5 display: flex;
6
7 img {
8 margin-right: 15px;
9
10 &:not(.channel) {
11 @include avatar(100px);
12 }
13
14 &.channel {
15 @include channel-avatar(100px);
16 }
17 }
18
19 .actor-info {
20 display: inline-flex;
21 flex-direction: column;
22
23 .actor-info-display-name {
24 font-size: 20px;
25 font-weight: $font-bold;
26
27 @media screen and (max-width: $small-view) {
28 font-size: 16px;
29 }
30 }
31
32 .actor-info-username {
33 position: relative;
34 font-size: 14px;
35 color: pvar(--greyForegroundColor);
36 }
37
38 .actor-info-followers {
39 font-size: 15px;
40 padding-bottom: .5rem;
41 }
42 }
43}
44
45.actor-img-edit-container {
46 position: relative;
47 width: 0;
48}
49
50.actor-img-edit-button {
51 top: 55px;
52 right: 45px;
53 border-radius: 50%;
54}
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts
index b459c591f..d0d269489 100644
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts
+++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts
@@ -1,21 +1,27 @@
1import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
2import { Notifier, ServerService } from '@app/core' 3import { Notifier, ServerService } from '@app/core'
4import { Account, VideoChannel } from '@app/shared/shared-main'
3import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' 5import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
4import { getBytes } from '@root-helpers/bytes' 6import { getBytes } from '@root-helpers/bytes'
5import { Account } from '../account/account.model'
6import { VideoChannel } from '../video-channel/video-channel.model'
7import { Actor } from './actor.model'
8 7
9@Component({ 8@Component({
10 selector: 'my-actor-avatar-info', 9 selector: 'my-actor-avatar-edit',
11 templateUrl: './actor-avatar-info.component.html', 10 templateUrl: './actor-avatar-edit.component.html',
12 styleUrls: [ './actor-avatar-info.component.scss' ] 11 styleUrls: [
12 './actor-image-edit.scss',
13 './actor-avatar-edit.component.scss'
14 ]
13}) 15})
14export class ActorAvatarInfoComponent implements OnInit, OnChanges { 16export class ActorAvatarEditComponent implements OnInit {
15 @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> 17 @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement>
16 @ViewChild('avatarPopover') avatarPopover: NgbPopover 18 @ViewChild('avatarPopover') avatarPopover: NgbPopover
17 19
18 @Input() actor: VideoChannel | Account 20 @Input() actor: VideoChannel | Account
21 @Input() editable = true
22 @Input() displaySubscribers = true
23 @Input() displayUsername = true
24 @Input() previewImage = false
19 25
20 @Output() avatarChange = new EventEmitter<FormData>() 26 @Output() avatarChange = new EventEmitter<FormData>()
21 @Output() avatarDelete = new EventEmitter<void>() 27 @Output() avatarDelete = new EventEmitter<void>()
@@ -24,9 +30,10 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
24 maxAvatarSize = 0 30 maxAvatarSize = 0
25 avatarExtensions = '' 31 avatarExtensions = ''
26 32
27 private avatarUrl: string 33 preview: SafeResourceUrl
28 34
29 constructor ( 35 constructor (
36 private sanitizer: DomSanitizer,
30 private serverService: ServerService, 37 private serverService: ServerService,
31 private notifier: Notifier 38 private notifier: Notifier
32 ) { } 39 ) { }
@@ -42,12 +49,6 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
42 }) 49 })
43 } 50 }
44 51
45 ngOnChanges (changes: SimpleChanges) {
46 if (changes['actor']) {
47 this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.actor)
48 }
49 }
50
51 onAvatarChange (input: HTMLInputElement) { 52 onAvatarChange (input: HTMLInputElement) {
52 this.avatarfileInput = new ElementRef(input) 53 this.avatarfileInput = new ElementRef(input)
53 54
@@ -61,13 +62,22 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
61 formData.append('avatarfile', avatarfile) 62 formData.append('avatarfile', avatarfile)
62 this.avatarPopover?.close() 63 this.avatarPopover?.close()
63 this.avatarChange.emit(formData) 64 this.avatarChange.emit(formData)
65
66 if (this.previewImage) {
67 this.preview = this.sanitizer.bypassSecurityTrustResourceUrl(URL.createObjectURL(avatarfile))
68 }
64 } 69 }
65 70
66 deleteAvatar () { 71 deleteAvatar () {
72 this.preview = undefined
67 this.avatarDelete.emit() 73 this.avatarDelete.emit()
68 } 74 }
69 75
70 hasAvatar () { 76 hasAvatar () {
71 return !!this.avatarUrl 77 return !!this.preview || !!this.actor.avatar
78 }
79
80 isChannel () {
81 return !!(this.actor as VideoChannel).ownerAccount
72 } 82 }
73} 83}
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html
new file mode 100644
index 000000000..266fc26c5
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html
@@ -0,0 +1,34 @@
1<div class="actor" *ngIf="actor">
2 <div class="actor-img-edit-container">
3 <div class="banner-placeholder">
4 <img *ngIf="hasBanner()" [src]="preview || actor.bannerUrl" alt="Banner" />
5 </div>
6
7 <div *ngIf="!hasBanner()" class="actor-img-edit-button" [ngbTooltip]="bannerFormat" placement="right" container="body">
8 <my-global-icon iconName="upload"></my-global-icon>
9 <label for="bannerfile" i18n>Upload a new banner</label>
10 <input #bannerfileInput type="file" name="bannerfile" id="bannerfile" [accept]="bannerExtensions" (change)="onBannerChange(bannerfileInput)"/>
11 </div>
12
13 <div
14 *ngIf="hasBanner()" class="actor-img-edit-button"
15 #bannerPopover="ngbPopover" [ngbPopover]="bannerEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right"
16 >
17 <my-global-icon iconName="edit"></my-global-icon>
18 <label for="bannerMenu" i18n>Change your banner</label>
19 </div>
20 </div>
21</div>
22
23<ng-template #bannerEditContent>
24 <div class="dropdown-item c-hand" [ngbTooltip]="bannerFormat" placement="right" container="body">
25 <my-global-icon iconName="upload"></my-global-icon>
26 <span for="bannerfile" i18n>Upload a new banner</span>
27 <input #bannerfileInput type="file" name="bannerfile" id="bannerfile" [accept]="bannerExtensions" (change)="onBannerChange(bannerfileInput)"/>
28 </div>
29
30 <div class="dropdown-item c-hand" (click)="deleteBanner()" (key.enter)="deleteBanner()">
31 <my-global-icon iconName="delete"></my-global-icon>
32 <span i18n>Remove banner</span>
33 </div>
34</ng-template>
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss
new file mode 100644
index 000000000..23606f871
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss
@@ -0,0 +1,27 @@
1@import '_variables';
2@import '_mixins';
3
4.banner-placeholder {
5 @include block-ratio('> div, > img', $banner-inverted-ratio);
6}
7
8.banner-placeholder {
9 background-color: pvar(--greyBackgroundColor);
10}
11
12.actor-img-edit-container {
13 position: relative;
14 display: flex;
15 justify-content: center;
16 align-items: center;
17}
18
19.actor-img-edit-button {
20 position: absolute;
21 width: auto;
22
23 label {
24 font-weight: $font-semibold;
25 margin-bottom: 0;
26 }
27}
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts
new file mode 100644
index 000000000..8c12d3c4c
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts
@@ -0,0 +1,76 @@
1import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
2import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
3import { Notifier, ServerService } from '@app/core'
4import { VideoChannel } from '@app/shared/shared-main'
5import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
6import { getBytes } from '@root-helpers/bytes'
7
8@Component({
9 selector: 'my-actor-banner-edit',
10 templateUrl: './actor-banner-edit.component.html',
11 styleUrls: [
12 './actor-image-edit.scss',
13 './actor-banner-edit.component.scss'
14 ]
15})
16export class ActorBannerEditComponent implements OnInit {
17 @ViewChild('bannerfileInput') bannerfileInput: ElementRef<HTMLInputElement>
18 @ViewChild('bannerPopover') bannerPopover: NgbPopover
19
20 @Input() actor: VideoChannel
21 @Input() previewImage = false
22
23 @Output() bannerChange = new EventEmitter<FormData>()
24 @Output() bannerDelete = new EventEmitter<void>()
25
26 bannerFormat = ''
27 maxBannerSize = 0
28 bannerExtensions = ''
29
30 preview: SafeResourceUrl
31
32 constructor (
33 private sanitizer: DomSanitizer,
34 private serverService: ServerService,
35 private notifier: Notifier
36 ) { }
37
38 ngOnInit (): void {
39 this.serverService.getConfig()
40 .subscribe(config => {
41 this.maxBannerSize = config.banner.file.size.max
42 this.bannerExtensions = config.banner.file.extensions.join(', ')
43
44 // tslint:disable:max-line-length
45 this.bannerFormat = $localize`ratio 6/1, recommended size: 1600x266, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}`
46 })
47 }
48
49 onBannerChange (input: HTMLInputElement) {
50 this.bannerfileInput = new ElementRef(input)
51
52 const bannerfile = this.bannerfileInput.nativeElement.files[ 0 ]
53 if (bannerfile.size > this.maxBannerSize) {
54 this.notifier.error('Error', $localize`This image is too large.`)
55 return
56 }
57
58 const formData = new FormData()
59 formData.append('bannerfile', bannerfile)
60 this.bannerPopover?.close()
61 this.bannerChange.emit(formData)
62
63 if (this.previewImage) {
64 this.preview = this.sanitizer.bypassSecurityTrustResourceUrl(URL.createObjectURL(bannerfile))
65 }
66 }
67
68 deleteBanner () {
69 this.preview = undefined
70 this.bannerDelete.emit()
71 }
72
73 hasBanner () {
74 return !!this.preview || !!this.actor.bannerUrl
75 }
76}
diff --git a/client/src/app/shared/shared-actor-image/actor-image-edit.scss b/client/src/app/shared/shared-actor-image/actor-image-edit.scss
new file mode 100644
index 000000000..918955a89
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-image-edit.scss
@@ -0,0 +1,35 @@
1@import '_variables';
2@import '_mixins';
3
4.actor ::ng-deep .popover-image-info .popover-body {
5 padding: 0;
6
7 .dropdown-item {
8 padding: 6px 10px;
9 border-radius: 4px;
10
11 &:first-child {
12 @include peertube-file;
13 display: block;
14 }
15 }
16}
17
18.actor-img-edit-button {
19 @include peertube-button-file(21px);
20 @include button-with-icon(19px);
21 @include orange-button;
22
23 margin-top: 10px;
24 margin-bottom: 5px;
25 cursor: pointer;
26
27 input {
28 width: 30px;
29 height: 30px;
30 }
31
32 my-global-icon {
33 right: 7px;
34 }
35}
diff --git a/client/src/app/shared/shared-actor-image/index.ts b/client/src/app/shared/shared-actor-image/index.ts
new file mode 100644
index 000000000..18a9038eb
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/index.ts
@@ -0,0 +1 @@
export * from './shared-actor-image.module'
diff --git a/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts b/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts
new file mode 100644
index 000000000..6044f9925
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts
@@ -0,0 +1,29 @@
1
2import { CommonModule } from '@angular/common'
3import { NgModule } from '@angular/core'
4import { SharedGlobalIconModule } from '../shared-icons'
5import { SharedMainModule } from '../shared-main'
6import { ActorAvatarEditComponent } from './actor-avatar-edit.component'
7import { ActorBannerEditComponent } from './actor-banner-edit.component'
8
9@NgModule({
10 imports: [
11 CommonModule,
12
13 SharedMainModule,
14 SharedGlobalIconModule
15 ],
16
17 declarations: [
18 ActorAvatarEditComponent,
19 ActorBannerEditComponent
20 ],
21
22 exports: [
23 ActorAvatarEditComponent,
24 ActorBannerEditComponent
25 ],
26
27 providers: [ ]
28})
29export class SharedActorImageModule { }
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 e7441e4c1..9f252f299 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
@@ -12,9 +12,10 @@
12 12
13 <button 13 <button
14 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button" 14 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button"
15 class="btn btn-outline-secondary" i18n-title title="Copy" 15 class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy"
16 > 16 >
17 <span class="glyphicon glyphicon-copy"></span> 17 <span class="glyphicon glyphicon-duplicate"></span>
18 Copy
18 </button> 19 </button>
19 </div> 20 </div>
20</div> 21</div>
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.scss b/client/src/app/shared/shared-forms/markdown-textarea.component.scss
index fcddfea03..8203c7d1c 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.scss
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.scss
@@ -131,7 +131,7 @@ $input-border-radius: 3px;
131 border-right: none; 131 border-right: none;
132 132
133 :last-child { 133 :last-child {
134 margin-right: $not-expanded-horizontal-margins; 134 margin-right: pvar(--horizontalMarginContent);
135 } 135 }
136 } 136 }
137 137
diff --git a/client/src/app/shared/shared-forms/select/select-options.component.ts b/client/src/app/shared/shared-forms/select/select-options.component.ts
index 2890670e5..8482b9dea 100644
--- a/client/src/app/shared/shared-forms/select/select-options.component.ts
+++ b/client/src/app/shared/shared-forms/select/select-options.component.ts
@@ -1,4 +1,4 @@
1import { Component, forwardRef, Input } from '@angular/core' 1import { Component, forwardRef, HostListener, Input } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { SelectOptionsItem } from '../../../../types/select-options-item.model' 3import { SelectOptionsItem } from '../../../../types/select-options-item.model'
4 4
@@ -26,6 +26,13 @@ export class SelectOptionsComponent implements ControlValueAccessor {
26 26
27 propagateChange = (_: any) => { /* empty */ } 27 propagateChange = (_: any) => { /* empty */ }
28 28
29 // Allow plugins to update our value
30 @HostListener('change', [ '$event.target' ])
31 handleChange (event: any) {
32 this.writeValue(event.value)
33 this.onModelChange()
34 }
35
29 writeValue (id: number | string) { 36 writeValue (id: number | string) {
30 this.selectedId = id 37 this.selectedId = id
31 } 38 }
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 275600d60..2f6b420e3 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
@@ -31,7 +31,7 @@ ngb-accordion ::ng-deep {
31 padding: 0; 31 padding: 0;
32 32
33 & + .collapse.show { 33 & + .collapse.show {
34 background-color: var(--submenuColor); 34 background-color: var(--submenuBackgroundColor);
35 } 35 }
36 } 36 }
37 } 37 }
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 ce2557147..d505b6739 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
@@ -11,7 +11,7 @@
11 <tr> 11 <tr>
12 <th i18n class="label" scope="row"> 12 <th i18n class="label" scope="row">
13 <div>Default NSFW/sensitive videos policy</div> 13 <div>Default NSFW/sensitive videos policy</div>
14 <div class="more-info">can be redefined by the users</div> 14 <div class="c-hand more-info" (click)="openQuickSettingsHighlight()">can be redefined by the users</div>
15 </th> 15 </th>
16 16
17 <td class="value">{{ buildNSFWLabel() }}</td> 17 <td class="value">{{ buildNSFWLabel() }}</td>
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 0166157f9..c3b3dfdfd 100644
--- a/client/src/app/shared/shared-instance/instance-features-table.component.ts
+++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts
@@ -1,6 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
3import { ServerConfig } from '@shared/models' 3import { ServerConfig } from '@shared/models'
4import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service'
4 5
5@Component({ 6@Component({
6 selector: 'my-instance-features-table', 7 selector: 'my-instance-features-table',
@@ -11,7 +12,10 @@ export class InstanceFeaturesTableComponent implements OnInit {
11 quotaHelpIndication = '' 12 quotaHelpIndication = ''
12 serverConfig: ServerConfig 13 serverConfig: ServerConfig
13 14
14 constructor (private serverService: ServerService) { } 15 constructor (
16 private serverService: ServerService,
17 private modalService: PeertubeModalService
18 ) { }
15 19
16 get initialUserVideoQuota () { 20 get initialUserVideoQuota () {
17 return this.serverConfig.user.videoQuota 21 return this.serverConfig.user.videoQuota
@@ -56,6 +60,10 @@ export class InstanceFeaturesTableComponent implements OnInit {
56 return this.serverService.getServerVersionAndCommit() 60 return this.serverService.getServerVersionAndCommit()
57 } 61 }
58 62
63 openQuickSettingsHighlight () {
64 this.modalService.openQuickSettingsSubject.next()
65 }
66
59 private getApproximateTime (seconds: number) { 67 private getApproximateTime (seconds: number) {
60 const hours = Math.floor(seconds / 3600) 68 const hours = Math.floor(seconds / 3600)
61 let pluralSuffix = '' 69 let pluralSuffix = ''
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts
index b71a893d1..17fddff09 100644
--- a/client/src/app/shared/shared-main/account/account.model.ts
+++ b/client/src/app/shared/shared-main/account/account.model.ts
@@ -1,4 +1,4 @@
1import { Account as ServerAccount, Avatar } from '@shared/models' 1import { Account as ServerAccount, ActorImage } from '@shared/models'
2import { Actor } from './actor.model' 2import { Actor } from './actor.model'
3 3
4export class Account extends Actor implements ServerAccount { 4export class Account extends Actor implements ServerAccount {
@@ -38,7 +38,7 @@ export class Account extends Actor implements ServerAccount {
38 this.mutedServerByInstance = false 38 this.mutedServerByInstance = false
39 } 39 }
40 40
41 updateAvatar (newAvatar: Avatar) { 41 updateAvatar (newAvatar: ActorImage) {
42 this.avatar = newAvatar 42 this.avatar = newAvatar
43 43
44 this.updateComputedAttributes() 44 this.updateComputedAttributes()
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html b/client/src/app/shared/shared-main/account/actor-avatar-info.component.html
deleted file mode 100644
index 30584fd00..000000000
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html
+++ /dev/null
@@ -1,43 +0,0 @@
1<ng-container *ngIf="actor">
2 <div class="actor">
3 <div class="d-flex">
4 <img [src]="actor.avatarUrl" alt="Avatar" />
5
6 <div class="actor-img-edit-container">
7
8 <div *ngIf="!hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body">
9 <my-global-icon iconName="upload"></my-global-icon>
10 <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label>
11 <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
12 </div>
13
14 <div *ngIf="hasAvatar()" class="actor-img-edit-button" #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-avatar-info" autoClose="outside" placement="right">
15 <my-global-icon iconName="edit"></my-global-icon>
16 <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label>
17 </div>
18
19 </div>
20 </div>
21
22
23 <div class="actor-info">
24 <div class="actor-info-names">
25 <div class="actor-info-display-name">{{ actor.displayName }}</div>
26 <div class="actor-info-username">{{ actor.name }}</div>
27 </div>
28 <div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
29 </div>
30 </div>
31</ng-container>
32
33<ng-template #avatarEditContent>
34 <div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body">
35 <my-global-icon iconName="upload"></my-global-icon>
36 <span for="avatarfile" i18n>Upload a new avatar</span>
37 <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
38 </div>
39 <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()">
40 <my-global-icon iconName="delete"></my-global-icon>
41 <span i18n>Remove avatar</span>
42 </div>
43</ng-template>
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss b/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss
deleted file mode 100644
index 57c298508..000000000
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss
+++ /dev/null
@@ -1,86 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4.actor {
5 display: flex;
6
7 img {
8 @include avatar(100px);
9
10 margin-right: 15px;
11 }
12
13 .actor-img-edit-container {
14 position: relative;
15 width: 0;
16
17 .actor-img-edit-button {
18 @include peertube-button-file(21px);
19 @include button-with-icon(19px);
20 @include orange-button;
21
22 margin-top: 10px;
23 margin-bottom: 5px;
24 border-radius: 50%;
25 top: 55px;
26 right: 45px;
27 cursor: pointer;
28
29 input {
30 width: 30px;
31 height: 30px;
32 }
33
34 my-global-icon {
35 right: 7px;
36 }
37 }
38 }
39
40 .actor-info {
41 justify-content: center;
42 display: inline-flex;
43 flex-direction: column;
44
45 .actor-info-names {
46 display: flex;
47 align-items: center;
48
49 .actor-info-display-name {
50 font-size: 20px;
51 font-weight: $font-bold;
52
53 @media screen and (max-width: $small-view) {
54 font-size: 16px;
55 }
56 }
57
58 .actor-info-username {
59 margin-left: 7px;
60 position: relative;
61 top: 2px;
62 font-size: 14px;
63 color: $grey-actor-name;
64 }
65 }
66
67 .actor-info-followers {
68 font-size: 15px;
69 padding-bottom: .5rem;
70 }
71 }
72}
73
74.actor-img-edit-container ::ng-deep .popover-avatar-info .popover-body {
75 padding: 0;
76
77 .dropdown-item {
78 padding: 6px 10px;
79 border-radius: 4px;
80
81 &:first-child {
82 @include peertube-file;
83 display: block;
84 }
85 }
86}
diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts
index 8222c9769..1ee0c297e 100644
--- a/client/src/app/shared/shared-main/account/actor.model.ts
+++ b/client/src/app/shared/shared-main/account/actor.model.ts
@@ -1,17 +1,20 @@
1import { Actor as ActorServer, Avatar } from '@shared/models' 1import { Actor as ActorServer, ActorImage } from '@shared/models'
2import { getAbsoluteAPIUrl } from '@app/helpers' 2import { getAbsoluteAPIUrl } from '@app/helpers'
3 3
4export abstract class Actor implements ActorServer { 4export abstract class Actor implements ActorServer {
5 id: number 5 id: number
6 url: string
7 name: string 6 name: string
7
8 host: string 8 host: string
9 url: string
10
9 followingCount: number 11 followingCount: number
10 followersCount: number 12 followersCount: number
13
11 createdAt: Date | string 14 createdAt: Date | string
12 updatedAt: Date | string 15 updatedAt: Date | string
13 avatar: Avatar
14 16
17 avatar: ActorImage
15 avatarUrl: string 18 avatarUrl: string
16 19
17 isLocal: boolean 20 isLocal: boolean
@@ -24,6 +27,8 @@ export abstract class Actor implements ActorServer {
24 27
25 return absoluteAPIUrl + actor.avatar.path 28 return absoluteAPIUrl + actor.avatar.path
26 } 29 }
30
31 return ''
27 } 32 }
28 33
29 static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) { 34 static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) {
@@ -42,11 +47,11 @@ export abstract class Actor implements ActorServer {
42 return host.trim() === thisHost 47 return host.trim() === thisHost
43 } 48 }
44 49
45 protected constructor (hash: ActorServer) { 50 protected constructor (hash: Partial<ActorServer>) {
46 this.id = hash.id 51 this.id = hash.id
47 this.url = hash.url 52 this.url = hash.url ?? ''
48 this.name = hash.name 53 this.name = hash.name ?? ''
49 this.host = hash.host 54 this.host = hash.host ?? ''
50 this.followingCount = hash.followingCount 55 this.followingCount = hash.followingCount
51 this.followersCount = hash.followersCount 56 this.followersCount = hash.followersCount
52 57
diff --git a/client/src/app/shared/shared-main/account/index.ts b/client/src/app/shared/shared-main/account/index.ts
index 61c800e56..b80ddb9f5 100644
--- a/client/src/app/shared/shared-main/account/index.ts
+++ b/client/src/app/shared/shared-main/account/index.ts
@@ -1,5 +1,3 @@
1export * from './account.model' 1export * from './account.model'
2export * from './account.service' 2export * from './account.service'
3export * from './actor-avatar-info.component'
4export * from './actor.model' 3export * from './actor.model'
5export * from './video-avatar-channel.component'
diff --git a/client/src/app/shared/shared-main/angular/autofocus.directive.ts b/client/src/app/shared/shared-main/angular/autofocus.directive.ts
new file mode 100644
index 000000000..5f087d79d
--- /dev/null
+++ b/client/src/app/shared/shared-main/angular/autofocus.directive.ts
@@ -0,0 +1,12 @@
1import { AfterViewInit, Directive, ElementRef } from '@angular/core'
2
3@Directive({
4 selector: '[autofocus]'
5})
6export class AutofocusDirective implements AfterViewInit {
7 constructor (private host: ElementRef) { }
8
9 ngAfterViewInit () {
10 this.host.nativeElement.focus()
11 }
12}
diff --git a/client/src/app/shared/shared-main/angular/index.ts b/client/src/app/shared/shared-main/angular/index.ts
index 29f8b3650..8ea47bb33 100644
--- a/client/src/app/shared/shared-main/angular/index.ts
+++ b/client/src/app/shared/shared-main/angular/index.ts
@@ -1,3 +1,4 @@
1export * from './autofocus.directive'
1export * from './bytes.pipe' 2export * from './bytes.pipe'
2export * from './duration-formatter.pipe' 3export * from './duration-formatter.pipe'
3export * from './from-now.pipe' 4export * from './from-now.pipe'
diff --git a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
index 3ddaffbdf..4fe3b964d 100644
--- a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
+++ b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
@@ -27,7 +27,9 @@ export class AuthInterceptor implements HttpInterceptor {
27 catchError((err: HttpErrorResponse) => { 27 catchError((err: HttpErrorResponse) => {
28 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') { 28 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') {
29 return this.handleTokenExpired(req, next) 29 return this.handleTokenExpired(req, next)
30 } else if (err.status === HttpStatusCode.UNAUTHORIZED_401) { 30 }
31
32 if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
31 return this.handleNotAuthenticated(err) 33 return this.handleNotAuthenticated(err)
32 } 34 }
33 35
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 fb0d97122..c20c02e23 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,14 +1,15 @@
1<span> 1<div class="root">
2 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="showInput()"></my-global-icon>
3
4 <input 2 <input
5 #ref 3 #ref
6 type="text" 4 type="text"
7 [(ngModel)]="value" 5 [(ngModel)]="value"
8 (focusout)="focusLost()"
9 (keyup.enter)="searchChange()" 6 (keyup.enter)="searchChange()"
10 [hidden]="!shown" 7 [hidden]="!inputShown"
11 [name]="name" 8 [name]="name"
12 [placeholder]="placeholder" 9 [placeholder]="placeholder"
13 > 10 >
14</span> 11
12 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon>
13
14 <my-global-icon *ngIf="!alwaysShow && inputShown" i18n-title title="Close search" iconName="cross" (click)="hideInput()"></my-global-icon>
15</div>
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
index 591b04fb2..5ae48f81b 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
@@ -1,29 +1,29 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4span { 4.root {
5 opacity: .6; 5 display: flex;
6
7 &:focus-within {
8 opacity: 1;
9 }
10} 6}
11 7
12my-global-icon { 8my-global-icon {
13 height: 18px; 9 height: 28px;
14 position: relative; 10 width: 28px;
15 top: -2px; 11 margin-left: 10px;
16} 12 cursor: pointer;
17 13
18input { 14 &:hover {
19 @include peertube-input-text(150px); 15 color: pvar(--mainHoverColor);
16 }
20 17
21 height: 22px; // maximum height for the account/video-channels links 18 &[iconName=search] {
22 padding-left: 10px; 19 color: pvar(--mainForegroundColor);
23 background-color: transparent; 20 }
24 border: none;
25 21
26 &::placeholder { 22 &[iconName=cross] {
27 font-size: 15px; 23 color: pvar(--mainForegroundColor);
28 } 24 }
29} 25}
26
27input {
28 @include peertube-input-text(200px);
29}
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
index 86ae9ab42..224d71134 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
@@ -1,7 +1,7 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { Subject } from 'rxjs' 1import { Subject } from 'rxjs'
4import { debounceTime, distinctUntilChanged } from 'rxjs/operators' 2import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
3import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router'
5 5
6@Component({ 6@Component({
7 selector: 'simple-search-input', 7 selector: 'simple-search-input',
@@ -13,11 +13,14 @@ export class SimpleSearchInputComponent implements OnInit {
13 13
14 @Input() name = 'search' 14 @Input() name = 'search'
15 @Input() placeholder = $localize`Search` 15 @Input() placeholder = $localize`Search`
16 @Input() iconTitle = $localize`Search`
17 @Input() alwaysShow = true
16 18
17 @Output() searchChanged = new EventEmitter<string>() 19 @Output() searchChanged = new EventEmitter<string>()
20 @Output() inputDisplayChanged = new EventEmitter<boolean>()
18 21
19 value = '' 22 value = ''
20 shown: boolean 23 inputShown: boolean
21 24
22 private searchSubject = new Subject<string>() 25 private searchSubject = new Subject<string>()
23 26
@@ -35,20 +38,51 @@ export class SimpleSearchInputComponent implements OnInit {
35 .subscribe(value => this.searchChanged.emit(value)) 38 .subscribe(value => this.searchChanged.emit(value))
36 39
37 this.searchSubject.next(this.value) 40 this.searchSubject.next(this.value)
41
42 if (this.isInputShown()) this.showInput(false)
38 } 43 }
39 44
40 showInput () { 45 isInputShown () {
41 this.shown = true 46 if (this.alwaysShow) return true
42 setTimeout(() => this.input.nativeElement.focus()) 47
48 return this.inputShown
49 }
50
51 onIconClick () {
52 if (!this.isInputShown()) {
53 this.showInput()
54 return
55 }
56
57 this.searchChange()
58 }
59
60 showInput (focus = true) {
61 this.inputShown = true
62 this.inputDisplayChanged.emit(this.inputShown)
63
64 if (focus) {
65 setTimeout(() => this.input.nativeElement.focus())
66 }
67 }
68
69 hideInput () {
70 this.inputShown = false
71
72 if (this.isInputShown() === false) {
73 this.inputDisplayChanged.emit(this.inputShown)
74 }
43 } 75 }
44 76
45 focusLost () { 77 focusLost () {
46 if (this.value !== '') return 78 if (this.value) return
47 this.shown = false 79
80 this.hideInput()
48 } 81 }
49 82
50 searchChange () { 83 searchChange () {
51 this.router.navigate(['./search'], { relativeTo: this.route }) 84 this.router.navigate([ './search' ], { relativeTo: this.route })
85
52 this.searchSubject.next(this.value) 86 this.searchSubject.next(this.value)
53 } 87 }
54} 88}
diff --git a/client/src/app/shared/shared-main/peertube-modal/index.ts b/client/src/app/shared/shared-main/peertube-modal/index.ts
new file mode 100644
index 000000000..d631522e4
--- /dev/null
+++ b/client/src/app/shared/shared-main/peertube-modal/index.ts
@@ -0,0 +1 @@
export * from './peertube-modal.service'
diff --git a/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts b/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts
new file mode 100644
index 000000000..79da08a5c
--- /dev/null
+++ b/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts
@@ -0,0 +1,7 @@
1import { Injectable } from '@angular/core'
2import { Subject } from 'rxjs'
3
4@Injectable({ providedIn: 'root' })
5export class PeertubeModalService {
6 openQuickSettingsSubject = new Subject<void>()
7}
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index 9d550996d..16d230f46 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -6,19 +6,20 @@ import { NgModule } from '@angular/core'
6import { FormsModule, ReactiveFormsModule } from '@angular/forms' 6import { FormsModule, ReactiveFormsModule } from '@angular/forms'
7import { RouterModule } from '@angular/router' 7import { RouterModule } from '@angular/router'
8import { 8import {
9 NgbButtonsModule,
9 NgbCollapseModule, 10 NgbCollapseModule,
10 NgbDropdownModule, 11 NgbDropdownModule,
11 NgbModalModule, 12 NgbModalModule,
12 NgbNavModule, 13 NgbNavModule,
13 NgbPopoverModule, 14 NgbPopoverModule,
14 NgbTooltipModule, 15 NgbTooltipModule
15 NgbButtonsModule
16} from '@ng-bootstrap/ng-bootstrap' 16} from '@ng-bootstrap/ng-bootstrap'
17import { LoadingBarModule } from '@ngx-loading-bar/core' 17import { LoadingBarModule } from '@ngx-loading-bar/core'
18import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' 18import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
19import { SharedGlobalIconModule } from '../shared-icons' 19import { SharedGlobalIconModule } from '../shared-icons'
20import { AccountService, ActorAvatarInfoComponent, VideoAvatarChannelComponent } from './account' 20import { AccountService } from './account'
21import { 21import {
22 AutofocusDirective,
22 BytesPipe, 23 BytesPipe,
23 DurationFormatterPipe, 24 DurationFormatterPipe,
24 FromNowPipe, 25 FromNowPipe,
@@ -31,7 +32,7 @@ import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditBu
31import { DateToggleComponent } from './date' 32import { DateToggleComponent } from './date'
32import { FeedComponent } from './feeds' 33import { FeedComponent } from './feeds'
33import { LoaderComponent, SmallLoaderComponent } from './loaders' 34import { LoaderComponent, SmallLoaderComponent } from './loaders'
34import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent, SimpleSearchInputComponent } from './misc' 35import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc'
35import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' 36import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
36import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' 37import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
37import { VideoCaptionService } from './video-caption' 38import { VideoCaptionService } from './video-caption'
@@ -64,13 +65,11 @@ import { VideoChannelService } from './video-channel'
64 ], 65 ],
65 66
66 declarations: [ 67 declarations: [
67 VideoAvatarChannelComponent,
68 ActorAvatarInfoComponent,
69
70 FromNowPipe, 68 FromNowPipe,
71 NumberFormatterPipe, 69 NumberFormatterPipe,
72 BytesPipe, 70 BytesPipe,
73 DurationFormatterPipe, 71 DurationFormatterPipe,
72 AutofocusDirective,
74 73
75 InfiniteScrollerDirective, 74 InfiniteScrollerDirective,
76 PeerTubeTemplateDirective, 75 PeerTubeTemplateDirective,
@@ -118,13 +117,11 @@ import { VideoChannelService } from './video-channel'
118 117
119 PrimeSharedModule, 118 PrimeSharedModule,
120 119
121 VideoAvatarChannelComponent,
122 ActorAvatarInfoComponent,
123
124 FromNowPipe, 120 FromNowPipe,
125 BytesPipe, 121 BytesPipe,
126 NumberFormatterPipe, 122 NumberFormatterPipe,
127 DurationFormatterPipe, 123 DurationFormatterPipe,
124 AutofocusDirective,
128 125
129 InfiniteScrollerDirective, 126 InfiniteScrollerDirective,
130 PeerTubeTemplateDirective, 127 PeerTubeTemplateDirective,
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index 1211995fd..88a4811da 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -6,6 +6,7 @@ import {
6 AbuseState, 6 AbuseState,
7 ActorInfo, 7 ActorInfo,
8 FollowState, 8 FollowState,
9 PluginType,
9 UserNotification as UserNotificationServer, 10 UserNotification as UserNotificationServer,
10 UserNotificationType, 11 UserNotificationType,
11 UserRight, 12 UserRight,
@@ -74,20 +75,40 @@ export class UserNotification implements UserNotificationServer {
74 } 75 }
75 } 76 }
76 77
78 plugin?: {
79 name: string
80 type: PluginType
81 latestVersion: string
82 }
83
84 peertube?: {
85 latestVersion: string
86 }
87
77 createdAt: string 88 createdAt: string
78 updatedAt: string 89 updatedAt: string
79 90
80 // Additional fields 91 // Additional fields
81 videoUrl?: string 92 videoUrl?: string
82 commentUrl?: any[] 93 commentUrl?: any[]
94
83 abuseUrl?: string 95 abuseUrl?: string
84 abuseQueryParams?: { [id: string]: string } = {} 96 abuseQueryParams?: { [id: string]: string } = {}
97
85 videoAutoBlacklistUrl?: string 98 videoAutoBlacklistUrl?: string
99
86 accountUrl?: string 100 accountUrl?: string
101
87 videoImportIdentifier?: string 102 videoImportIdentifier?: string
88 videoImportUrl?: string 103 videoImportUrl?: string
104
89 instanceFollowUrl?: string 105 instanceFollowUrl?: string
90 106
107 peertubeVersionLink?: string
108
109 pluginUrl?: string
110 pluginQueryParams?: { [id: string]: string } = {}
111
91 constructor (hash: UserNotificationServer, user: AuthUser) { 112 constructor (hash: UserNotificationServer, user: AuthUser) {
92 this.id = hash.id 113 this.id = hash.id
93 this.type = hash.type 114 this.type = hash.type
@@ -114,6 +135,9 @@ export class UserNotification implements UserNotificationServer {
114 this.actorFollow = hash.actorFollow 135 this.actorFollow = hash.actorFollow
115 if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower) 136 if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower)
116 137
138 this.plugin = hash.plugin
139 this.peertube = hash.peertube
140
117 this.createdAt = hash.createdAt 141 this.createdAt = hash.createdAt
118 this.updatedAt = hash.updatedAt 142 this.updatedAt = hash.updatedAt
119 143
@@ -197,6 +221,15 @@ export class UserNotification implements UserNotificationServer {
197 case UserNotificationType.AUTO_INSTANCE_FOLLOWING: 221 case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
198 this.instanceFollowUrl = '/admin/follows/following-list' 222 this.instanceFollowUrl = '/admin/follows/following-list'
199 break 223 break
224
225 case UserNotificationType.NEW_PEERTUBE_VERSION:
226 this.peertubeVersionLink = 'https://joinpeertube.org/news'
227 break
228
229 case UserNotificationType.NEW_PLUGIN_VERSION:
230 this.pluginUrl = `/admin/plugins/list-installed`
231 this.pluginQueryParams.pluginType = this.plugin.type + ''
232 break
200 } 233 }
201 } catch (err) { 234 } catch (err) {
202 this.type = null 235 this.type = null
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html
index 265af8d55..325f0eaae 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.html
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.html
@@ -4,7 +4,7 @@
4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)"> 4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)">
5 5
6 <ng-container [ngSwitch]="notification.type"> 6 <ng-container [ngSwitch]="notification.type">
7 <ng-container *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION"> 7 <ng-container *ngSwitchCase="1"> <!-- UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION -->
8 <ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container> 8 <ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container>
9 9
10 <ng-template #hasVideo> 10 <ng-template #hasVideo>
@@ -26,7 +26,7 @@
26 </ng-template> 26 </ng-template>
27 </ng-container> 27 </ng-container>
28 28
29 <ng-container *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO"> 29 <ng-container *ngSwitchCase="5"> <!-- UserNotificationType.UNBLACKLIST_ON_MY_VIDEO -->
30 <my-global-icon iconName="undo" aria-hidden="true"></my-global-icon> 30 <my-global-icon iconName="undo" aria-hidden="true"></my-global-icon>
31 31
32 <div class="message" i18n> 32 <div class="message" i18n>
@@ -34,7 +34,7 @@
34 </div> 34 </div>
35 </ng-container> 35 </ng-container>
36 36
37 <ng-container *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO"> 37 <ng-container *ngSwitchCase="4"> <!-- UserNotificationType.BLACKLIST_ON_MY_VIDEO -->
38 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon> 38 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon>
39 39
40 <div class="message" i18n> 40 <div class="message" i18n>
@@ -42,7 +42,7 @@
42 </div> 42 </div>
43 </ng-container> 43 </ng-container>
44 44
45 <ng-container *ngSwitchCase="UserNotificationType.NEW_ABUSE_FOR_MODERATORS"> 45 <ng-container *ngSwitchCase="3"> <!-- UserNotificationType.NEW_ABUSE_FOR_MODERATORS -->
46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
47 47
48 <div class="message" *ngIf="notification.videoUrl" i18n> 48 <div class="message" *ngIf="notification.videoUrl" i18n>
@@ -63,7 +63,7 @@
63 </div> 63 </div>
64 </ng-container> 64 </ng-container>
65 65
66 <ng-container *ngSwitchCase="UserNotificationType.ABUSE_STATE_CHANGE"> 66 <ng-container *ngSwitchCase="15"> <!-- UserNotificationType.ABUSE_STATE_CHANGE -->
67 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 67 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
68 68
69 <div class="message" i18n> 69 <div class="message" i18n>
@@ -73,7 +73,7 @@
73 </div> 73 </div>
74 </ng-container> 74 </ng-container>
75 75
76 <ng-container *ngSwitchCase="UserNotificationType.ABUSE_NEW_MESSAGE"> 76 <ng-container *ngSwitchCase="16"> <!-- UserNotificationType.ABUSE_NEW_MESSAGE -->
77 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 77 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
78 78
79 <div class="message" i18n> 79 <div class="message" i18n>
@@ -81,7 +81,7 @@
81 </div> 81 </div>
82 </ng-container> 82 </ng-container>
83 83
84 <ng-container *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS"> 84 <ng-container *ngSwitchCase="12"> <!-- UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS -->
85 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon> 85 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon>
86 86
87 <div class="message" i18n> 87 <div class="message" i18n>
@@ -89,7 +89,7 @@
89 </div> 89 </div>
90 </ng-container> 90 </ng-container>
91 91
92 <ng-container *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO"> 92 <ng-container *ngSwitchCase="2">
93 <ng-container *ngIf="notification.comment"> 93 <ng-container *ngIf="notification.comment">
94 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 94 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
95 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> 95 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
@@ -109,7 +109,7 @@
109 </ng-container> 109 </ng-container>
110 </ng-container> 110 </ng-container>
111 111
112 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED"> 112 <ng-container *ngSwitchCase="6"> <!-- UserNotificationType.MY_VIDEO_PUBLISHED -->
113 <my-global-icon iconName="film" aria-hidden="true"></my-global-icon> 113 <my-global-icon iconName="film" aria-hidden="true"></my-global-icon>
114 114
115 <div class="message" i18n> 115 <div class="message" i18n>
@@ -117,7 +117,7 @@
117 </div> 117 </div>
118 </ng-container> 118 </ng-container>
119 119
120 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS"> 120 <ng-container *ngSwitchCase="7"> <!-- UserNotificationType.MY_VIDEO_IMPORT_SUCCESS -->
121 <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon> 121 <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon>
122 122
123 <div class="message" i18n> 123 <div class="message" i18n>
@@ -125,7 +125,7 @@
125 </div> 125 </div>
126 </ng-container> 126 </ng-container>
127 127
128 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR"> 128 <ng-container *ngSwitchCase="8"> <!-- UserNotificationType.MY_VIDEO_IMPORT_ERROR -->
129 <my-global-icon iconName="cloud-error" aria-hidden="true"></my-global-icon> 129 <my-global-icon iconName="cloud-error" aria-hidden="true"></my-global-icon>
130 130
131 <div class="message" i18n> 131 <div class="message" i18n>
@@ -133,7 +133,7 @@
133 </div> 133 </div>
134 </ng-container> 134 </ng-container>
135 135
136 <ng-container *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION"> 136 <ng-container *ngSwitchCase="9"> <!-- UserNotificationType.NEW_USER_REGISTRATION -->
137 <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon> 137 <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon>
138 138
139 <div class="message" i18n> 139 <div class="message" i18n>
@@ -141,7 +141,7 @@
141 </div> 141 </div>
142 </ng-container> 142 </ng-container>
143 143
144 <ng-container *ngSwitchCase="UserNotificationType.NEW_FOLLOW"> 144 <ng-container *ngSwitchCase="10"> <!-- UserNotificationType.NEW_FOLLOW -->
145 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 145 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
146 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" /> 146 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" />
147 </a> 147 </a>
@@ -154,7 +154,7 @@
154 </div> 154 </div>
155 </ng-container> 155 </ng-container>
156 156
157 <ng-container *ngSwitchCase="UserNotificationType.COMMENT_MENTION"> 157 <ng-container *ngSwitchCase="11">
158 <ng-container *ngIf="notification.comment"> 158 <ng-container *ngIf="notification.comment">
159 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 159 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
160 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> 160 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
@@ -174,7 +174,7 @@
174 </ng-container> 174 </ng-container>
175 </ng-container> 175 </ng-container>
176 176
177 <ng-container *ngSwitchCase="UserNotificationType.NEW_INSTANCE_FOLLOWER"> 177 <ng-container *ngSwitchCase="13"> <!-- UserNotificationType.NEW_INSTANCE_FOLLOWER -->
178 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon> 178 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon>
179 179
180 <div class="message" i18n> 180 <div class="message" i18n>
@@ -183,7 +183,7 @@
183 </div> 183 </div>
184 </ng-container> 184 </ng-container>
185 185
186 <ng-container *ngSwitchCase="UserNotificationType.AUTO_INSTANCE_FOLLOWING"> 186 <ng-container *ngSwitchCase="14"> <!-- UserNotificationType.AUTO_INSTANCE_FOLLOWING -->
187 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon> 187 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon>
188 188
189 <div class="message" i18n> 189 <div class="message" i18n>
@@ -191,6 +191,22 @@
191 </div> 191 </div>
192 </ng-container> 192 </ng-container>
193 193
194 <ng-container *ngSwitchCase="17"> <!-- UserNotificationType.NEW_PLUGIN_VERSION -->
195 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
196
197 <div class="message" i18n>
198 <a (click)="markAsRead(notification)" [routerLink]="notification.pluginUrl" [queryParams]="notification.pluginQueryParams">A new version of the plugin/theme {{ notification.plugin.name }}</a> is available: {{ notification.plugin.latestVersion }}
199 </div>
200 </ng-container>
201
202 <ng-container *ngSwitchCase="18"> <!-- UserNotificationType.NEW_PEERTUBE_VERSION -->
203 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
204
205 <div class="message" i18n>
206 <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
207 </div>
208 </ng-container>
209
194 <ng-container *ngSwitchDefault> 210 <ng-container *ngSwitchDefault>
195 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> 211 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
196 212
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.ts b/client/src/app/shared/shared-main/users/user-notifications.component.ts
index 387c49d94..d7c722355 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.ts
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.ts
@@ -21,9 +21,6 @@ export class UserNotificationsComponent implements OnInit {
21 notifications: UserNotification[] = [] 21 notifications: UserNotification[] = []
22 sortField = 'createdAt' 22 sortField = 'createdAt'
23 23
24 // So we can access it in the template
25 UserNotificationType = UserNotificationType
26
27 componentPagination: ComponentPagination 24 componentPagination: ComponentPagination
28 25
29 onDataSubject = new Subject<any[]>() 26 onDataSubject = new Subject<any[]>()
@@ -48,7 +45,7 @@ export class UserNotificationsComponent implements OnInit {
48 } 45 }
49 46
50 loadNotifications (reset?: boolean) { 47 loadNotifications (reset?: boolean) {
51 this.userNotificationService.listMyNotifications({ 48 const options = {
52 pagination: this.componentPagination, 49 pagination: this.componentPagination,
53 ignoreLoadingBar: this.ignoreLoadingBar, 50 ignoreLoadingBar: this.ignoreLoadingBar,
54 sort: { 51 sort: {
@@ -56,7 +53,9 @@ export class UserNotificationsComponent implements OnInit {
56 // if we order by creation date, we want DESC. all other fields are ASC (like unread). 53 // if we order by creation date, we want DESC. all other fields are ASC (like unread).
57 order: this.sortField === 'createdAt' ? -1 : 1 54 order: this.sortField === 'createdAt' ? -1 : 1
58 } 55 }
59 }) 56 }
57
58 this.userNotificationService.listMyNotifications(options)
60 .subscribe( 59 .subscribe(
61 result => { 60 result => {
62 this.notifications = reset ? result.data : this.notifications.concat(result.data) 61 this.notifications = reset ? result.data : this.notifications.concat(result.data)
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 c6a63fe6c..1ba3fcc0e 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
@@ -1,15 +1,22 @@
1import { VideoChannel as ServerVideoChannel, ViewsPerDate, Account, Avatar } from '@shared/models' 1import { getAbsoluteAPIUrl } from '@app/helpers'
2import { Account as ServerAccount, ActorImage, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models'
3import { Account } from '../account/account.model'
2import { Actor } from '../account/actor.model' 4import { Actor } from '../account/actor.model'
3 5
4export class VideoChannel extends Actor implements ServerVideoChannel { 6export class VideoChannel extends Actor implements ServerVideoChannel {
5 displayName: string 7 displayName: string
6 description: string 8 description: string
7 support: string 9 support: string
10
8 isLocal: boolean 11 isLocal: boolean
12
9 nameWithHost: string 13 nameWithHost: string
10 nameWithHostForced: string 14 nameWithHostForced: string
11 15
12 ownerAccount?: Account 16 banner: ActorImage
17 bannerUrl: string
18
19 ownerAccount?: ServerAccount
13 ownerBy?: string 20 ownerBy?: string
14 ownerAvatarUrl?: string 21 ownerAvatarUrl?: string
15 22
@@ -21,19 +28,33 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
21 return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL() 28 return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL()
22 } 29 }
23 30
31 static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) {
32 if (channel?.banner?.url) return channel.banner.url
33
34 if (channel && channel.banner) {
35 const absoluteAPIUrl = getAbsoluteAPIUrl()
36
37 return absoluteAPIUrl + channel.banner.path
38 }
39
40 return ''
41 }
42
24 static GET_DEFAULT_AVATAR_URL () { 43 static GET_DEFAULT_AVATAR_URL () {
25 return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png` 44 return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png`
26 } 45 }
27 46
28 constructor (hash: ServerVideoChannel) { 47 constructor (hash: Partial<ServerVideoChannel>) {
29 super(hash) 48 super(hash)
30 49
31 this.updateComputedAttributes()
32
33 this.displayName = hash.displayName 50 this.displayName = hash.displayName
34 this.description = hash.description 51 this.description = hash.description
35 this.support = hash.support 52 this.support = hash.support
53
54 this.banner = hash.banner
55
36 this.isLocal = hash.isLocal 56 this.isLocal = hash.isLocal
57
37 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) 58 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
38 this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) 59 this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true)
39 60
@@ -46,22 +67,34 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
46 if (hash.ownerAccount) { 67 if (hash.ownerAccount) {
47 this.ownerAccount = hash.ownerAccount 68 this.ownerAccount = hash.ownerAccount
48 this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host) 69 this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
49 this.ownerAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.ownerAccount) 70 this.ownerAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.ownerAccount)
50 } 71 }
72
73 this.updateComputedAttributes()
51 } 74 }
52 75
53 updateAvatar (newAvatar: Avatar) { 76 updateAvatar (newAvatar: ActorImage) {
54 this.avatar = newAvatar 77 this.avatar = newAvatar
55 78
56 this.updateComputedAttributes() 79 this.updateComputedAttributes()
57 } 80 }
58 81
59 resetAvatar () { 82 resetAvatar () {
60 this.avatar = null 83 this.updateAvatar(null)
61 this.avatarUrl = VideoChannel.GET_DEFAULT_AVATAR_URL() 84 }
85
86 updateBanner (newBanner: ActorImage) {
87 this.banner = newBanner
88
89 this.updateComputedAttributes()
90 }
91
92 resetBanner () {
93 this.updateBanner(null)
62 } 94 }
63 95
64 private updateComputedAttributes () { 96 updateComputedAttributes () {
65 this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this) 97 this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this)
98 this.bannerUrl = VideoChannel.GET_ACTOR_BANNER_URL(this)
66 } 99 }
67} 100}
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
index eff3fad4d..e65261763 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
@@ -3,7 +3,7 @@ import { catchError, map, tap } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' 5import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
6import { Avatar, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models' 6import { ActorImage, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models'
7import { environment } from '../../../../environments/environment' 7import { environment } from '../../../../environments/environment'
8import { Account } from '../account' 8import { Account } from '../account'
9import { AccountService } from '../account/account.service' 9import { AccountService } from '../account/account.service'
@@ -82,15 +82,15 @@ export class VideoChannelService {
82 ) 82 )
83 } 83 }
84 84
85 changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) { 85 changeVideoChannelImage (videoChannelName: string, avatarForm: FormData, type: 'avatar' | 'banner') {
86 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick' 86 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type + '/pick'
87 87
88 return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm) 88 return this.authHttp.post<{ avatar?: ActorImage, banner?: ActorImage }>(url, avatarForm)
89 .pipe(catchError(err => this.restExtractor.handleError(err))) 89 .pipe(catchError(err => this.restExtractor.handleError(err)))
90 } 90 }
91 91
92 deleteVideoChannelAvatar (videoChannelName: string) { 92 deleteVideoChannelImage (videoChannelName: string, type: 'avatar' | 'banner') {
93 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar' 93 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type
94 94
95 return this.authHttp.delete(url) 95 return this.authHttp.delete(url)
96 .pipe( 96 .pipe(
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 adb6e884f..1c2c4a575 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -6,7 +6,7 @@ import { Actor } from '@app/shared/shared-main/account/actor.model'
6import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model' 6import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model'
7import { peertubeTranslate } from '@shared/core-utils/i18n' 7import { peertubeTranslate } from '@shared/core-utils/i18n'
8import { 8import {
9 Avatar, 9 ActorImage,
10 ServerConfig, 10 ServerConfig,
11 UserRight, 11 UserRight,
12 Video as VideoServerModel, 12 Video as VideoServerModel,
@@ -72,7 +72,7 @@ export class Video implements VideoServerModel {
72 displayName: string 72 displayName: string
73 url: string 73 url: string
74 host: string 74 host: string
75 avatar?: Avatar 75 avatar?: ActorImage
76 } 76 }
77 77
78 channel: { 78 channel: {
@@ -81,7 +81,7 @@ export class Video implements VideoServerModel {
81 displayName: string 81 displayName: string
82 url: string 82 url: string
83 host: string 83 host: string
84 avatar?: Avatar 84 avatar?: ActorImage
85 } 85 }
86 86
87 userHistory?: { 87 userHistory?: {
diff --git a/client/src/app/shared/shared-moderation/moderation.scss b/client/src/app/shared/shared-moderation/moderation.scss
index 4a4e05535..cdcc12fe0 100644
--- a/client/src/app/shared/shared-moderation/moderation.scss
+++ b/client/src/app/shared/shared-moderation/moderation.scss
@@ -32,7 +32,7 @@
32 color: pvar(--inputPlaceholderColor); 32 color: pvar(--inputPlaceholderColor);
33 } 33 }
34 34
35 @include large-screen-ratio($selector: 'div, ::ng-deep iframe') { 35 @include block-ratio($selector: 'div, ::ng-deep iframe') {
36 width: 100% !important; 36 width: 100% !important;
37 height: 100% !important; 37 height: 100% !important;
38 left: 0; 38 left: 0;
diff --git a/client/src/app/shared/shared-moderation/report-modals/report.component.scss b/client/src/app/shared/shared-moderation/report-modals/report.component.scss
index b2606cbd8..0567330f5 100644
--- a/client/src/app/shared/shared-moderation/report-modals/report.component.scss
+++ b/client/src/app/shared/shared-moderation/report-modals/report.component.scss
@@ -21,7 +21,7 @@ textarea {
21} 21}
22 22
23.screenratio { 23.screenratio {
24 @include large-screen-ratio($selector: 'div, ::ng-deep iframe') { 24 @include block-ratio($selector: 'div, ::ng-deep iframe') {
25 left: 0; 25 left: 0;
26 }; 26 };
27} 27}
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
index 5b06c0bc7..4ca6f52ad 100644
--- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
@@ -61,7 +61,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
61 baseUrl: this.video.embedUrl, 61 baseUrl: this.video.embedUrl,
62 title: false, 62 title: false,
63 warningTitle: false 63 warningTitle: false
64 }) 64 }),
65 this.video.name
65 ) 66 )
66 ) 67 )
67 } 68 }
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.ts b/client/src/app/shared/shared-share-modal/video-share.component.ts
index b06ff3751..e8760bfcc 100644
--- a/client/src/app/shared/shared-share-modal/video-share.component.ts
+++ b/client/src/app/shared/shared-share-modal/video-share.component.ts
@@ -86,14 +86,14 @@ export class VideoShareComponent {
86 const options = this.getVideoOptions(this.video.embedUrl) 86 const options = this.getVideoOptions(this.video.embedUrl)
87 87
88 const embedUrl = buildVideoLink(options) 88 const embedUrl = buildVideoLink(options)
89 return buildVideoOrPlaylistEmbed(embedUrl) 89 return buildVideoOrPlaylistEmbed(embedUrl, this.video.name)
90 } 90 }
91 91
92 getPlaylistIframeCode () { 92 getPlaylistIframeCode () {
93 const options = this.getPlaylistOptions(this.playlist.embedUrl) 93 const options = this.getPlaylistOptions(this.playlist.embedUrl)
94 94
95 const embedUrl = buildPlaylistLink(options) 95 const embedUrl = buildPlaylistLink(options)
96 return buildVideoOrPlaylistEmbed(embedUrl) 96 return buildVideoOrPlaylistEmbed(embedUrl, this.playlist.displayName)
97 } 97 }
98 98
99 getVideoUrl () { 99 getVideoUrl () {
diff --git a/client/src/app/shared/shared-support-modal/index.ts b/client/src/app/shared/shared-support-modal/index.ts
new file mode 100644
index 000000000..f41bb4bc2
--- /dev/null
+++ b/client/src/app/shared/shared-support-modal/index.ts
@@ -0,0 +1,3 @@
1export * from './support-modal.component'
2
3export * from './shared-support-modal.module'
diff --git a/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts b/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts
new file mode 100644
index 000000000..1101d5535
--- /dev/null
+++ b/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts
@@ -0,0 +1,24 @@
1import { NgModule } from '@angular/core'
2import { SharedFormModule } from '../shared-forms'
3import { SharedGlobalIconModule } from '../shared-icons'
4import { SharedMainModule } from '../shared-main/shared-main.module'
5import { SupportModalComponent } from './support-modal.component'
6
7@NgModule({
8 imports: [
9 SharedMainModule,
10 SharedFormModule,
11 SharedGlobalIconModule
12 ],
13
14 declarations: [
15 SupportModalComponent
16 ],
17
18 exports: [
19 SupportModalComponent
20 ],
21
22 providers: [ ]
23})
24export class SharedSupportModal { }
diff --git a/client/src/app/+videos/+video-watch/modal/video-support.component.html b/client/src/app/shared/shared-support-modal/support-modal.component.html
index 935656d23..4a967987f 100644
--- a/client/src/app/+videos/+video-watch/modal/video-support.component.html
+++ b/client/src/app/shared/shared-support-modal/support-modal.component.html
@@ -1,10 +1,10 @@
1<ng-template #modal let-hide="close"> 1<ng-template #modal let-hide="close">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Support {{ video.account.displayName }}</h4> 3 <h4 i18n class="modal-title">Support {{ displayName }}</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> 7 <div class="modal-body" [innerHTML]="htmlSupport"></div>
8 8
9 <div class="modal-footer inputs"> 9 <div class="modal-footer inputs">
10 <input 10 <input
diff --git a/client/src/app/+videos/+video-watch/modal/video-support.component.scss b/client/src/app/shared/shared-support-modal/support-modal.component.scss
index 184e09027..184e09027 100644
--- a/client/src/app/+videos/+video-watch/modal/video-support.component.scss
+++ b/client/src/app/shared/shared-support-modal/support-modal.component.scss
diff --git a/client/src/app/shared/shared-support-modal/support-modal.component.ts b/client/src/app/shared/shared-support-modal/support-modal.component.ts
new file mode 100644
index 000000000..ae603c7a8
--- /dev/null
+++ b/client/src/app/shared/shared-support-modal/support-modal.component.ts
@@ -0,0 +1,40 @@
1import { Component, Input, ViewChild } from '@angular/core'
2import { MarkdownService } from '@app/core'
3import { VideoDetails } from '@app/shared/shared-main'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { VideoChannel } from '@shared/models'
6
7@Component({
8 selector: 'my-support-modal',
9 templateUrl: './support-modal.component.html',
10 styleUrls: [ './support-modal.component.scss' ]
11})
12export class SupportModalComponent {
13 @Input() video: VideoDetails = null
14 @Input() videoChannel: VideoChannel = null
15
16 @ViewChild('modal', { static: true }) modal: NgbModal
17
18 htmlSupport = ''
19 displayName = ''
20
21 constructor (
22 private markdownService: MarkdownService,
23 private modalService: NgbModal
24 ) { }
25
26 show () {
27 const modalRef = this.modalService.open(this.modal, { centered: true })
28
29 const support = this.video?.support || this.videoChannel.support
30
31 this.markdownService.enhancedMarkdownToHTML(support)
32 .then(r => this.htmlSupport = r)
33
34 this.displayName = this.video
35 ? this.video.channel.displayName
36 : this.videoChannel.displayName
37
38 return modalRef
39 }
40}
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.html b/client/src/app/shared/shared-video-miniature/abstract-video-list.html
index 07f79cd6d..ee5df28be 100644
--- a/client/src/app/shared/shared-video-miniature/abstract-video-list.html
+++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.html
@@ -43,7 +43,7 @@
43 <div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div> 43 <div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div>
44 <div 44 <div
45 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" 45 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()"
46 class="videos" 46 class="videos" [ngClass]="{ 'display-as-row': displayAsRow() }"
47 > 47 >
48 <ng-container *ngFor="let video of videos; trackBy: videoById;"> 48 <ng-container *ngFor="let video of videos; trackBy: videoById;">
49 <h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)"> 49 <h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)">
@@ -52,8 +52,7 @@
52 52
53 <div class="video-wrapper"> 53 <div class="video-wrapper">
54 <my-video-miniature 54 <my-video-miniature
55 [fitWidth]="true" 55 [video]="video" [user]="userMiniature" [displayAsRow]="displayAsRow()"
56 [video]="video" [user]="userMiniature" [ownerDisplayType]="ownerDisplayType"
57 [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" 56 [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions"
58 (videoBlocked)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" 57 (videoBlocked)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)"
59 > 58 >
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss
index 0a8aa8fa4..6570b63d0 100644
--- a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss
+++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss
@@ -69,7 +69,16 @@ $iconSize: 16px;
69} 69}
70 70
71.margin-content { 71.margin-content {
72 @include fluid-videos-miniature-layout; 72 @include grid-videos-miniature-layout;
73}
74
75.display-as-row.videos {
76 margin-left: pvar(--horizontalMarginContent);
77 margin-right: pvar(--horizontalMarginContent);
78
79 .video-wrapper {
80 margin-bottom: 15px;
81 }
73} 82}
74 83
75@media screen and (max-width: $mobile-view) { 84@media screen and (max-width: $mobile-view) {
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
index c13cb3748..f83380513 100644
--- a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
+++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
@@ -28,8 +28,8 @@ import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@sha
28import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models' 28import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models'
29import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' 29import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
30import { Syndication, Video } from '../shared-main' 30import { Syndication, Video } from '../shared-main'
31import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component'
32import { GenericHeaderComponent, VideoListHeaderComponent } from './video-list-header.component' 31import { GenericHeaderComponent, VideoListHeaderComponent } from './video-list-header.component'
32import { MiniatureDisplayOptions } from './video-miniature.component'
33 33
34enum GroupDate { 34enum GroupDate {
35 UNKNOWN = 0, 35 UNKNOWN = 0,
@@ -65,7 +65,6 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, AfterConte
65 loadOnInit = true 65 loadOnInit = true
66 loadUserVideoPreferences = false 66 loadUserVideoPreferences = false
67 67
68 ownerDisplayType: OwnerDisplayType = 'account'
69 displayModerationBlock = false 68 displayModerationBlock = false
70 titleTooltip: string 69 titleTooltip: string
71 displayVideoActions = true 70 displayVideoActions = true
@@ -320,6 +319,11 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, AfterConte
320 viewContainerRef.createComponent(componentFactory, 0, injector) 319 viewContainerRef.createComponent(componentFactory, 0, injector)
321 } 320 }
322 321
322 // Can be redefined by child
323 displayAsRow () {
324 return false
325 }
326
323 // On videos hook for children that want to do something 327 // On videos hook for children that want to do something
324 protected onMoreVideos () { /* empty */ } 328 protected onMoreVideos () { /* empty */ }
325 329
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 4608e93e7..8a9218343 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
@@ -17,85 +17,116 @@
17 </div> 17 </div>
18 18
19 <div class="modal-body"> 19 <div class="modal-body">
20 <div class="form-group"> 20 <div class="alert alert-warning" *ngIf="isConfidentialVideo()" i18n>
21 <div class="alert alert-warning" *ngIf="isConfidentialVideo()" i18n> 21 The following link contains a private token and should not be shared with anyone.
22 The following link contains a private token and should not be shared with anyone. 22 </div>
23 </div>
24 23
24 <ng-container *ngIf="type === 'subtitles'">
25 <div class="input-group input-group-sm"> 25 <div class="input-group input-group-sm">
26 <div class="input-group-prepend peertube-select-container">
27 <select *ngIf="type === 'video'" [(ngModel)]="resolutionId" (ngModelChange)="onResolutionIdChange()">
28 <option *ngFor="let file of getVideoFiles()" [value]="file.resolution.id">{{ file.resolution.label }}</option>
29 </select>
30
31 <select *ngIf="type === 'subtitles'" [(ngModel)]="subtitleLanguageId">
32 <option *ngFor="let caption of videoCaptions" [value]="caption.language.id">{{ caption.language.label }}</option>
33 </select>
34 </div>
35
36 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> 26 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
37 <div class="input-group-append" *ngIf="!isConfidentialVideo()"> 27 <div class="input-group-append" *ngIf="!isConfidentialVideo()">
38 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> 28 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
39 <span class="glyphicon glyphicon-copy"></span> 29 <span class="glyphicon glyphicon-duplicate"></span>
40 </button> 30 </button>
41 </div> 31 </div>
42 </div> 32 </div>
43 </div> 33 </ng-container>
44 34
45 <ng-container *ngIf="type === 'video' && videoFile?.metadata"> 35 <ng-container *ngIf="type === 'video'">
46 <div ngbNav #nav="ngbNav" class="nav-tabs"> 36 <div ngbNav #resolutionNav="ngbNav" class="nav-tabs" [activeId]="resolutionId" (activeIdChange)="onResolutionIdChange($event)">
47 37
48 <ng-container ngbNavItem> 38 <ng-container *ngFor="let file of getVideoFiles()" [ngbNavItem]="file.resolution.id">
49 <a ngbNavLink i18n>Format</a> 39 <a ngbNavLink i18n>{{ file.resolution.label }}</a>
40
50 <ng-template ngbNavContent> 41 <ng-template ngbNavContent>
51 <div class="file-metadata"> 42 <div class="nav-content">
52 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue"> 43 <div class="input-group input-group-sm">
53 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> 44 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
54 <span class="metadata-attribute-value">{{ item.value.value }}</span> 45 <div class="input-group-append" *ngIf="!isConfidentialVideo()">
46 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
47 <span class="glyphicon glyphicon-duplicate"></span>
48 </button>
49 </div>
55 </div> 50 </div>
56 </div> 51 </div>
57 </ng-template> 52 </ng-template>
58 </ng-container> 53 </ng-container>
59 54 </div>
60 <ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined"> 55 <div [ngbNavOutlet]="resolutionNav"></div>
61 <a ngbNavLink i18n>Video stream</a> 56
62 <ng-template ngbNavContent> 57 <div class="advanced-filters collapse-transition" [ngbCollapse]="isAdvancedCustomizationCollapsed">
63 <div class="file-metadata"> 58 <ng-container *ngIf="videoFile?.metadata">
64 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue"> 59 <div ngbNav #nav="ngbNav" class="nav-tabs nav-metadata">
65 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> 60 <ng-container ngbNavItem>
66 <span class="metadata-attribute-value">{{ item.value.value }}</span> 61 <a ngbNavLink i18n>Format</a>
67 </div> 62 <ng-template ngbNavContent>
63 <div class="file-metadata">
64 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue">
65 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
66 <span class="metadata-attribute-value">{{ item.value.value }}</span>
67 </div>
68 </div>
69 </ng-template>
70
71 <ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined">
72 <a ngbNavLink i18n>Video stream</a>
73 <ng-template ngbNavContent>
74 <div class="file-metadata">
75 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue">
76 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
77 <span class="metadata-attribute-value">{{ item.value.value }}</span>
78 </div>
79 </div>
80 </ng-template>
81 </ng-container>
82
83 <ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined">
84 <a ngbNavLink i18n>Audio stream</a>
85 <ng-template ngbNavContent>
86 <div class="file-metadata">
87 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue">
88 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
89 <span class="metadata-attribute-value">{{ item.value.value }}</span>
90 </div>
91 </div>
92 </ng-template>
93 </ng-container>
94
95 </ng-container>
96 </div>
97 <div [ngbNavOutlet]="nav"></div>
98 <div class="download-type">
99 <div class="peertube-radio-container">
100 <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct">
101 <label i18n for="download-direct">Direct download</label>
68 </div> 102 </div>
69 </ng-template> 103 <div class="peertube-radio-container">
70 </ng-container> 104 <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent">
71 105 <label i18n for="download-torrent">Torrent (.torrent file)</label>
72 <ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined">
73 <a ngbNavLink i18n>Audio stream</a>
74 <ng-template ngbNavContent>
75 <div class="file-metadata">
76 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue">
77 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
78 <span class="metadata-attribute-value">{{ item.value.value }}</span>
79 </div>
80 </div> 106 </div>
81 </ng-template> 107 </div>
82 </ng-container> 108 </ng-container>
83 </div> 109 </div>
84 110
85 <div [ngbNavOutlet]="nav"></div> 111 <div (click)="isAdvancedCustomizationCollapsed = !isAdvancedCustomizationCollapsed" role="button" class="advanced-filters-button"
86 </ng-container> 112 [attr.aria-expanded]="!isAdvancedCustomizationCollapsed" aria-controls="collapseBasic">
87 113 <ng-container *ngIf="isAdvancedCustomizationCollapsed">
88 <div class="download-type" *ngIf="type === 'video'"> 114 <span class="glyphicon glyphicon-menu-down"></span>
89 <div class="peertube-radio-container"> 115
90 <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct"> 116 <ng-container i18n>
91 <label i18n for="download-direct">Direct download</label> 117 Advanced
92 </div> 118 </ng-container>
93 119 </ng-container>
94 <div class="peertube-radio-container"> 120
95 <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent"> 121 <ng-container *ngIf="!isAdvancedCustomizationCollapsed">
96 <label i18n for="download-torrent">Torrent (.torrent file)</label> 122 <span class="glyphicon glyphicon-menu-up"></span>
123
124 <ng-container i18n>
125 Simple
126 </ng-container>
127 </ng-container>
97 </div> 128 </div>
98 </div> 129 </ng-container>
99 </div> 130 </div>
100 131
101 <div class="modal-footer inputs"> 132 <div class="modal-footer inputs">
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 d407e9531..199c3dac8 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
@@ -1,6 +1,28 @@
1@import 'variables'; 1@import 'variables';
2@import 'mixins'; 2@import 'mixins';
3 3
4.nav-content {
5 margin-top: 30px;
6}
7
8.advanced-filters-button {
9 display: flex;
10 justify-content: center;
11 align-items: center;
12 margin-top: 20px;
13 font-size: 16px;
14 font-weight: 600;
15 cursor: pointer;
16
17 .nav-tabs {
18 margin-top: 10x;
19 }
20
21 .glyphicon {
22 margin-right: 5px;
23 }
24}
25
4.peertube-select-container { 26.peertube-select-container {
5 @include peertube-select-container(85px); 27 @include peertube-select-container(85px);
6 28
@@ -15,12 +37,21 @@
15 } 37 }
16} 38}
17 39
40.action-button-cancel {
41 @include peertube-button-link;
42}
43
44.action-button-submit {
45 @include peertube-button-link;
46 @include orange-button;
47}
48
18#dropdownDownloadType { 49#dropdownDownloadType {
19 cursor: pointer; 50 cursor: pointer;
20} 51}
21 52
22.download-type { 53.download-type {
23 margin-top: 30px; 54 margin-top: 20px;
24 55
25 .peertube-radio-container { 56 .peertube-radio-container {
26 @include peertube-radio-container; 57 @include peertube-radio-container;
@@ -30,6 +61,10 @@
30 } 61 }
31} 62}
32 63
64.nav-metadata {
65 margin-top: 20px;
66}
67
33.file-metadata { 68.file-metadata {
34 padding: 1rem; 69 padding: 1rem;
35} 70}
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts
index 90f4daf7c..1e3745d94 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.ts
@@ -1,7 +1,9 @@
1import { mapValues, pick } from 'lodash-es' 1import { mapValues, pick } from 'lodash-es'
2import { pipe } from 'rxjs'
3import { tap } from 'rxjs/operators'
2import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' 4import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core'
3import { AuthService, Notifier } from '@app/core' 5import { AuthService, HooksService, Notifier } from '@app/core'
4import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
5import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' 7import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models'
6import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' 8import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main'
7 9
@@ -16,7 +18,7 @@ type FileMetadata = { [key: string]: { label: string, value: string }}
16export class VideoDownloadComponent { 18export class VideoDownloadComponent {
17 @ViewChild('modal', { static: true }) modal: ElementRef 19 @ViewChild('modal', { static: true }) modal: ElementRef
18 20
19 downloadType: 'direct' | 'torrent' = 'torrent' 21 downloadType: 'direct' | 'torrent' = 'direct'
20 resolutionId: number | string = -1 22 resolutionId: number | string = -1
21 subtitleLanguageId: string 23 subtitleLanguageId: string
22 24
@@ -26,7 +28,9 @@ export class VideoDownloadComponent {
26 videoFileMetadataVideoStream: FileMetadata | undefined 28 videoFileMetadataVideoStream: FileMetadata | undefined
27 videoFileMetadataAudioStream: FileMetadata | undefined 29 videoFileMetadataAudioStream: FileMetadata | undefined
28 videoCaptions: VideoCaption[] 30 videoCaptions: VideoCaption[]
29 activeModal: NgbActiveModal 31 activeModal: NgbModalRef
32
33 isAdvancedCustomizationCollapsed = true
30 34
31 type: DownloadType = 'video' 35 type: DownloadType = 'video'
32 36
@@ -38,7 +42,8 @@ export class VideoDownloadComponent {
38 private notifier: Notifier, 42 private notifier: Notifier,
39 private modalService: NgbModal, 43 private modalService: NgbModal,
40 private videoService: VideoService, 44 private videoService: VideoService,
41 private auth: AuthService 45 private auth: AuthService,
46 private hooks: HooksService
42 ) { 47 ) {
43 this.bytesPipe = new BytesPipe() 48 this.bytesPipe = new BytesPipe()
44 this.numbersPipe = new NumberFormatterPipe(this.localeId) 49 this.numbersPipe = new NumberFormatterPipe(this.localeId)
@@ -62,9 +67,13 @@ export class VideoDownloadComponent {
62 67
63 this.activeModal = this.modalService.open(this.modal, { centered: true }) 68 this.activeModal = this.modalService.open(this.modal, { centered: true })
64 69
65 this.resolutionId = this.getVideoFiles()[0].resolution.id 70 this.onResolutionIdChange(this.getVideoFiles()[0].resolution.id)
66 this.onResolutionIdChange() 71
67 if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id 72 if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id
73
74 this.activeModal.shown.subscribe(() => {
75 this.hooks.runAction('action:modal.video-download.shown', 'common')
76 })
68 } 77 }
69 78
70 onClose () { 79 onClose () {
@@ -83,11 +92,15 @@ export class VideoDownloadComponent {
83 : this.getVideoFileLink() 92 : this.getVideoFileLink()
84 } 93 }
85 94
86 async onResolutionIdChange () { 95 async onResolutionIdChange (resolutionId: number) {
96 this.resolutionId = resolutionId
87 this.videoFile = this.getVideoFile() 97 this.videoFile = this.getVideoFile()
88 if (this.videoFile.metadata || !this.videoFile.metadataUrl) return
89 98
90 await this.hydrateMetadataFromMetadataUrl(this.videoFile) 99 if (!this.videoFile.metadata) {
100 if (!this.videoFile.metadataUrl) return
101
102 await this.hydrateMetadataFromMetadataUrl(this.videoFile)
103 }
91 104
92 this.videoFileMetadataFormat = this.videoFile 105 this.videoFileMetadataFormat = this.videoFile
93 ? this.getMetadataFormat(this.videoFile.metadata.format) 106 ? this.getMetadataFormat(this.videoFile.metadata.format)
@@ -101,9 +114,6 @@ export class VideoDownloadComponent {
101 } 114 }
102 115
103 getVideoFile () { 116 getVideoFile () {
104 // HTML select send us a string, so convert it to a number
105 this.resolutionId = parseInt(this.resolutionId.toString(), 10)
106
107 const file = this.getVideoFiles().find(f => f.resolution.id === this.resolutionId) 117 const file = this.getVideoFiles().find(f => f.resolution.id === this.resolutionId)
108 if (!file) { 118 if (!file) {
109 console.error('Could not find file with resolution %d.', this.resolutionId) 119 console.error('Could not find file with resolution %d.', this.resolutionId)
@@ -201,7 +211,7 @@ export class VideoDownloadComponent {
201 211
202 private hydrateMetadataFromMetadataUrl (file: VideoFile) { 212 private hydrateMetadataFromMetadataUrl (file: VideoFile) {
203 const observable = this.videoService.getVideoFileMetadata(file.metadataUrl) 213 const observable = this.videoService.getVideoFileMetadata(file.metadataUrl)
204 observable.subscribe(res => file.metadata = res) 214 .pipe(tap(res => file.metadata = res))
205 215
206 return observable.toPromise() 216 return observable.toPromise()
207 } 217 }
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
index 7a6df7b64..bac8bcc2d 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
@@ -1,4 +1,4 @@
1<div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow, 'fit-width': fitWidth }" (mouseenter)="loadActions()"> 1<div class="video-miniature" [ngClass]="getClasses()" (mouseenter)="loadActions()">
2 <my-video-thumbnail 2 <my-video-thumbnail
3 [video]="video" [nsfw]="isVideoBlur" [videoRouterLink]="videoRouterLink" [videoHref]="videoHref" [videoTarget]="videoTarget" 3 [video]="video" [nsfw]="isVideoBlur" [videoRouterLink]="videoRouterLink" [videoHref]="videoHref" [videoTarget]="videoTarget"
4 [displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)" 4 [displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)"
@@ -9,9 +9,9 @@
9 9
10 <div class="video-bottom"> 10 <div class="video-bottom">
11 <div class="video-miniature-information"> 11 <div class="video-miniature-information">
12 <div class="d-inline-flex video-miniature-meta"> 12 <div class="d-flex video-miniature-meta">
13 <a *ngIf="displayOptions.avatar" class="avatar" [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> 13 <a *ngIf="displayOptions.avatar" class="avatar" [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
14 <img [src]="getAvatarUrl()" alt="" /> 14 <img [src]="getAvatarUrl()" alt="" [ngClass]="{ channel: displayOwnerVideoChannel() }" />
15 </a> 15 </a>
16 16
17 <div class="w-100 d-flex flex-column"> 17 <div class="w-100 d-flex flex-column">
@@ -33,7 +33,7 @@
33 </span> 33 </span>
34 </span> 34 </span>
35 35
36 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]"> 36 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
37 {{ video.byAccount }} 37 {{ video.byAccount }}
38 </a> 38 </a>
39 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> 39 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
index 38cac5b6e..621951919 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
@@ -3,198 +3,205 @@
3@import '_miniature'; 3@import '_miniature';
4 4
5$more-button-width: 40px; 5$more-button-width: 40px;
6$more-margin-right: 15px;
7 6
8.video-miniature { 7.video-miniature-name {
9 display: inline-flex; 8 @include miniature-name;
10 flex-direction: column; 9}
11 padding-bottom: $video-miniature-margin-bottom;
12 vertical-align: top;
13 10
14 .video-bottom { 11.video-miniature-information {
15 display: flex; 12 width: calc(100% - #{$more-button-width});
13}
16 14
17 .video-miniature-information { 15.avatar {
18 width: $video-miniature-width - $more-button-width - $more-margin-right; 16 margin: 10px 10px 0 0;
19 line-height: normal;
20 17
21 .avatar { 18 img:not(.channel) {
22 margin: 10px 10px 0 0; 19 @include avatar(40px);
20 }
23 21
24 img { 22 img.channel {
25 @include avatar(40px); 23 @include channel-avatar(40px);
26 } 24 }
27 } 25}
28 26
29 .video-miniature-name { 27.video-miniature-created-at-views {
30 @include miniature-name; 28 font-size: 13px;
31 width: calc(100% - #{$more-button-width}); 29}
32 }
33 30
34 .video-miniature-meta { 31.video-miniature-account,
35 width: calc(100% + #{$more-button-width}); 32.video-miniature-channel {
36 overflow: hidden; 33 @include disable-default-a-behaviour;
37 } 34 @include ellipsis;
38 35
39 .video-miniature-created-at-views { 36 display: block;
40 display: block; 37 font-size: 13px;
41 font-size: 13px; 38 color: pvar(--greyForegroundColor);
42 }
43 39
44 .video-miniature-account, 40 &:hover {
45 .video-miniature-channel { 41 color: $grey-foreground-hover-color;
46 @include disable-default-a-behaviour; 42 }
47 @include ellipsis; 43}
48 44
49 display: block; 45.video-info-privacy,
50 font-size: 13px; 46.video-info-blocked .blocked-label,
51 color: pvar(--greyForegroundColor); 47.video-info-nsfw {
48 font-weight: $font-semibold;
49}
52 50
53 &:hover { 51.video-info-blocked {
54 color: $grey-foreground-hover-color; 52 color: red;
55 }
56 }
57 53
58 .video-info-privacy, 54 .blocked-reason::before {
59 .video-info-blocked .blocked-label, 55 content: ' - ';
60 .video-info-nsfw { 56 }
61 font-weight: $font-semibold; 57}
62 }
63 58
64 .video-info-blocked { 59.video-info-nsfw {
65 color: red; 60 color: red;
61}
66 62
67 .blocked-reason::before { 63.video-actions {
68 content: ' - '; 64 width: $more-button-width;
69 } 65 height: 30px;
70 }
71 66
72 .video-info-nsfw { 67 ::ng-deep .dropdown-root:not(.show) {
73 color: red; 68 opacity: 0;
74 } 69 }
75 }
76 70
77 .video-actions { 71 ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root {
78 margin-top: 3px; 72 opacity: 1;
79 width: $more-button-width; 73 }
80 height: 30px;
81 74
82 ::ng-deep .dropdown-root:not(.show) { 75 ::ng-deep .more-icon {
83 opacity: 0; 76 opacity: .6;
84 }
85 77
86 ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root { 78 &:hover {
87 opacity: 1; 79 opacity: 1;
88 } 80 }
81 }
82}
89 83
90 ::ng-deep .more-icon { 84.video-miniature {
91 opacity: .6; 85 &:hover ::ng-deep .video-thumbnail-actions-overlay,
86 &:hover .video-actions ::ng-deep .dropdown-root {
87 opacity: 1 !important;
88 }
89}
92 90
93 &:hover { 91// Grid mode
94 opacity: 1; 92// Takes all the width on mobile
95 } 93.video-miniature:not(.display-as-row) {
96 } 94 display: flex;
97 } 95 flex-direction: column;
96 padding-bottom: $video-miniature-margin-bottom;
97 width: 100%;
98 98
99 @media screen and (max-width: $small-view) { 99 my-video-thumbnail {
100 .video-miniature-information { 100 @include block-ratio($selector: '::ng-deep .video-thumbnail');
101 margin: 0 10px; 101 }
102 }
103 102
104 .video-actions { 103 .video-bottom {
105 margin: 0; 104 display: flex;
106 top: -3px; 105 width: 100%;
106 }
107 107
108 ::ng-deep .dropdown-root { 108 .video-miniature-name {
109 opacity: 1 !important; 109 margin-top: 10px;
110 } 110 margin-bottom: 5px;
111 }
112 }
113 } 111 }
114 112
115 &:hover ::ng-deep .video-thumbnail .video-thumbnail-actions-overlay, 113 .video-miniature-created-at-views {
116 &:hover .video-bottom .video-actions ::ng-deep .dropdown-root { 114 display: block;
117 opacity: 1;
118 } 115 }
119 116
120 &.fit-width { 117 .video-actions {
118 margin-top: 3px;
119 }
120
121 @media screen and (max-width: $small-view) {
121 width: 100%; 122 width: 100%;
123 margin-bottom: 25px;
124
125 .video-miniature-information {
126 margin: 0 10px;
127
128 width: 100%;
129 text-align: left;
130 }
122 131
123 .video-bottom { 132 .video-actions {
124 width: 100% !important; 133 margin: 0;
134 top: -3px;
125 135
126 .video-miniature-information { 136 ::ng-deep .dropdown-root {
127 width: calc(100% - #{$more-button-width}) !important; 137 opacity: 1 !important;
128 } 138 }
129 } 139 }
130 140
131 my-video-thumbnail { 141 ::ng-deep .video-thumbnail {
132 @include large-screen-ratio($selector: '::ng-deep .video-thumbnail'); 142 border-radius: 0;
133 } 143 }
134 } 144 }
145}
146
147.video-miniature.display-as-row {
148 --rowThumbnailWidth: #{$video-thumbnail-width};
149 --rowThumbnailHeight: #{$video-thumbnail-height};
150
151 display: flex;
152 flex-direction: row;
135 153
136 &.display-as-row { 154 .video-bottom {
137 flex-direction: row;
138 padding-bottom: 0;
139 height: auto;
140 display: flex; 155 display: flex;
141 flex-grow: 1; 156 }
142 157
143 my-video-thumbnail { 158 // We don't display avatar in row mode
144 margin-right: 10px; 159 .avatar {
145 } 160 display: none;
161 }
146 162
147 .video-bottom { 163 my-video-thumbnail {
148 .video-miniature-information { 164 min-width: var(--rowThumbnailWidth);
149 @media screen and (min-width: $small-view) { 165 max-width: var(--rowThumbnailWidth);
150 width: auto; 166 height: var(--rowThumbnailHeight);
151 min-width: 500px; 167 margin-right: 10px;
152 } 168 }
153
154 .video-miniature-name {
155 @include ellipsis-multiline(1.3em, 2);
156
157 margin-top: 2px;
158 margin-bottom: 5px;
159 }
160
161 .video-miniature-created-at-views,
162 .video-miniature-account,
163 .video-miniature-channel {
164 font-size: 95%;
165 width: fit-content;
166 }
167
168 .video-miniature-created-at-views + .video-miniature-channel {
169 margin-top: 5px;
170 }
171
172 .video-info-privacy {
173 margin-top: 5px;
174 }
175
176 .video-info-blocked {
177 margin-top: 3px;
178 }
179 }
180 169
181 .video-actions { 170 .video-miniature-name {
182 margin: 0; 171 @include ellipsis-multiline($video-miniature-row-name-font-size, 2);
183 top: -3px; 172 }
184 } 173
185 } 174 .video-miniature-created-at-views,
175 .video-miniature-account,
176 .video-miniature-channel {
177 font-size: $video-miniature-row-info-font-size;
178 }
186 179
187 @media screen and (max-width: $small-view) { 180 .video-actions {
188 flex-direction: column; 181 margin-top: -3px;
189 height: auto; 182 }
183}
190 184
191 my-video-thumbnail { 185@include on-small-main-col {
192 margin-right: 0; 186 .video-miniature.display-as-row {
193 } 187 --rowThumbnailWidth: #{$video-thumbnail-medium-width};
188 --rowThumbnailHeight: #{$video-thumbnail-medium-height};
189 }
190}
194 191
195 .video-miniature-information { 192@include on-mobile-main-col {
196 min-width: initial; 193 .video-miniature.display-as-row {
197 } 194 --rowThumbnailWidth: #{$video-thumbnail-small-width};
195 --rowThumbnailHeight: #{$video-thumbnail-small-height};
196
197 .video-miniature-name {
198 font-size: $video-miniature-row-info-font-size;
199 }
200
201 .video-miniature-created-at-views,
202 .video-miniature-account,
203 .video-miniature-channel {
204 font-size: $video-miniature-row-mobile-info-font-size;
198 } 205 }
199 } 206 }
200} 207}
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 cc5665ab1..48da92d6b 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
@@ -16,7 +16,6 @@ import { Video } from '../shared-main'
16import { VideoPlaylistService } from '../shared-video-playlist' 16import { VideoPlaylistService } from '../shared-video-playlist'
17import { VideoActionsDisplayType } from './video-actions-dropdown.component' 17import { VideoActionsDisplayType } from './video-actions-dropdown.component'
18 18
19export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto'
20export type MiniatureDisplayOptions = { 19export type MiniatureDisplayOptions = {
21 date?: boolean 20 date?: boolean
22 views?: boolean 21 views?: boolean
@@ -40,7 +39,6 @@ export class VideoMiniatureComponent implements OnInit {
40 @Input() user: User 39 @Input() user: User
41 @Input() video: Video 40 @Input() video: Video
42 41
43 @Input() ownerDisplayType: OwnerDisplayType = 'account'
44 @Input() displayOptions: MiniatureDisplayOptions = { 42 @Input() displayOptions: MiniatureDisplayOptions = {
45 date: true, 43 date: true,
46 views: true, 44 views: true,
@@ -51,9 +49,9 @@ export class VideoMiniatureComponent implements OnInit {
51 state: false, 49 state: false,
52 blacklistInfo: false 50 blacklistInfo: false
53 } 51 }
54 @Input() displayAsRow = false
55 @Input() displayVideoActions = true 52 @Input() displayVideoActions = true
56 @Input() fitWidth = false 53
54 @Input() displayAsRow = false
57 55
58 @Input() videoLinkType: VideoLinkType = 'internal' 56 @Input() videoLinkType: VideoLinkType = 'internal'
59 57
@@ -89,7 +87,7 @@ export class VideoMiniatureComponent implements OnInit {
89 videoHref: string 87 videoHref: string
90 videoTarget: string 88 videoTarget: string
91 89
92 private ownerDisplayTypeChosen: 'account' | 'videoChannel' 90 private ownerDisplayType: 'account' | 'videoChannel'
93 91
94 constructor ( 92 constructor (
95 private screenService: ScreenService, 93 private screenService: ScreenService,
@@ -140,11 +138,11 @@ export class VideoMiniatureComponent implements OnInit {
140 } 138 }
141 139
142 displayOwnerAccount () { 140 displayOwnerAccount () {
143 return this.ownerDisplayTypeChosen === 'account' 141 return this.ownerDisplayType === 'account'
144 } 142 }
145 143
146 displayOwnerVideoChannel () { 144 displayOwnerVideoChannel () {
147 return this.ownerDisplayTypeChosen === 'videoChannel' 145 return this.ownerDisplayType === 'videoChannel'
148 } 146 }
149 147
150 isUnlistedVideo () { 148 isUnlistedVideo () {
@@ -183,7 +181,7 @@ export class VideoMiniatureComponent implements OnInit {
183 } 181 }
184 182
185 getAvatarUrl () { 183 getAvatarUrl () {
186 if (this.ownerDisplayTypeChosen === 'account') { 184 if (this.displayOwnerAccount()) {
187 return this.video.accountAvatarUrl 185 return this.video.accountAvatarUrl
188 } 186 }
189 187
@@ -244,21 +242,26 @@ export class VideoMiniatureComponent implements OnInit {
244 return this.displayVideoActions && this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined 242 return this.displayVideoActions && this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined
245 } 243 }
246 244
247 private setUpBy () { 245 getClasses () {
248 if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') { 246 return {
249 this.ownerDisplayTypeChosen = this.ownerDisplayType 247 'display-as-row': this.displayAsRow
250 return
251 } 248 }
249 }
250
251 private setUpBy () {
252 const accountName = this.video.account.name
252 253
253 // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) 254 // If the video channel name is an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12)
255 // Or has not been customized (default created channel display name)
254 // -> Use the account name 256 // -> Use the account name
255 if ( 257 if (
256 this.video.channel.name === `${this.video.account.name}_channel` || 258 this.video.channel.displayName === `Default ${accountName} channel` ||
259 this.video.channel.displayName === `Main ${accountName} channel` ||
257 this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) 260 this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
258 ) { 261 ) {
259 this.ownerDisplayTypeChosen = 'account' 262 this.ownerDisplayType = 'account'
260 } else { 263 } else {
261 this.ownerDisplayTypeChosen = 'videoChannel' 264 this.ownerDisplayType = 'videoChannel'
262 } 265 }
263 } 266 }
264 267
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.html b/client/src/app/shared/shared-video-miniature/videos-selection.component.html
index 8caeaf092..dec9e99f3 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.html
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.html
@@ -9,8 +9,7 @@
9 9
10 <my-video-miniature 10 <my-video-miniature
11 [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions" 11 [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions"
12 [displayVideoActions]="false" [ownerDisplayType]="ownerDisplayType" 12 [displayVideoActions]="false" [user]="user"
13 [user]="user"
14 ></my-video-miniature> 13 ></my-video-miniature>
15 14
16 <!-- Display only once --> 15 <!-- Display only once -->
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.scss b/client/src/app/shared/shared-video-miniature/videos-selection.component.scss
index c33e11889..a2939d521 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.scss
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.scss
@@ -5,24 +5,24 @@
5 display: flex; 5 display: flex;
6 justify-content: flex-end; 6 justify-content: flex-end;
7 flex-grow: 1; 7 flex-grow: 1;
8}
8 9
9 .action-selection-mode-child { 10.action-selection-mode-child {
10 position: fixed; 11 position: fixed;
11
12 .action-button {
13 display: block;
14 margin-left: 55px;
15 }
16 12
17 .action-button-cancel-selection { 13 .action-button {
18 @include peertube-button; 14 display: block;
19 @include grey-button; 15 margin-left: 55px;
20 }
21 } 16 }
22} 17}
23 18
19.action-button-cancel-selection {
20 @include peertube-button;
21 @include grey-button;
22}
23
24.video { 24.video {
25 @include row-blocks; 25 @include row-blocks($column-responsive: false);
26 26
27 &:first-child { 27 &:first-child {
28 margin-top: 47px; 28 margin-top: 47px;
@@ -40,18 +40,16 @@
40 } 40 }
41} 41}
42 42
43@media screen and (max-width: $small-view) {
44 .video {
45 flex-direction: column;
46 height: auto;
47 43
48 .checkbox-container { 44@include on-small-main-col {
49 display: none; 45 .video {
50 } 46 flex-wrap: wrap;
47 }
48}
51 49
52 my-button { 50@include on-mobile-main-col {
53 margin-top: 10px; 51 .checkbox-container {
54 } 52 display: none;
55 } 53 }
56 54
57 .action-selection-mode { 55 .action-selection-mode {
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
index ca1cf2264..f8c3800d7 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
@@ -17,7 +17,7 @@ import { AuthService, ComponentPagination, LocalStorageService, Notifier, Screen
17import { ResultList, VideoSortField } from '@shared/models' 17import { ResultList, VideoSortField } from '@shared/models'
18import { PeerTubeTemplateDirective, Video } from '../shared-main' 18import { PeerTubeTemplateDirective, Video } from '../shared-main'
19import { AbstractVideoList } from './abstract-video-list' 19import { AbstractVideoList } from './abstract-video-list'
20import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component' 20import { MiniatureDisplayOptions } from './video-miniature.component'
21 21
22export type SelectionType = { [ id: number ]: boolean } 22export type SelectionType = { [ id: number ]: boolean }
23 23
@@ -31,7 +31,6 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
31 @Input() pagination: ComponentPagination 31 @Input() pagination: ComponentPagination
32 @Input() titlePage: string 32 @Input() titlePage: string
33 @Input() miniatureDisplayOptions: MiniatureDisplayOptions 33 @Input() miniatureDisplayOptions: MiniatureDisplayOptions
34 @Input() ownerDisplayType: OwnerDisplayType
35 34
36 @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>> 35 @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>>
37 36
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
index 86f6664cb..f50f95003 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
@@ -1,4 +1,4 @@
1<div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage }"> 1<div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage, 'display-as-row': displayAsRow }">
2 <a 2 <a
3 [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description" 3 [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description"
4 class="miniature-thumbnail" 4 class="miniature-thumbnail"
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
index 1b16dbb01..c5be5f292 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
@@ -4,6 +4,7 @@
4 4
5.miniature { 5.miniature {
6 display: inline-block; 6 display: inline-block;
7 width: 100%;
7 8
8 &.no-videos:not(.to-manage){ 9 &.no-videos:not(.to-manage){
9 a { 10 a {
@@ -17,62 +18,92 @@
17 display: none; 18 display: none;
18 } 19 }
19 } 20 }
21}
20 22
21 .miniature-thumbnail { 23.miniature-thumbnail {
22 @include miniature-thumbnail; 24 @include miniature-thumbnail;
23 25
24 .miniature-playlist-info-overlay { 26 .miniature-playlist-info-overlay {
25 @include static-thumbnail-overlay; 27 @include static-thumbnail-overlay;
26 28
27 position: absolute; 29 position: absolute;
28 right: 0; 30 right: 0;
29 bottom: 0; 31 bottom: 0;
30 height: $video-thumbnail-height; 32 height: 100%;
31 padding: 0 10px; 33 padding: 0 10px;
32 display: flex; 34 display: flex;
33 align-items: center; 35 align-items: center;
34 font-size: 14px; 36 font-size: 14px;
35 font-weight: $font-semibold; 37 font-weight: $font-semibold;
36 }
37 } 38 }
39}
38 40
39 .miniature-info { 41.miniature-info {
40 width: 200px;
41 margin-top: 2px;
42 line-height: normal;
43
44 .miniature-name {
45 @include miniature-name;
46 42
47 @include ellipsis-multiline(1.3em, 2); 43 .miniature-name {
44 @include miniature-name;
45 @include ellipsis-multiline(1.3em, 2);
48 46
49 margin: 0; 47 margin: 0;
50 } 48 }
51 49
52 .by { 50 .by {
53 @include disable-default-a-behaviour; 51 @include disable-default-a-behaviour;
54 52
55 display: block; 53 display: block;
56 color: pvar(--greyForegroundColor); 54 color: pvar(--greyForegroundColor);
57 } 55 }
58 56
59 .privacy-date { 57 .privacy-date {
60 margin-top: 5px; 58 margin-top: 5px;
61 59
62 .video-info-privacy { 60 .video-info-privacy {
63 font-size: 14px; 61 font-size: 14px;
64 font-weight: $font-semibold; 62 font-weight: $font-semibold;
65 63
66 &::after { 64 &::after {
67 content: '-'; 65 content: '-';
68 margin: 0 3px; 66 margin: 0 3px;
69 }
70 } 67 }
71 } 68 }
69 }
72 70
73 .video-info-description { 71 .video-info-description {
74 margin-top: 10px; 72 margin-top: 10px;
75 color: pvar(--greyForegroundColor); 73 color: pvar(--greyForegroundColor);
76 } 74 }
75}
76
77.miniature:not(.display-as-row) {
78 .miniature-thumbnail {
79 margin-top: 10px;
80 margin-bottom: 5px;
81 }
82}
83
84.miniature.display-as-row {
85 --rowThumbnailWidth: #{$video-thumbnail-width};
86 --rowThumbnailHeight: #{$video-thumbnail-height};
87
88 display: flex;
89
90 .miniature-thumbnail {
91 width: var(--rowThumbnailWidth);
92 height: var(--rowThumbnailHeight);
93 margin-right: 10px;
94 }
95}
96
97@include on-small-main-col {
98 .miniature.display-as-row {
99 --rowThumbnailWidth: #{$video-thumbnail-medium-width};
100 --rowThumbnailHeight: #{$video-thumbnail-medium-height};
101 }
102}
103
104@include on-mobile-main-col {
105 .miniature.display-as-row {
106 --rowThumbnailWidth: #{$video-thumbnail-small-width};
107 --rowThumbnailHeight: #{$video-thumbnail-small-height};
77 } 108 }
78} 109}
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
index 251aa868a..6b0b1056f 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
@@ -12,6 +12,7 @@ export class VideoPlaylistMiniatureComponent {
12 @Input() displayChannel = false 12 @Input() displayChannel = false
13 @Input() displayDescription = false 13 @Input() displayDescription = false
14 @Input() displayPrivacy = false 14 @Input() displayPrivacy = false
15 @Input() displayAsRow = false
15 16
16 getPlaylistUrl () { 17 getPlaylistUrl () {
17 if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ] 18 if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ]
diff --git a/client/src/assets/images/feather/cloud-download.svg b/client/src/assets/images/feather/cloud-download.svg
index 3a4e58df1..16526d338 100644
--- a/client/src/assets/images/feather/cloud-download.svg
+++ b/client/src/assets/images/feather/cloud-download.svg
@@ -1,6 +1,6 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> 1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2 <defs/> 2 <defs/>
3 <g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-width="2"> 3 <g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-width="2">
4 <path stroke-linejoin="round" d="M8 17H5h0a4 4 0 111-7.9v-.6a5.5 5.5 0 0110.8-1.4A5 5 0 0123 12a5 5 0 01-5 5h-2"/> 4 <path stroke-linejoin="round" d="M8 17H5h0a4 4 0 111-7.9v-.6a5.5 5.5 0 0110.8-1.4A5 5 0 0123 12a5 5 0 01-5 5h-2"/>
5 <path d="M12 13v8"/> 5 <path d="M12 13v8"/>
6 <path stroke-linejoin="round" d="M15 20l-3 3-3-3"/> 6 <path stroke-linejoin="round" d="M15 20l-3 3-3-3"/>
diff --git a/client/src/assets/images/feather/subscriptions.svg b/client/src/assets/images/feather/subscriptions.svg
deleted file mode 100644
index c7216352a..000000000
--- a/client/src/assets/images/feather/subscriptions.svg
+++ /dev/null
@@ -1,19 +0,0 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2 <defs/>
3 <defs>
4 <linearGradient id="a" x1="50%" x2="50%" y1="0%" y2="97.33%">
5 <stop stop-color="#000" offset="0%"/>
6 <stop stop-color="#000" offset="100%" stop-opacity=".25"/>
7 </linearGradient>
8 <linearGradient id="b" x1="50%" x2="50%" y1="0%" y2="97.86%">
9 <stop stop-color="#000" offset="0%"/>
10 <stop stop-color="#000" offset="100%" stop-opacity=".25"/>
11 </linearGradient>
12 </defs>
13 <g fill="none" fill-rule="evenodd">
14 <circle cx="12" cy="10" r="3" fill="#000"/>
15 <path fill="url(#a)" fill-rule="nonzero" d="M16.39 13.85A5.68 5.68 0 0018 10c0-3.26-2.74-6-6-6s-6 2.74-6 6c0 1.42.58 2.7 1.62 3.85a.5.5 0 00.74-.67A4.7 4.7 0 017 10c0-2.7 2.3-5 5-5s5 2.3 5 5a4.7 4.7 0 01-1.36 3.18.5.5 0 10.75.67z"/>
16 <path fill="url(#b)" fill-rule="nonzero" d="M17.57 18.3A9.99 9.99 0 0012 0a10 10 0 00-5.56 18.31 1 1 0 101.11-1.66 7.99 7.99 0 118.9 0 1 1 0 101.12 1.66z"/>
17 <path fill="#000" d="M9.33 15.98A1.64 1.64 0 0111 14h2c1.1 0 1.85.88 1.67 1.98l-1 6.04c-.1.54-.61.98-1.17.98h-1c-.55 0-1.07-.43-1.16-.98l-1.01-6.04z"/>
18 </g>
19</svg>
diff --git a/client/src/assets/images/misc/language.svg b/client/src/assets/images/misc/language.svg
index 8fd1d0ba8..204136f0b 100644
--- a/client/src/assets/images/misc/language.svg
+++ b/client/src/assets/images/misc/language.svg
@@ -1,7 +1,7 @@
1<svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.2)" viewBox="0 0 200 200"> 1<svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.2)" viewBox="0 0 200 200">
2 <defs/> 2 <defs/>
3 <path stroke="#000" stroke-width="3" d="M93 155H42a18 18 0 01-18-18V29a5 5 0 015-5h89a5 5 0 015 6L98 151a5 5 0 01-5 4zM34 34v103a8 8 0 008 8h47l22-111z"/> 3 <path stroke="currentColor" stroke-width="3" d="M93 155H42a18 18 0 01-18-18V29a5 5 0 015-5h89a5 5 0 015 6L98 151a5 5 0 01-5 4zM34 34v103a8 8 0 008 8h47l22-111z"/>
4 <path stroke="#000" stroke-width="3" d="M171 176H75a5 5 0 01-5-6l4-21a5 5 0 0110 2l-3 15h85V63a8 8 0 00-8-8h-45a5 5 0 010-10h45a18 18 0 0118 18v108a5 5 0 01-5 5zM50 92h0a5 5 0 01-5-5V63a17 17 0 0135 0v24a5 5 0 01-10 0V62a7 7 0 00-15 0v25a5 5 0 01-5 5z"/> 4 <path stroke="currentColor" stroke-width="3" d="M171 176H75a5 5 0 01-5-6l4-21a5 5 0 0110 2l-3 15h85V63a8 8 0 00-8-8h-45a5 5 0 010-10h45a18 18 0 0118 18v108a5 5 0 01-5 5zM50 92h0a5 5 0 01-5-5V63a17 17 0 0135 0v24a5 5 0 01-10 0V62a7 7 0 00-15 0v25a5 5 0 01-5 5z"/>
5 <path stroke="#000" stroke-width="3" d="M75 76H50a5 5 0 010-10h25a5 5 0 010 10zM120 155a5 5 0 01-3-9l21-21h-18a5 5 0 010-10h30a5 5 0 014 9l-30 30a5 5 0 01-4 1z"/> 5 <path stroke="currentColor" stroke-width="3" d="M75 76H50a5 5 0 010-10h25a5 5 0 010 10zM120 155a5 5 0 01-3-9l21-21h-18a5 5 0 010-10h30a5 5 0 014 9l-30 30a5 5 0 01-4 1z"/>
6 <path stroke="#000" stroke-width="3" d="M150 155a5 5 0 01-4-1l-14-15a5 5 0 017-7l15 14a5 5 0 01-4 9zM143 110h-15a5 5 0 110-10h15a5 5 0 010 10z"/> 6 <path stroke="currentColor" stroke-width="3" d="M150 155a5 5 0 01-4-1l-14-15a5 5 0 017-7l15 14a5 5 0 01-4 9zM143 110h-15a5 5 0 110-10h15a5 5 0 010 10z"/>
7</svg> 7</svg>
diff --git a/client/src/assets/images/misc/npm.svg b/client/src/assets/images/misc/npm.svg
index 1d1d82784..8a4869f12 100644
--- a/client/src/assets/images/misc/npm.svg
+++ b/client/src/assets/images/misc/npm.svg
@@ -1,5 +1,5 @@
1<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 7" style="transform: scale(1.3) translateY(1px);"> 1<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 7" style="transform: scale(1.3) translateY(1px);">
2 <path fill="#000" d="M0,0h18v6H9v1H5V6H0V0z M1,5h2V2h1v3h1V1H1V5z M6,1v5h2V5h2V1H6z M8,2h1v2H8V2z M11,1v4h2V2h1v3h1V2h1v3h1V1H11z"/> 2 <path fill="currentColor" d="M0,0h18v6H9v1H5V6H0V0z M1,5h2V2h1v3h1V1H1V5z M6,1v5h2V5h2V1H6z M8,2h1v2H8V2z M11,1v4h2V2h1v3h1V2h1v3h1V1H11z"/>
3 <polygon fill="#FFFFFF" points="1,5 3,5 3,2 4,2 4,5 5,5 5,1 1,1 "/> 3 <polygon fill="#FFFFFF" points="1,5 3,5 3,2 4,2 4,5 5,5 5,1 1,1 "/>
4 <polygon fill="#FFFFFF" d="M6,1v5h2V5h2V1H6z M9,4H8V2h1V4z"/> 4 <polygon fill="#FFFFFF" d="M6,1v5h2V5h2V1H6z M9,4H8V2h1V4z"/>
5 <polygon fill="#FFFFFF" points="11,1 11,5 13,5 13,2 14,2 14,5 15,5 15,2 16,2 16,5 17,5 17,1 "/> 5 <polygon fill="#FFFFFF" points="11,1 11,5 13,5 13,2 14,2 14,5 15,5 15,2 16,2 16,5 17,5 17,1 "/>
diff --git a/client/src/assets/images/misc/peertube-x.svg b/client/src/assets/images/misc/peertube-x.svg
index 0099e627d..30ab665e7 100644
--- a/client/src/assets/images/misc/peertube-x.svg
+++ b/client/src/assets/images/misc/peertube-x.svg
@@ -1,20 +1,17 @@
1<?xml version="1.0" encoding="utf-8"?> 1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2<!-- Generator: Adobe Illustrator 23.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 2 <style type="text/css">
3<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 3 .st0{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}
4 viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> 4 .st1{fill:currentColor;}
5<style type="text/css">
6 .st0{fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}
7 .st1{fill:#211F20;}
8</style> 5</style>
9<line class="st0" x1="17.1" y1="9.5" x2="22.1" y2="14.5"/> 6 <line class="st0" x1="17.1" y1="9.5" x2="22.1" y2="14.5" />
10<line class="st0" x1="22.1" y1="9.5" x2="17.1" y2="14.5"/> 7 <line class="st0" x1="22.1" y1="9.5" x2="17.1" y2="14.5" />
11<g>
12 <g> 8 <g>
13 <g> 9 <g>
14 <path class="st1" d="M2,2.6V12l6.9-4.3"/> 10 <g>
15 <path class="st1" d="M2,12v9.4l6.9-5.2"/> 11 <path class="st1" d="M2,2.6V12l6.9-4.3" />
16 <path class="st1" d="M8.9,7.7v8.6l6.9-4.3"/> 12 <path class="st1" d="M2,12v9.4l6.9-5.2" />
13 <path class="st1" d="M8.9,7.7v8.6l6.9-4.3" />
14 </g>
17 </g> 15 </g>
18 </g> 16 </g>
19</g>
20</svg> 17</svg>
diff --git a/client/src/assets/images/misc/playlist-add.svg b/client/src/assets/images/misc/playlist-add.svg
index 7ec77b851..4be495e83 100644
--- a/client/src/assets/images/misc/playlist-add.svg
+++ b/client/src/assets/images/misc/playlist-add.svg
@@ -1,5 +1,5 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.7 426.7"> 1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.7 426.7">
2 <defs/> 2 <defs/>
3 <path fill="#000" d="M0 64h256v42.7H0zM0 149.3h256V192H0zM0 234.7h170.7v42.7H0z"/> 3 <path fill="currentColor" d="M0 64h256v42.7H0zM0 149.3h256V192H0zM0 234.7h170.7v42.7H0z"/>
4 <path fill="#000" d="M341.3 234.7v-85.4h-42.6v85.4h-85.4v42.6h85.4v85.4h42.6v-85.4h85.4v-42.6z"/> 4 <path fill="currentColor" d="M341.3 234.7v-85.4h-42.6v85.4h-85.4v42.6h85.4v85.4h42.6v-85.4h85.4v-42.6z"/>
5</svg> 5</svg>
diff --git a/client/src/assets/images/misc/support.svg b/client/src/assets/images/misc/support.svg
index 66280e18d..be3f58c24 100644
--- a/client/src/assets/images/misc/support.svg
+++ b/client/src/assets/images/misc/support.svg
@@ -6,9 +6,9 @@
6 <g transform="translate(2.669496,27.625894)"> 6 <g transform="translate(2.669496,27.625894)">
7 <g transform="matrix(0.1,0,0,-0.1,0,511)"> 7 <g transform="matrix(0.1,0,0,-0.1,0,511)">
8 <path d="m 3744.3542,4564.3712 c -217.4,-34.2 -520.3,-200.3 -693.7,-376.2 -263.8,-263.8 -388.4,-571.6 -388.4,-952.6 0,-256.5 44,-437.2 173.4,-684 75.7,-144.1 197.9,-280.9 747.5,-842.7 1106.5,-1133.40001 1138.2,-1165.20001 1253,-1194.50001 188.1,-51.3 214.9,-29.3 1162.7,938.00001 498.3,508.1 911.1,950.2 962.4,1030.8 263.8,415.3 283.3,964.9 48.8,1409.4 -180.8,342 -581.3,620.4 -972.2,676.6 -332.2,48.9 -671.7,-36.6 -967.3,-236.9 l -156.3,-109.9 -119.7,87.9 c -158.8,117.2 -351.8,202.7 -554.5,244.3 -183.1,39.1 -295.4,41.6 -495.7,9.8 z" 8 <path d="m 3744.3542,4564.3712 c -217.4,-34.2 -520.3,-200.3 -693.7,-376.2 -263.8,-263.8 -388.4,-571.6 -388.4,-952.6 0,-256.5 44,-437.2 173.4,-684 75.7,-144.1 197.9,-280.9 747.5,-842.7 1106.5,-1133.40001 1138.2,-1165.20001 1253,-1194.50001 188.1,-51.3 214.9,-29.3 1162.7,938.00001 498.3,508.1 911.1,950.2 962.4,1030.8 263.8,415.3 283.3,964.9 48.8,1409.4 -180.8,342 -581.3,620.4 -972.2,676.6 -332.2,48.9 -671.7,-36.6 -967.3,-236.9 l -156.3,-109.9 -119.7,87.9 c -158.8,117.2 -351.8,202.7 -554.5,244.3 -183.1,39.1 -295.4,41.6 -495.7,9.8 z"
9 fill="#000"/> 9 fill="currentColor"/>
10 <path d="m 7991.4051,47.633899 c -39.1,-19.5 -473.9,-437.299999 -964.9,-925.800029 l -891.6,-891.59997 h -830.5 c -757.2,0 -837.8,4.9 -913.6,44 -207.6,112.4 -227.2,415.2 -39.1,561.8 66,53.7 83,53.7 950.2,53.7 989.3,0 1008.8,2.5 1094.3,173.49997 56.2,105 56.2,317.50003 4.9,427.50003 -83.1,175.9 4.8,168.5 -1915.1,168.5 h -1722 l -173.4,-63.5 c -95.3,-34.2 -232.1,-102.6 -305.3,-151.5 -73.3,-48.9 -442.1,-400.60003 -823.2,-779.2 l -688.80006,-693.7 664.40006,-647.3 c 366.4,-354.2 779.2,-754.8 918.4,-889.1 l 251.6,-241.8 481.2,481.2 481.2,481.2 h 1487.6 c 1294.6,0 1494.9,4.9 1565.8,39.1 58.6,26.9 339.6,368.8 1028.4,1248.2 522.8,666.89997 964.9,1243.3 982,1284.9 41.5,92.8 2.5,212.499999 -95.3,297.999999 -66,53.7 -95.3,61.1 -273.6,61.1 -132,-0.1 -224.8,-12.3 -273.6,-39.2 z" 10 <path d="m 7991.4051,47.633899 c -39.1,-19.5 -473.9,-437.299999 -964.9,-925.800029 l -891.6,-891.59997 h -830.5 c -757.2,0 -837.8,4.9 -913.6,44 -207.6,112.4 -227.2,415.2 -39.1,561.8 66,53.7 83,53.7 950.2,53.7 989.3,0 1008.8,2.5 1094.3,173.49997 56.2,105 56.2,317.50003 4.9,427.50003 -83.1,175.9 4.8,168.5 -1915.1,168.5 h -1722 l -173.4,-63.5 c -95.3,-34.2 -232.1,-102.6 -305.3,-151.5 -73.3,-48.9 -442.1,-400.60003 -823.2,-779.2 l -688.80006,-693.7 664.40006,-647.3 c 366.4,-354.2 779.2,-754.8 918.4,-889.1 l 251.6,-241.8 481.2,481.2 481.2,481.2 h 1487.6 c 1294.6,0 1494.9,4.9 1565.8,39.1 58.6,26.9 339.6,368.8 1028.4,1248.2 522.8,666.89997 964.9,1243.3 982,1284.9 41.5,92.8 2.5,212.499999 -95.3,297.999999 -66,53.7 -95.3,61.1 -273.6,61.1 -132,-0.1 -224.8,-12.3 -273.6,-39.2 z"
11 fill="#000"/> 11 fill="currentColor"/>
12 </g> 12 </g>
13 </g> 13 </g>
14</svg> 14</svg>
diff --git a/client/src/assets/images/misc/video-lang.svg b/client/src/assets/images/misc/video-lang.svg
index 5ffed18da..6bcaeb9be 100644
--- a/client/src/assets/images/misc/video-lang.svg
+++ b/client/src/assets/images/misc/video-lang.svg
@@ -1,7 +1,7 @@
1<svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.1)" viewBox="0 0 24 24"> 1<svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.1)" viewBox="0 0 24 24">
2 <defs/> 2 <defs/>
3 <g class="layer"> 3 <g class="layer">
4 <path fill="#fff" fill-rule="evenodd" stroke="#000" stroke-width="1.8" d="M20.5 6.7s-.2-1.4-.8-2c-.7-.8-1.6-.8-2-.9-2.7-.2-6.9-.2-6.9-.2h0s-4.2 0-7 .2c-.3 0-1.2 0-2 .9-.5.6-.7 2-.7 2L.9 10v1.6l.2 3.3s.2 1.4.8 2c.7.8 1.7.8 2.2.9 1.6.2 6.7.2 6.7.2s4.2 0 7-.2c.3 0 1.2 0 2-.9.5-.6.7-2 .7-2l.2-3.3V10l-.2-3.3h0z"/> 4 <path fill="#fff" fill-rule="evenodd" stroke="currentColor" stroke-width="1.8" d="M20.5 6.7s-.2-1.4-.8-2c-.7-.8-1.6-.8-2-.9-2.7-.2-6.9-.2-6.9-.2h0s-4.2 0-7 .2c-.3 0-1.2 0-2 .9-.5.6-.7 2-.7 2L.9 10v1.6l.2 3.3s.2 1.4.8 2c.7.8 1.7.8 2.2.9 1.6.2 6.7.2 6.7.2s4.2 0 7-.2c.3 0 1.2 0 2-.9.5-.6.7-2 .7-2l.2-3.3V10l-.2-3.3h0z"/>
5 <path d="M8.7 14.7a.7.7 0 01-.5-1.2l2.9-3H8.7a.7.7 0 010-1.3h4a.7.7 0 01.5 1.2l-4 4a.7.7 0 01-.5.3zM11.7 8.6h-2a.7.7 0 110-1.4h2a.7.7 0 010 1.4z"/> 5 <path d="M8.7 14.7a.7.7 0 01-.5-1.2l2.9-3H8.7a.7.7 0 010-1.3h4a.7.7 0 01.5 1.2l-4 4a.7.7 0 01-.5.3zM11.7 8.6h-2a.7.7 0 110-1.4h2a.7.7 0 010 1.4z"/>
6 </g> 6 </g>
7</svg> 7</svg>
diff --git a/client/src/assets/player/peertube-player-local-storage.ts b/client/src/assets/player/peertube-player-local-storage.ts
index 75ccfe618..cf2cfb472 100644
--- a/client/src/assets/player/peertube-player-local-storage.ts
+++ b/client/src/assets/player/peertube-player-local-storage.ts
@@ -68,6 +68,51 @@ function getStoredLastSubtitle () {
68 return getLocalStorage('last-subtitle') 68 return getLocalStorage('last-subtitle')
69} 69}
70 70
71function saveVideoWatchHistory(videoUUID: string, duration: number) {
72 return setLocalStorage(`video-watch-history`, JSON.stringify({
73 ...getStoredVideoWatchHistory(),
74 [videoUUID]: {
75 duration,
76 date: `${(new Date()).toISOString()}`
77 }
78 }))
79}
80
81function getStoredVideoWatchHistory(videoUUID?: string) {
82 let data
83
84 try {
85 data = JSON.parse(getLocalStorage('video-watch-history'))
86 } catch (error) {
87 console.error('Cannot parse video watch history from local storage: ', error)
88 }
89
90 data = data || {}
91
92 if (videoUUID) return data[videoUUID]
93
94 return data
95}
96
97function cleanupVideoWatch() {
98 const data = getStoredVideoWatchHistory()
99
100 const newData = Object.keys(data).reduce((acc, videoUUID) => {
101 const date = Date.parse(data[videoUUID].date)
102
103 const diff = Math.ceil(((new Date()).getTime() - date) / (1000 * 3600 * 24))
104
105 if (diff > 30) return acc
106
107 return {
108 ...acc,
109 [videoUUID]: data[videoUUID]
110 }
111 }, {})
112
113 setLocalStorage('video-watch-history', JSON.stringify(newData))
114}
115
71// --------------------------------------------------------------------------- 116// ---------------------------------------------------------------------------
72 117
73export { 118export {
@@ -81,7 +126,10 @@ export {
81 saveAverageBandwidth, 126 saveAverageBandwidth,
82 getAverageBandwidthInStore, 127 getAverageBandwidthInStore,
83 saveLastSubtitle, 128 saveLastSubtitle,
84 getStoredLastSubtitle 129 getStoredLastSubtitle,
130 saveVideoWatchHistory,
131 getStoredVideoWatchHistory,
132 cleanupVideoWatch
85} 133}
86 134
87// --------------------------------------------------------------------------- 135// ---------------------------------------------------------------------------
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index 2cbef48ea..119dec379 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -98,6 +98,7 @@ export interface CommonOptions extends CustomizationOptions {
98 98
99 videoViewUrl: string 99 videoViewUrl: string
100 embedUrl: string 100 embedUrl: string
101 embedTitle: string
101 102
102 isLive: boolean 103 isLive: boolean
103 104
@@ -105,6 +106,8 @@ export interface CommonOptions extends CustomizationOptions {
105 106
106 videoCaptions: VideoJSCaption[] 107 videoCaptions: VideoJSCaption[]
107 108
109 videoUUID: string
110
108 userWatching?: UserWatching 111 userWatching?: UserWatching
109 112
110 serverUrl: string 113 serverUrl: string
@@ -165,7 +168,7 @@ export class PeertubePlayerManager {
165 PeertubePlayerManager.alreadyPlayed = true 168 PeertubePlayerManager.alreadyPlayed = true
166 }) 169 })
167 170
168 self.addContextMenu(mode, player, options.common.embedUrl) 171 self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle)
169 172
170 player.bezels() 173 player.bezels()
171 174
@@ -203,7 +206,7 @@ export class PeertubePlayerManager {
203 videojs(newVideoElement, videojsOptions, function (this: videojs.Player) { 206 videojs(newVideoElement, videojsOptions, function (this: videojs.Player) {
204 const player = this 207 const player = this
205 208
206 self.addContextMenu(mode, player, options.common.embedUrl) 209 self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle)
207 210
208 PeertubePlayerManager.onPlayerChange(player) 211 PeertubePlayerManager.onPlayerChange(player)
209 }) 212 })
@@ -230,7 +233,8 @@ export class PeertubePlayerManager {
230 subtitle: commonOptions.subtitle, 233 subtitle: commonOptions.subtitle,
231 videoCaptions: commonOptions.videoCaptions, 234 videoCaptions: commonOptions.videoCaptions,
232 stopTime: commonOptions.stopTime, 235 stopTime: commonOptions.stopTime,
233 isLive: commonOptions.isLive 236 isLive: commonOptions.isLive,
237 videoUUID: commonOptions.videoUUID
234 } 238 }
235 } 239 }
236 240
@@ -271,7 +275,7 @@ export class PeertubePlayerManager {
271 275
272 poster: commonOptions.poster, 276 poster: commonOptions.poster,
273 inactivityTimeout: commonOptions.inactivityTimeout, 277 inactivityTimeout: commonOptions.inactivityTimeout,
274 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], 278 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
275 279
276 plugins, 280 plugins,
277 281
@@ -492,7 +496,7 @@ export class PeertubePlayerManager {
492 return children 496 return children
493 } 497 }
494 498
495 private static addContextMenu (mode: PlayerMode, player: videojs.Player, videoEmbedUrl: string) { 499 private static addContextMenu (mode: PlayerMode, player: videojs.Player, videoEmbedUrl: string, videoEmbedTitle: string) {
496 const content = [ 500 const content = [
497 { 501 {
498 label: player.localize('Copy the video URL'), 502 label: player.localize('Copy the video URL'),
@@ -509,7 +513,7 @@ export class PeertubePlayerManager {
509 { 513 {
510 label: player.localize('Copy embed code'), 514 label: player.localize('Copy embed code'),
511 listener: () => { 515 listener: () => {
512 copyToClipboard(buildVideoOrPlaylistEmbed(videoEmbedUrl)) 516 copyToClipboard(buildVideoOrPlaylistEmbed(videoEmbedUrl, videoEmbedTitle))
513 } 517 }
514 } 518 }
515 ] 519 ]
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
index 75a6e662e..07c7e33f6 100644
--- a/client/src/assets/player/peertube-plugin.ts
+++ b/client/src/assets/player/peertube-plugin.ts
@@ -13,6 +13,7 @@ import {
13 getStoredVolume, 13 getStoredVolume,
14 saveLastSubtitle, 14 saveLastSubtitle,
15 saveMuteInStore, 15 saveMuteInStore,
16 saveVideoWatchHistory,
16 saveVolumeInStore 17 saveVolumeInStore
17} from './peertube-player-local-storage' 18} from './peertube-player-local-storage'
18 19
@@ -120,7 +121,7 @@ class PeerTubePlugin extends Plugin {
120 this.initializePlayer() 121 this.initializePlayer()
121 this.runViewAdd() 122 this.runViewAdd()
122 123
123 if (options.userWatching) this.runUserWatchVideo(options.userWatching) 124 this.runUserWatchVideo(options.userWatching, options.videoUUID)
124 }) 125 })
125 } 126 }
126 127
@@ -178,7 +179,7 @@ class PeerTubePlugin extends Plugin {
178 }, 1000) 179 }, 1000)
179 } 180 }
180 181
181 private runUserWatchVideo (options: UserWatching) { 182 private runUserWatchVideo (options: UserWatching, videoUUID: string) {
182 let lastCurrentTime = 0 183 let lastCurrentTime = 0
183 184
184 this.userWatchingVideoInterval = setInterval(() => { 185 this.userWatchingVideoInterval = setInterval(() => {
@@ -187,8 +188,12 @@ class PeerTubePlugin extends Plugin {
187 if (currentTime - lastCurrentTime >= 1) { 188 if (currentTime - lastCurrentTime >= 1) {
188 lastCurrentTime = currentTime 189 lastCurrentTime = currentTime
189 190
190 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) 191 if (options) {
191 .catch(err => console.error('Cannot notify user is watching.', err)) 192 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
193 .catch(err => console.error('Cannot notify user is watching.', err))
194 } else {
195 saveVideoWatchHistory(videoUUID, currentTime)
196 }
192 } 197 }
193 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) 198 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
194 } 199 }
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index e5259092c..4a6c80247 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -108,6 +108,8 @@ type PeerTubePluginOptions = {
108 stopTime: number | string 108 stopTime: number | string
109 109
110 isLive: boolean 110 isLive: boolean
111
112 videoUUID: string
111} 113}
112 114
113type PlaylistPluginOptions = { 115type PlaylistPluginOptions = {
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts
index 6767459ce..d7451fa1d 100644
--- a/client/src/assets/player/utils.ts
+++ b/client/src/assets/player/utils.ts
@@ -1,4 +1,5 @@
1import { VideoFile } from '@shared/models' 1import { VideoFile } from '@shared/models'
2import { escapeHTML } from '@shared/core-utils/renderer'
2 3
3function toTitleCase (str: string) { 4function toTitleCase (str: string) {
4 return str.charAt(0).toUpperCase() + str.slice(1) 5 return str.charAt(0).toUpperCase() + str.slice(1)
@@ -170,9 +171,11 @@ function secondsToTime (seconds: number, full = false, symbol?: string) {
170 return time 171 return time
171} 172}
172 173
173function buildVideoOrPlaylistEmbed (embedUrl: string) { 174function buildVideoOrPlaylistEmbed (embedUrl: string, embedTitle: string) {
175 const title = escapeHTML(embedTitle)
174 return '<iframe width="560" height="315" ' + 176 return '<iframe width="560" height="315" ' +
175 'sandbox="allow-same-origin allow-scripts allow-popups" ' + 177 'sandbox="allow-same-origin allow-scripts allow-popups" ' +
178 'title="' + title + '" ' +
176 'src="' + embedUrl + '" ' + 179 'src="' + embedUrl + '" ' +
177 'frameborder="0" allowfullscreen>' + 180 'frameborder="0" allowfullscreen>' +
178 '</iframe>' 181 '</iframe>'
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index a0009eecc..c01938147 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -15,6 +15,8 @@ $assets-path: '../../assets/';
15@import './primeng-custom'; 15@import './primeng-custom';
16@import './ng-select.scss'; 16@import './ng-select.scss';
17 17
18@import './classes.scss';
19
18[hidden] { 20[hidden] {
19 display: none !important; 21 display: none !important;
20} 22}
@@ -36,7 +38,9 @@ body {
36 38
37 --menuBackgroundColor: #{$menu-background}; 39 --menuBackgroundColor: #{$menu-background};
38 --menuForegroundColor: #{$menu-color}; 40 --menuForegroundColor: #{$menu-color};
39 --submenuColor: #{$sub-menu-color}; 41
42 --submenuBackgroundColor: #{$sub-menu-background-color};
43 --channelBackgroundColor: #{$channel-background-color};
40 44
41 --inputForegroundColor: #{$input-foreground-color}; 45 --inputForegroundColor: #{$input-foreground-color};
42 --inputBackgroundColor: #{$input-background-color}; 46 --inputBackgroundColor: #{$input-background-color};
@@ -53,7 +57,9 @@ body {
53 57
54 --activatedActionButtonColor: #{$activated-action-button-color}; 58 --activatedActionButtonColor: #{$activated-action-button-color};
55 59
56 --expanded-horizontal-margin-content: #{$expanded-horizontal-margins}; 60 --horizontalMarginContent: #{$not-expanded-horizontal-margins};
61 --videosHorizontalMarginContent: 6vw;
62 --mainColWidth: calc(100vw - #{$menu-width});
57 63
58 font-family: $main-fonts; 64 font-family: $main-fonts;
59 font-weight: $font-regular; 65 font-weight: $font-regular;
@@ -146,24 +152,26 @@ my-input-toggle-hidden ::ng-deep input {
146 outline: none; 152 outline: none;
147 153
148 .margin-content { 154 .margin-content {
149 margin-left: $not-expanded-horizontal-margins; 155 margin-left: pvar(--horizontalMarginContent);
150 margin-right: $not-expanded-horizontal-margins; 156 margin-right: pvar(--horizontalMarginContent);
151 flex-grow: 1; 157 flex-grow: 1;
152 } 158 }
153 159
154 .sub-menu { 160 .sub-menu {
155 background-color: pvar(--submenuColor); 161 background-color: pvar(--submenuBackgroundColor);
156 width: 100%; 162 width: 100%;
157 display: flex; 163 display: flex;
158 align-items: center; 164 align-items: center;
159 padding-left: $not-expanded-horizontal-margins; 165 padding-left: pvar(--horizontalMarginContent);
160 padding-right: $not-expanded-horizontal-margins; 166 padding-right: pvar(--horizontalMarginContent);
161 height: $sub-menu-height; 167 height: $sub-menu-height;
162 margin-bottom: $sub-menu-margin-bottom; 168 margin-bottom: $sub-menu-margin-bottom;
169 overflow-x: auto;
163 170
164 &.sub-menu-fixed { 171 &.sub-menu-fixed {
165 position: fixed; 172 position: fixed;
166 z-index: #{z('sub-menu') - 1}; 173 z-index: #{z('sub-menu') - 1};
174 max-width: pvar(--mainColWidth);
167 } 175 }
168 } 176 }
169 177
@@ -174,18 +182,11 @@ my-input-toggle-hidden ::ng-deep input {
174 182
175 // Override some properties if the main content is expanded (no menu on the left) 183 // Override some properties if the main content is expanded (no menu on the left)
176 &.expanded { 184 &.expanded {
185 --horizontalMarginContent: #{$expanded-horizontal-margins};
186 --mainColWidth: 100vw;
187
177 margin-left: 0; 188 margin-left: 0;
178 width: 100%; 189 width: 100%;
179
180 .margin-content {
181 margin-left: var(--expanded-horizontal-margin-content);
182 margin-right: var(--expanded-horizontal-margin-content);
183 }
184
185 .sub-menu {
186 padding-left: var(--expanded-horizontal-margin-content);
187 padding-right: var(--expanded-horizontal-margin-content);
188 }
189 } 190 }
190 191
191 &.lock-scroll .main-row > router-outlet + * { 192 &.lock-scroll .main-row > router-outlet + * {
@@ -263,7 +264,7 @@ my-input-toggle-hidden ::ng-deep input {
263 opacity: 0.6; 264 opacity: 0.6;
264 265
265 &.active { 266 &.active {
266 background-color: pvar(--submenuColor); 267 background-color: pvar(--submenuBackgroundColor);
267 } 268 }
268 269
269 &.active, &:hover, &:active, &:focus { 270 &.active, &:hover, &:active, &:focus {
@@ -277,11 +278,6 @@ my-input-toggle-hidden ::ng-deep input {
277 font-weight: bold; 278 font-weight: bold;
278} 279}
279 280
280@keyframes spin {
281 from { transform: scale(1) rotate(0deg);}
282 to { transform: scale(1) rotate(360deg);}
283}
284
285// In tables, don't have a hover different background 281// In tables, don't have a hover different background
286table { 282table {
287 .action-button-edit, .action-button-delete { 283 .action-button-edit, .action-button-delete {
@@ -338,29 +334,34 @@ ngx-loading-bar {
338 334
339@media screen and (max-width: #{breakpoint(xxl)}) { 335@media screen and (max-width: #{breakpoint(xxl)}) {
340 .main-col { 336 .main-col {
337 & {
338 --horizontalMarginContent: #{$not-expanded-horizontal-margins / 2};
339 }
340
341 &.expanded { 341 &.expanded {
342 .margin-content { 342 --horizontalMarginContent: #{$expanded-horizontal-margins / 2};
343 --expanded-horizontal-margin-content: #{$expanded-horizontal-margins/2};
344 }
345 } 343 }
344
345 --videosHorizontalMarginContent: 30px;
346 } 346 }
347} 347}
348 348
349@media screen and (max-width: #{breakpoint(lg)}) { 349@media screen and (max-width: #{breakpoint(lg)}) {
350 /* the following applies from 500px to 900px and is partially overriden from 500px to 800px by changes below to $small-view */
351 .main-col { 350 .main-col {
352 &, &.expanded { 351 --videosHorizontalMarginContent: #{pvar(--horizontalMarginContent)};
353 .margin-content { 352 }
354 --expanded-horizontal-margin-content: #{$expanded-horizontal-margins/3}; 353
355 } 354 /* the following applies from 500px to 900px and is partially overriden from 500px to 800px by changes below to $small-view */
355 .main-col,
356 .main-col.expanded {
357 --horizontalMarginContent: #{$expanded-horizontal-margins / 3};
356 358
357 .sub-menu { 359 .sub-menu {
358 padding-left: 50px; 360 padding-left: 50px;
359 padding-right: 50px; 361 padding-right: 50px;
360 362
361 .title-page { 363 .title-page {
362 font-size: 17px; 364 font-size: 17px;
363 }
364 } 365 }
365 } 366 }
366 } 367 }
@@ -373,98 +374,46 @@ ngx-loading-bar {
373} 374}
374 375
375@media screen and (max-width: $small-view) { 376@media screen and (max-width: $small-view) {
376 .main-col { 377 .main-col,
377 margin-left: 0; 378 .main-col.expanded {
378 379 --horizontalMarginContent: 15px;
379 &, &.expanded {
380 .margin-content {
381 --expanded-horizontal-margin-content: 15px;
382 }
383
384 .sub-menu {
385 width: 100vw;
386 padding-left: 15px;
387 padding-right: 15px;
388 margin-bottom: $sub-menu-margin-bottom-small-view;
389 overflow-x: auto;
390 }
391
392 // Use an appropriate offset top when sub-menu fixed
393 .margin-content.offset-content {
394 padding-top: $sub-menu-height + $sub-menu-margin-bottom-small-view;
395 }
396
397 .admin-sub-header {
398 @include admin-sub-header-responsive(15px*2);
399 }
400 380
401 my-markdown-textarea { 381 margin-left: 0;
402 .root {
403 max-width: 100% !important;
404 }
405 }
406
407 input[type=text],
408 input[type=password],
409 input[type=email],
410 textarea,
411 .peertube-select-container {
412 flex-grow: 1;
413 }
414 382
415 .caption input[type=text] { 383 .sub-menu {
416 width: unset !important; 384 width: 100vw;
417 flex-grow: 1; 385 padding-left: 15px;
418 } 386 padding-right: 15px;
387 margin-bottom: $sub-menu-margin-bottom-small-view;
388 overflow-x: auto;
419 } 389 }
420 }
421}
422 390
423// overflow-databale responsive rules 391 // Use an appropriate offset top when sub-menu fixed
424@media screen and (min-width: #{breakpoint(lg)}) { 392 .margin-content.offset-content {
425 .main-col { 393 padding-top: $sub-menu-height + $sub-menu-margin-bottom-small-view;
426 &.expanded {
427 @include overflow-datatable(breakpoint(lg), $expanded-horizontal-margins/2, $mobile-paginator: false);
428 } 394 }
429 395
430 &:not(.expanded) { 396 .admin-sub-header {
431 @include overflow-datatable(breakpoint(lg), $not-expanded-horizontal-margins + $menu-width/2, $mobile-paginator: false); 397 @include admin-sub-header-responsive;
432 } 398 }
433 }
434}
435 399
436@media screen and (max-width: #{breakpoint(lg)}) { 400 my-markdown-textarea {
437 .main-col { 401 .root {
438 &.expanded { 402 max-width: 100% !important;
439 @include overflow-datatable(breakpoint(lg), $expanded-horizontal-margins/3); 403 }
440 } 404 }
441 405
442 &:not(.expanded) { 406 input[type=text],
443 @include overflow-datatable(breakpoint(lg), $expanded-horizontal-margins/3 + $menu-width/2); 407 input[type=password],
444 } 408 input[type=email],
445 } 409 textarea,
446} 410 .peertube-select-container {
447 411 flex-grow: 1;
448@media screen and (max-width: $small-view) {
449 .main-col {
450 &:not(.expanded),
451 &.expanded {
452 @include overflow-datatable(breakpoint(lg), 15px);
453 } 412 }
454 }
455}
456 413
457@media screen and (min-width: $small-view) and (max-width: #{$small-view + $menu-width}) { 414 .caption input[type=text] {
458 .main-col { 415 width: unset !important;
459 &:not(.expanded) { 416 flex-grow: 1;
460 .admin-sub-header {
461 @include admin-sub-header-responsive($expanded-horizontal-margins/3 + $menu-width/2);
462 }
463
464 .sub-menu {
465 overflow-x: auto;
466 width: calc(100vw - #{$menu-width});
467 }
468 } 417 }
469 } 418 }
470} 419}
diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss
index 7047f6e03..75dc91d7a 100644
--- a/client/src/sass/bootstrap.scss
+++ b/client/src/sass/bootstrap.scss
@@ -9,6 +9,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
9 animation: spin .7s infinite linear; 9 animation: spin .7s infinite linear;
10} 10}
11 11
12.glyphicon-duplicate {
13 font-size: 70%;
14}
15
12.flex-auto { 16.flex-auto {
13 flex: auto; 17 flex: auto;
14} 18}
diff --git a/client/src/sass/classes.scss b/client/src/sass/classes.scss
new file mode 100644
index 000000000..af8e39573
--- /dev/null
+++ b/client/src/sass/classes.scss
@@ -0,0 +1,22 @@
1@import '_variables';
2@import '_mixins';
3
4.peertube-button {
5 @include peertube-button;
6}
7
8.peertube-button-link {
9 @include peertube-button-link;
10}
11
12.orange-button {
13 @include orange-button;
14}
15
16.orange-button-inverted {
17 @include orange-button-inverted;
18}
19
20.grey-button {
21 @include grey-button;
22}
diff --git a/client/src/sass/include/_actor.scss b/client/src/sass/include/_actor.scss
new file mode 100644
index 000000000..8d82a042c
--- /dev/null
+++ b/client/src/sass/include/_actor.scss
@@ -0,0 +1,92 @@
1@import '_variables';
2
3@mixin section-label-responsive {
4 color: pvar(--mainColor);
5 font-size: 12px;
6 margin-bottom: 15px;
7 font-weight: $font-bold;
8 letter-spacing: 2.5px;
9
10 @media screen and (max-width: $mobile-view) {
11 font-size: 10px;
12 letter-spacing: 2.1px;
13 margin-bottom: 5px;
14 }
15}
16
17@mixin show-more-description {
18 color: pvar(--mainColor);
19 cursor: pointer;
20 margin: 10px auto 45px auto;
21}
22
23@mixin avatar-row-responsive ($img-margin, $grey-font-size) {
24 display: flex;
25 grid-column: 1;
26 margin-bottom: 30px;
27
28 .channel-avatar {
29 @include channel-avatar(120px);
30 }
31
32 .account-avatar {
33 @include avatar(120px);
34 }
35
36 > div {
37 margin-left: $img-margin;
38 min-width: 1px;
39 }
40
41 .actor-info {
42 display: flex;
43
44 > div:first-child {
45 flex-grow: 1;
46 min-width: 1px;
47 }
48 }
49
50 .actor-display-name {
51 display: flex;
52 flex-wrap: wrap;
53 }
54
55 h1 {
56 font-size: 28px;
57 font-weight: $font-bold;
58 margin: 0;
59 }
60
61 .actor-handle {
62 @include ellipsis;
63 }
64
65 .actor-handle,
66 .actor-counters {
67 color: pvar(--greyForegroundColor);
68 font-size: $grey-font-size;
69 }
70
71 .actor-counters > *:not(:last-child)::after {
72 content: '•';
73 margin: 0 10px;
74 color: pvar(--mainColor);
75 }
76
77 @media screen and (max-width: $mobile-view) {
78 margin-bottom: 15px;
79
80 h1 {
81 font-size: 22px;
82 }
83
84 .channel-avatar {
85 @include channel-avatar(80px);
86 }
87
88 .account-avatar {
89 @include avatar(120px);
90 }
91 }
92}
diff --git a/client/src/sass/include/_miniature.scss b/client/src/sass/include/_miniature.scss
index 134b307b1..3b86f29b4 100644
--- a/client/src/sass/include/_miniature.scss
+++ b/client/src/sass/include/_miniature.scss
@@ -4,11 +4,11 @@
4@mixin miniature-name { 4@mixin miniature-name {
5 @include ellipsis-multiline(1.1em, 2); 5 @include ellipsis-multiline(1.1em, 2);
6 6
7 word-break: break-all;
8 word-wrap: break-word;
7 transition: color 0.2s; 9 transition: color 0.2s;
8 font-weight: $font-semibold; 10 font-weight: $font-semibold;
9 color: pvar(--mainForegroundColor); 11 color: pvar(--mainForegroundColor);
10 margin-top: 10px;
11 margin-bottom: 5px;
12 12
13 &:hover { 13 &:hover {
14 text-decoration: none; 14 text-decoration: none;
@@ -20,20 +20,20 @@
20 } 20 }
21} 21}
22 22
23$play-overlay-transition: 0.2s ease;
24$play-overlay-height: 26px;
25$play-overlay-width: 18px;
26
27@mixin miniature-thumbnail { 23@mixin miniature-thumbnail {
28 @include disable-outline; 24 @include disable-outline;
29 25
26 $play-overlay-transition: 0.2s ease;
27 $play-overlay-height: 26px;
28 $play-overlay-width: 18px;
29
30 display: flex; 30 display: flex;
31 flex-direction: column; 31 flex-direction: column;
32 position: relative; 32 position: relative;
33 border-radius: 3px; 33 border-radius: 3px;
34 width: 100%;
35 height: 100%;
34 overflow: hidden; 36 overflow: hidden;
35 width: $video-thumbnail-width;
36 height: $video-thumbnail-height;
37 background-color: #ececec; 37 background-color: #ececec;
38 transition: filter $play-overlay-transition; 38 transition: filter $play-overlay-transition;
39 39
@@ -97,154 +97,64 @@ $play-overlay-width: 18px;
97 color: #fff; 97 color: #fff;
98} 98}
99 99
100@mixin miniature-rows { 100// Use margin by default, or padding if $margin is false
101 &:first-child { 101@mixin grid-videos-miniature-margins ($margin: true, $min-margin: 0) {
102 padding-top: 30px; 102 --gridVideosMiniatureMargins: #{pvar(--videosHorizontalMarginContent)};
103 103
104 .section-title { 104 @if $margin {
105 border-top: none !important; 105 margin-left: var(--gridVideosMiniatureMargins) !important;
106 } 106 margin-right: var(--gridVideosMiniatureMargins) !important;
107 } 107 } @else {
108 108 padding-left: var(--gridVideosMiniatureMargins) !important;
109 .section-title { 109 padding-right: var(--gridVideosMiniatureMargins) !important;
110 font-size: 24px;
111 font-weight: $font-semibold;
112 padding-top: 15px;
113 margin-bottom: 15px;
114 display: flex;
115 justify-content: space-between;
116
117 &:not(h2) {
118 border-top: 1px solid $separator-border-color;
119 }
120
121 a {
122 &:hover, &:focus:not(.focus-visible), &:active {
123 text-decoration: none;
124 outline: none;
125 }
126
127 color: pvar(--mainForegroundColor);
128 }
129 }
130
131 &.channel {
132 .section-title {
133 a {
134 display: flex;
135 width: fit-content;
136 align-items: center;
137
138 img {
139 @include avatar(28px);
140
141 margin-right: 8px;
142 }
143 }
144
145 .followers {
146 color: pvar(--greyForegroundColor);
147 font-weight: normal;
148 font-size: 14px;
149 margin-left: 10px;
150 position: relative;
151 top: 2px;
152 }
153 }
154 }
155
156 .show-more {
157 position: relative;
158 top: -5px;
159 display: inline-block;
160 font-size: 16px;
161 text-transform: uppercase;
162 color: pvar(--greyForegroundColor);
163 margin-bottom: 10px;
164 font-weight: $font-semibold;
165 text-decoration: none;
166 } 110 }
167 111
168 @media screen and (max-width: $mobile-view) { 112 @media screen and (max-width: $mobile-view) {
169 max-height: initial; 113 --gridVideosMiniatureMargins: #{$min-margin};
170 overflow: initial;
171
172 .section-title {
173 font-size: 17px;
174 margin-left: 10px;
175 }
176 }
177}
178 114
179@mixin fluid-videos-miniature-layout {
180 margin-left: $not-expanded-horizontal-margins !important;
181 margin-right: $not-expanded-horizontal-margins !important;
182
183 @media screen and (max-width: $mobile-view) {
184 width: auto; 115 width: auto;
185 margin: 0 !important;
186
187 .videos {
188 text-align: center;
189
190 ::ng-deep .video-miniature {
191 padding-right: 0;
192 height: auto;
193 width: 100%;
194 margin-bottom: 25px;
195
196 .video-miniature-information {
197 width: 100% !important;
198 text-align: left;
199
200 span {
201 width: 100%;
202 }
203 }
204
205 .video-thumbnail {
206 border-radius: 0;
207 }
208 }
209 }
210 } 116 }
117}
211 118
212 @media screen and (min-width: #{breakpoint(fhd)}) { 119@mixin grid-videos-miniature-layout {
213 margin-left: 6vw !important; 120 @include grid-videos-miniature-margins;
214 margin-right: 6vw !important;
215 }
216 121
217 @media screen and (min-width: $mobile-view) { 122 @media screen and (min-width: $mobile-view) {
218 123 .videos,
219 .videos { 124 .playlists {
220 --miniature-min-width: #{$video-thumbnail-width - 15px}; 125 --miniatureMinWidth: #{$video-thumbnail-width - 25px};
221 --miniature-max-width: #{$video-thumbnail-width}; 126 --miniatureMaxWidth: #{$video-thumbnail-width};
222 127
223 display: grid; 128 display: grid;
224 column-gap: 5px; 129 column-gap: 30px;
225 grid-template-columns: repeat( 130 grid-template-columns: repeat(
226 auto-fill, 131 auto-fill,
227 minmax( 132 minmax(
228 var(--miniature-min-width), 133 var(--miniatureMinWidth),
229 1fr 134 1fr
230 ) 135 )
231 ); 136 );
232 137
233 @media screen and (min-width: #{breakpoint(fhd)}) { 138 .video-wrapper,
234 column-gap: 1%; 139 .playlist-wrapper {
235 --miniature-min-width: #{$video-thumbnail-width};
236 }
237
238 .video-wrapper {
239 margin: 0 auto; 140 margin: 0 auto;
240 width: 100%; 141 width: 100%;
241 142
242 my-video-miniature { 143 my-video-miniature,
144 my-video-playlist-miniature {
243 display: block; 145 display: block;
244 min-width: var(--miniature-min-width); 146 min-width: var(--miniatureMinWidth);
245 max-width: var(--miniature-max-width); 147 max-width: var(--miniatureMaxWidth);
246 } 148 }
247 } 149 }
150
151 @media screen and (min-width: #{breakpoint(xm)}) {
152 column-gap: 15px;
153 }
154
155 @media screen and (min-width: #{breakpoint(fhd)}) {
156 column-gap: 2%;
157 }
248 } 158 }
249 } 159 }
250} 160}
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index ca11488cb..bf844ac5d 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -23,17 +23,28 @@
23 display: block; 23 display: block;
24 /* Fallback for non-webkit */ 24 /* Fallback for non-webkit */
25 display: -webkit-box; 25 display: -webkit-box;
26 max-height: $font-size * $number-of-lines; 26 -webkit-line-clamp: $number-of-lines;
27 /* Fallback for non-webkit */ 27 /* Fallback for non-webkit */
28 font-size: $font-size; 28 font-size: $font-size;
29 line-height: $font-size; 29 line-height: $font-size;
30 overflow: hidden; 30 overflow: hidden;
31 text-overflow: ellipsis; 31 text-overflow: ellipsis;
32 max-height: $font-size * $number-of-lines;
32} 33}
33 34
34@mixin prefix($property, $parameters...) { 35@mixin fade-text ($fade-after, $background-color) {
35 @each $prefix in -webkit-, -moz-, -ms-, -o-, "" { 36 position: relative;
36 #{$prefix}#{$property}: $parameters; 37 overflow: hidden;
38
39 &:after {
40 content: '';
41 pointer-events: none;
42 width: 100%;
43 height: 100%;
44 position: absolute;
45 left: 0;
46 top: 0;
47 background: linear-gradient(transparent $fade-after, $background-color);
37 } 48 }
38} 49}
39 50
@@ -41,9 +52,6 @@
41 word-break: break-word; 52 word-break: break-word;
42 word-wrap: break-word; 53 word-wrap: break-word;
43 overflow-wrap: break-word; 54 overflow-wrap: break-word;
44 -webkit-hyphens: auto;
45 -ms-hyphens: auto;
46 -moz-hyphens: auto;
47 hyphens: auto; 55 hyphens: auto;
48} 56}
49 57
@@ -52,28 +60,6 @@
52 ::ng-deep .material { 60 ::ng-deep .material {
53 color: $color; 61 color: $color;
54 } 62 }
55
56 ::ng-deep svg {
57 path[fill="#000"],
58 g[fill="#000"],
59 rect[fill="#000"],
60 circle[fill="#000"],
61 polygon[fill="#000"] {
62 fill: $color;
63 }
64
65 path[stroke="#000"],
66 g[stroke="#000"],
67 rect[stroke="#000"],
68 circle[stroke="#000"],
69 polygon[stroke="#000"] {
70 stroke: $color;
71 }
72
73 stop[stop-color="#000"] {
74 stop-color: $color;
75 }
76 }
77} 63}
78 64
79@mixin fill-svg-color ($color) { 65@mixin fill-svg-color ($color) {
@@ -163,6 +149,33 @@
163 } 149 }
164} 150}
165 151
152@mixin orange-button-inverted {
153 @include button-focus(pvar(--mainColorLightest));
154
155 border: 2px solid pvar(--mainColor);
156 font-weight: $font-semibold;
157
158 &, &:active, &:focus {
159 color: pvar(--mainColor);
160 background-color: pvar(--mainBackgroundColor);
161 }
162
163 &:hover {
164 color: pvar(--mainColor);
165 background-color: pvar(--mainColorLightest);
166 }
167
168 &[disabled], &.disabled {
169 cursor: default;
170 color: pvar(--mainColor);
171 background-color: #C6C6C6;
172 }
173
174 my-global-icon {
175 @include apply-svg-color(pvar(--mainColor))
176 }
177}
178
166@mixin tertiary-button { 179@mixin tertiary-button {
167 @include button-focus($grey-button-outline-color); 180 @include button-focus($grey-button-outline-color);
168 181
@@ -534,6 +547,14 @@
534 min-height: $size; 547 min-height: $size;
535} 548}
536 549
550@mixin channel-avatar ($size) {
551 width: $size;
552 height: $size;
553 min-width: $size;
554 min-height: $size;
555 border-radius: 5px;
556}
557
537@mixin chevron ($size, $border-width) { 558@mixin chevron ($size, $border-width) {
538 border-style: solid; 559 border-style: solid;
539 border-width: $border-width $border-width 0 0; 560 border-width: $border-width $border-width 0 0;
@@ -593,103 +614,29 @@
593 } 614 }
594} 615}
595 616
596@mixin sub-menu-with-actor {
597 position: initial;
598 z-index: unset;
599 height: max-content;
600 display: flex;
601 flex-direction: column;
602 align-items: flex-start;
603
604 .actor {
605 display: flex;
606 margin-top: 20px;
607 margin-bottom: 20px;
608
609 img {
610 @include avatar(80px);
611
612 margin-right: 20px;
613 }
614
615 .actor-info {
616 display: flex;
617 flex-direction: column;
618 justify-content: center;
619
620 .actor-names {
621 display: flex;
622 align-items: center;
623 flex-wrap: wrap;
624
625 .actor-display-name {
626 font-size: 23px;
627 font-weight: $font-bold;
628 margin-right: 7px;
629 }
630
631 .actor-name {
632 position: relative;
633 top: 3px;
634 font-size: 14px;
635 color: $grey-actor-name;
636 }
637 }
638
639 .actor-lower {
640 grid-area: lower;
641 }
642
643 .actor-followers {
644 font-size: 15px;
645 }
646
647 .actor-owner {
648 @include actor-owner;
649 }
650 }
651 }
652
653 .links {
654 margin-top: 0;
655 margin-bottom: 15px;
656
657 a {
658 margin-top: 0;
659 margin-bottom: 0;
660 text-transform: uppercase;
661 font-weight: 600;
662 font-size: 110%;
663
664 @media screen and (max-width: $mobile-view) {
665 font-size: 130%;
666 }
667 }
668
669 list-overflow {
670 display: inline-block;
671 width: max-content;
672 }
673 }
674}
675
676@mixin create-button { 617@mixin create-button {
677 @include peertube-button-link; 618 @include peertube-button-link;
678 @include orange-button; 619 @include orange-button;
679 @include button-with-icon(20px, 5px, -1px); 620 @include button-with-icon(20px, 5px, -1px);
680} 621}
681 622
682@mixin row-blocks { 623@mixin row-blocks ($column-responsive: true) {
683 display: flex; 624 display: flex;
684 min-height: 130px; 625 min-height: 130px;
685 padding-bottom: 20px; 626 padding-bottom: 20px;
686 margin-bottom: 20px; 627 margin-bottom: 20px;
687 border-bottom: 1px solid #C6C6C6; 628 border-bottom: 1px solid #C6C6C6;
688 629
689 @media screen and (max-width: 800px) { 630 @media screen and (max-width: $small-view) {
690 flex-direction: column; 631 @if $column-responsive {
691 height: auto; 632 flex-direction: column;
692 align-items: center; 633 height: auto;
634 align-items: center;
635 } @else {
636 min-height: initial;
637 padding-bottom: 10px;
638 margin-bottom: 10px;
639 }
693 } 640 }
694} 641}
695 642
@@ -756,7 +703,7 @@
756 padding: 0.75rem 1rem; 703 padding: 0.75rem 1rem;
757 margin-bottom: 1rem; 704 margin-bottom: 1rem;
758 list-style: none; 705 list-style: none;
759 background-color: pvar(--submenuColor); 706 background-color: pvar(--submenuBackgroundColor);
760 border-radius: 0.25rem; 707 border-radius: 0.25rem;
761 708
762 .breadcrumb-item { 709 .breadcrumb-item {
@@ -811,7 +758,7 @@
811 & > a, 758 & > a,
812 & > div { 759 & > div {
813 padding: 20px; 760 padding: 20px;
814 background: pvar(--submenuColor); 761 background: pvar(--submenuBackgroundColor);
815 border-radius: 4px; 762 border-radius: 4px;
816 box-sizing: border-box; 763 box-sizing: border-box;
817 height: 100%; 764 height: 100%;
@@ -833,7 +780,7 @@
833 } 780 }
834} 781}
835 782
836@mixin divider($color: pvar(--submenuColor), $background: pvar(--mainBackgroundColor)) { 783@mixin divider($color: pvar(--submenuBackgroundColor), $background: pvar(--mainBackgroundColor)) {
837 width: 95%; 784 width: 95%;
838 border-top: .05rem solid $color; 785 border-top: .05rem solid $color;
839 height: .05rem; 786 height: .05rem;
@@ -916,7 +863,7 @@
916 } 863 }
917} 864}
918 865
919@mixin admin-sub-header-responsive ($horizontal-margins) { 866@mixin admin-sub-header-responsive {
920 flex-direction: column; 867 flex-direction: column;
921 868
922 .form-sub-title { 869 .form-sub-title {
@@ -931,7 +878,7 @@
931 white-space: nowrap; 878 white-space: nowrap;
932 height: 50px; 879 height: 50px;
933 padding: 10px 0; 880 padding: 10px 0;
934 width: calc(100vw - #{$horizontal-margins*2}); 881 width: 100%;
935 882
936 a { 883 a {
937 margin-left: 5px; 884 margin-left: 5px;
@@ -939,14 +886,16 @@
939 } 886 }
940} 887}
941 888
942// applies 16:9 ratio to a child element (using $selector) only using 889// applies ratio (default to 16:9) to a child element (using $selector) only using
943// an immediate's parent size. This allows 16:9 ratio without explicit 890// an immediate's parent size. This allows to set a ratio without explicit
944// dimensions, as width/height cannot be computed from each other. 891// dimensions, as width/height cannot be computed from each other.
945@mixin large-screen-ratio ($selector: 'div') { 892@mixin block-ratio ($selector: 'div', $inverted-ratio: 9/16) {
893 $padding-percent: percentage($inverted-ratio);
894
946 position: relative; 895 position: relative;
947 height: 0; 896 height: 0;
948 width: 100%; 897 width: 100%;
949 padding-top: 56%; 898 padding-top: $padding-percent;
950 899
951 #{$selector} { 900 #{$selector} {
952 position: absolute; 901 position: absolute;
@@ -991,3 +940,31 @@
991 940
992 border-left: $width solid rgba(255, 255, 255, 0.95); 941 border-left: $width solid rgba(255, 255, 255, 0.95);
993} 942}
943
944@mixin on-small-main-col () {
945 :host-context(.main-col:not(.expanded)) {
946 @media screen and (max-width: $small-view + $menu-width) {
947 @content;
948 }
949 }
950
951 :host-context(.main-col.expanded) {
952 @media screen and (max-width: $small-view) {
953 @content;
954 }
955 }
956}
957
958@mixin on-mobile-main-col () {
959 :host-context(.main-col:not(.expanded)) {
960 @media screen and (max-width: $mobile-view + $menu-width) {
961 @content;
962 }
963 }
964
965 :host-context(.main-col.expanded) {
966 @media screen and (max-width: $mobile-view) {
967 @content;
968 }
969 }
970}
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss
index c8316473d..d2a5d2bd9 100644
--- a/client/src/sass/include/_variables.scss
+++ b/client/src/sass/include/_variables.scss
@@ -16,9 +16,10 @@ $grey-foreground-hover-color: #303030;
16$grey-button-outline-color: scale-color($grey-foreground-color, $alpha: -95%); 16$grey-button-outline-color: scale-color($grey-foreground-color, $alpha: -95%);
17 17
18$main-color: hsl(24, 90%, 50%); 18$main-color: hsl(24, 90%, 50%);
19$main-hover-color: lighten($main-color, 5%);
20$main-color-lighter: lighten($main-color, 10%); 19$main-color-lighter: lighten($main-color, 10%);
21$main-color-lightest: lighten($main-color, 40%); 20$main-color-lightest: lighten($main-color, 40%);
21$main-hover-color: lighten($main-color, 5%);
22
22$secondary-color: hsl(187, 77%, 34%); 23$secondary-color: hsl(187, 77%, 34%);
23 24
24$support-button: inherit; 25$support-button: inherit;
@@ -47,18 +48,34 @@ $menu-bottom-color: #C6C6C6;
47$menu-width: 240px; 48$menu-width: 240px;
48$menu-lateral-padding: 26px; 49$menu-lateral-padding: 26px;
49 50
50$sub-menu-color: #F7F7F7; 51$sub-menu-background-color: #F7F7F7;
51$sub-menu-height: 81px; 52$sub-menu-height: 81px;
52 53
54$channel-background-color: #f6ede8;
55
56$banner-inverted-ratio: 1/6;
57
58$max-channels-width: 1200px;
59
53$footer-height: 30px; 60$footer-height: 30px;
54$footer-margin: 30px; 61$footer-margin: 30px;
55 62
56$separator-border-color: rgba(0, 0, 0, 0.10); 63$separator-border-color: rgba(0, 0, 0, 0.10);
57 64
58$video-miniature-width: 238px;
59$video-miniature-margin-bottom: 15px; 65$video-miniature-margin-bottom: 15px;
60$video-thumbnail-height: 122px; 66
61$video-thumbnail-width: 223px; 67$video-miniature-row-name-font-size: 1.3em;
68$video-miniature-row-mobile-name-font-size: 14px;
69
70$video-miniature-row-info-font-size: 14px;
71$video-miniature-row-mobile-info-font-size: 12px;
72
73$video-thumbnail-height: 153px;
74$video-thumbnail-width: 280px;
75$video-thumbnail-medium-height: 114px;
76$video-thumbnail-medium-width: 201px;
77$video-thumbnail-small-height: 71px;
78$video-thumbnail-small-width: 125px;
62 79
63$theater-bottom-space: 115px; 80$theater-bottom-space: 115px;
64 81
@@ -98,7 +115,9 @@ $variables: (
98 115
99 --menuBackgroundColor: var(--menuBackgroundColor), 116 --menuBackgroundColor: var(--menuBackgroundColor),
100 --menuForegroundColor: var(--menuForegroundColor), 117 --menuForegroundColor: var(--menuForegroundColor),
101 --submenuColor: var(--submenuColor), 118
119 --submenuBackgroundColor: var(--submenuBackgroundColor),
120 --channelBackgroundColor: var(--channelBackgroundColor),
102 121
103 --inputForegroundColor: var(--inputForegroundColor), 122 --inputForegroundColor: var(--inputForegroundColor),
104 --inputBackgroundColor: var(--inputBackgroundColor), 123 --inputBackgroundColor: var(--inputBackgroundColor),
@@ -116,11 +135,20 @@ $variables: (
116 --supportButtonHeartColor: var(--supportButtonHeartColor), 135 --supportButtonHeartColor: var(--supportButtonHeartColor),
117 136
118 --embedForegroundColor: var(--embedForegroundColor), 137 --embedForegroundColor: var(--embedForegroundColor),
119 --embedBigPlayBackgroundColor: var(--embedBigPlayBackgroundColor) 138 --embedBigPlayBackgroundColor: var(--embedBigPlayBackgroundColor),
139
140 --horizontalMarginContent: var(--horizontalMarginContent),
141 --videosHorizontalMarginContent: var(--videosHorizontalMarginContent),
142 --mainColWidth: var(--mainColWidth)
120); 143);
121 144
145// SASS type check our CSS variables
122@function pvar($variable) { 146@function pvar($variable) {
123 @return map-get($variables, $variable); 147 @if map-has-key($variables, $variable) {
148 @return map-get($variables, $variable);
149 } @else {
150 @error "ERROR: Variable #{$variable} does not exist";
151 }
124} 152}
125 153
126/*** z-index groups ***/ 154/*** z-index groups ***/
diff --git a/client/src/sass/ng-select.scss b/client/src/sass/ng-select.scss
index 54c805ccf..13fc1d6c2 100644
--- a/client/src/sass/ng-select.scss
+++ b/client/src/sass/ng-select.scss
@@ -11,7 +11,7 @@ $ng-select-highlight: #f2690d;
11$ng-select-box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest); 11$ng-select-box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest);
12// $ng-select-placeholder: lighten($ng-select-primary-text, 40) !default; 12// $ng-select-placeholder: lighten($ng-select-primary-text, 40) !default;
13$ng-select-height: 30px; 13$ng-select-height: 30px;
14// $ng-select-value-padding-left: 10px !default; 14$ng-select-value-padding-left: 15px;
15// $ng-select-value-font-size: 0.9em !default; 15// $ng-select-value-font-size: 0.9em !default;
16 16
17@import "~@ng-select/ng-select/scss/default.theme.scss"; 17@import "~@ng-select/ng-select/scss/default.theme.scss";
@@ -20,11 +20,6 @@ $ng-select-height: 30px;
20 font-size: .9em; 20 font-size: .9em;
21} 21}
22 22
23.ng-input,
24.ng-select .ng-select-container .ng-value-container {
25 padding-left: 15px !important;
26}
27
28.ng-select { 23.ng-select {
29 &.ng-select-focused { 24 &.ng-select-focused {
30 &:not(.ng-select-opened) > .ng-select-container { 25 &:not(.ng-select-opened) > .ng-select-container {
@@ -44,4 +39,11 @@ $ng-select-height: 30px;
44 &.ng-select-single .ng-value-container .ng-value { 39 &.ng-select-single .ng-value-container .ng-value {
45 color: pvar(--inputForegroundColor); 40 color: pvar(--inputForegroundColor);
46 } 41 }
42
43 &.ng-select-multiple .ng-select-container .ng-value-container {
44 padding-left: 12px;
45 .ng-value {
46 margin-left: 3px;
47 }
48 }
47} 49}
diff --git a/client/src/sass/player/context-menu.scss b/client/src/sass/player/context-menu.scss
index f3a28ead0..ad673eea7 100644
--- a/client/src/sass/player/context-menu.scss
+++ b/client/src/sass/player/context-menu.scss
@@ -14,7 +14,7 @@ $context-menu-width: 350px;
14 14
15 .vjs-menu-content { 15 .vjs-menu-content {
16 opacity: $primary-foreground-opacity; 16 opacity: $primary-foreground-opacity;
17 color: pvar(--embedForegroundCsolor); 17 color: pvar(--embedForegroundColor);
18 font-size: $font-size !important; 18 font-size: $font-size !important;
19 font-weight: $font-semibold; 19 font-weight: $font-semibold;
20 } 20 }
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss
index 0144e89fb..81aacf1d7 100644
--- a/client/src/sass/player/peertube-skin.scss
+++ b/client/src/sass/player/peertube-skin.scss
@@ -43,10 +43,6 @@ body {
43 } 43 }
44 } 44 }
45 45
46 .vjs-button > .vjs-icon-placeholder::before {
47 line-height: $control-bar-height;
48 }
49
50 .vjs-volume-level::before { 46 .vjs-volume-level::before {
51 content: ''; /* Remove Circle From Progress Bar */ 47 content: ''; /* Remove Circle From Progress Bar */
52 } 48 }
@@ -242,8 +238,19 @@ body {
242 @include disable-outline; 238 @include disable-outline;
243 239
244 cursor: pointer; 240 cursor: pointer;
245 font-size: $play-control-font-size;
246 width: 2em; 241 width: 2em;
242
243 .vjs-icon-placeholder {
244 line-height: $control-bar-height;
245 position: relative;
246 top: -1px;
247
248 &::before {
249 font-size: 28px;
250 line-height: unset;
251 position: relative;
252 }
253 }
247 } 254 }
248 255
249 .vjs-time-control { 256 .vjs-time-control {
@@ -375,7 +382,6 @@ body {
375 .vjs-mute-control { 382 .vjs-mute-control {
376 @include disable-outline; 383 @include disable-outline;
377 384
378 line-height: $control-bar-height;
379 padding: 0; 385 padding: 0;
380 width: 30px; 386 width: 30px;
381 387
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss
index afa577819..9c9b5d4fc 100644
--- a/client/src/sass/primeng-custom.scss
+++ b/client/src/sass/primeng-custom.scss
@@ -547,7 +547,7 @@ p-table {
547 height: 46px; 547 height: 46px;
548 548
549 &.p-highlight { 549 &.p-highlight {
550 background-color: pvar(--submenuColor) !important; 550 background-color: pvar(--submenuBackgroundColor) !important;
551 551
552 td, td > a { 552 td, td > a {
553 color: pvar(--mainForegroundColor) !important; 553 color: pvar(--mainForegroundColor) !important;
@@ -558,7 +558,7 @@ p-table {
558 .p-datatable-tbody { 558 .p-datatable-tbody {
559 tr { 559 tr {
560 &:hover { 560 &:hover {
561 background-color: pvar(--submenuColor) !important; 561 background-color: pvar(--submenuBackgroundColor) !important;
562 } 562 }
563 563
564 td { 564 td {
@@ -590,16 +590,16 @@ p-table {
590 th { 590 th {
591 border: none !important; 591 border: none !important;
592 border-bottom: 1px solid !important; 592 border-bottom: 1px solid !important;
593 border-color: pvar(--submenuColor) !important; 593 border-color: pvar(--submenuBackgroundColor) !important;
594 text-align: left !important; 594 text-align: left !important;
595 padding: 5px 0 5px 15px !important; 595 padding: 5px 0 5px 15px !important;
596 font-weight: $font-semibold !important; 596 font-weight: $font-semibold !important;
597 color: pvar(--mainForegroundColor) !important; 597 color: pvar(--mainForegroundColor) !important;
598 598
599 &.p-sortable-column:hover { 599 &.p-sortable-column:hover {
600 background-color: pvar(--submenuColor) !important; 600 background-color: pvar(--submenuBackgroundColor) !important;
601 border: 1px solid !important; 601 border: 1px solid !important;
602 border-color: pvar(--submenuColor) !important; 602 border-color: pvar(--submenuBackgroundColor) !important;
603 border-width: 0 1px !important; 603 border-width: 0 1px !important;
604 604
605 &:first-child { 605 &:first-child {
@@ -608,7 +608,7 @@ p-table {
608 } 608 }
609 609
610 &.p-highlight { 610 &.p-highlight {
611 background-color: pvar(--submenuColor) !important; 611 background-color: pvar(--submenuBackgroundColor) !important;
612 612
613 .pi { 613 .pi {
614 @extend .glyphicon; 614 @extend .glyphicon;
@@ -654,7 +654,7 @@ p-table {
654 position: relative; 654 position: relative;
655 border: none; 655 border: none;
656 border-top: 1px solid !important; 656 border-top: 1px solid !important;
657 border-color: pvar(--submenuColor) !important; 657 border-color: pvar(--submenuBackgroundColor) !important;
658 height: 40px; 658 height: 40px;
659 display: flex; 659 display: flex;
660 justify-content: center; 660 justify-content: center;
@@ -753,29 +753,32 @@ p-table {
753} 753}
754 754
755// overflow data table 755// overflow data table
756@mixin overflow-datatable ($table-min-width, $horizontal-margins, $mobile-paginator: true) { 756p-table {
757 p-table { 757 .p-datatable-wrapper {
758 .p-datatable-wrapper { 758 overflow-x: auto;
759 overflow-x: auto; 759 max-width: 100%;
760 max-width: calc(100vw - #{$horizontal-margins * 2});
761
762 table {
763 min-width: $table-min-width;
764 }
765 }
766 760
767 @if $mobile-paginator { 761 table {
768 p-paginator .p-paginator-bottom { 762 min-width: breakpoint(lg);
769 display: block; 763 }
764 }
770 765
771 .p-paginator-current { 766 @media screen and (max-width: #{breakpoint(lg)}) {
772 position: relative; 767 // Prevent overflow
773 display: block; 768 p-paginator {
774 } 769 .p-paginator-current,
770 .p-dropdown {
771 top: 0;
772 margin-top: 30px;
773 }
774 }
775 }
775 776
776 a, .p-paginator-pages { 777 @media screen and (max-width: $mobile-view) {
777 vertical-align: middle; 778 // Prevent overflow
778 } 779 p-paginator {
780 .p-paginator-pages > .p-paginator-page:not(.p-highlight) {
781 display: none;
779 } 782 }
780 } 783 }
781 } 784 }
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index cf4bc6f03..ae8f176b7 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -531,6 +531,7 @@ export class PeerTubeEmbed {
531 videoCaptions, 531 videoCaptions,
532 inactivityTimeout: 2500, 532 inactivityTimeout: 2500,
533 videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views', 533 videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views',
534 videoUUID: videoInfo.uuid,
534 535
535 isLive: videoInfo.isLive, 536 isLive: videoInfo.isLive,
536 537
@@ -545,7 +546,8 @@ export class PeerTubeEmbed {
545 546
546 serverUrl: window.location.origin, 547 serverUrl: window.location.origin,
547 language: navigator.language, 548 language: navigator.language,
548 embedUrl: window.location.origin + videoInfo.embedPath 549 embedUrl: window.location.origin + videoInfo.embedPath,
550 embedTitle: videoInfo.name
549 }, 551 },
550 552
551 webtorrent: { 553 webtorrent: {
@@ -783,6 +785,8 @@ export class PeerTubeEmbed {
783 785
784 showModal: unimplemented, 786 showModal: unimplemented,
785 787
788 getServerConfig: unimplemented,
789
786 markdownRenderer: { 790 markdownRenderer: {
787 textMarkdownToHTML: unimplemented, 791 textMarkdownToHTML: unimplemented,
788 enhancedMarkdownToHTML: unimplemented 792 enhancedMarkdownToHTML: unimplemented
diff --git a/client/src/types/register-client-option.model.ts b/client/src/types/register-client-option.model.ts
index e3c6d803d..7e5356a2b 100644
--- a/client/src/types/register-client-option.model.ts
+++ b/client/src/types/register-client-option.model.ts
@@ -1,5 +1,6 @@
1import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' 1import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
2import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model' 2import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model'
3import { ServerConfig } from '@shared/models/server'
3 4
4export type RegisterClientOptions = { 5export type RegisterClientOptions = {
5 registerHook: (options: RegisterClientHookOptions) => void 6 registerHook: (options: RegisterClientHookOptions) => void
@@ -16,6 +17,8 @@ export type RegisterClientHelpers = {
16 17
17 getSettings: () => Promise<{ [ name: string ]: string }> 18 getSettings: () => Promise<{ [ name: string ]: string }>
18 19
20 getServerConfig: () => Promise<ServerConfig>
21
19 notifier: { 22 notifier: {
20 info: (text: string, title?: string, timeout?: number) => void, 23 info: (text: string, title?: string, timeout?: number) => void,
21 error: (text: string, title?: string, timeout?: number) => void, 24 error: (text: string, title?: string, timeout?: number) => void,