aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.html2
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html2
-rw-r--r--client/src/app/+about/about-instance/contact-admin-modal.component.html7
-rw-r--r--client/src/app/+about/about-instance/contact-admin-modal.component.scss1
-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.html114
-rw-r--r--client/src/app/+accounts/accounts.component.scss179
-rw-r--r--client/src/app/+accounts/accounts.component.ts121
-rw-r--r--client/src/app/+accounts/accounts.module.ts8
-rw-r--r--client/src/app/+admin/admin.module.ts4
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html2
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html2
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html2
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html7
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts3
-rw-r--r--client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html10
-rw-r--r--client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.scss16
-rw-r--r--client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html2
-rw-r--r--client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts25
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.html8
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts11
-rw-r--r--client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html2
-rw-r--r--client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts54
-rw-r--r--client/src/app/+admin/plugins/shared/plugin-api.service.ts16
-rw-r--r--client/src/app/+admin/plugins/shared/plugin-list.component.scss2
-rw-r--r--client/src/app/+admin/system/debug/debug.component.html18
-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/+admin/users/user-list/user-list.component.html9
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts4
-rw-r--r--client/src/app/+login/login.component.html8
-rw-r--r--client/src/app/+login/login.component.scss24
-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.ts8
-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-library.module.ts4
-rw-r--r--client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html6
-rw-r--r--client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.scss8
-rw-r--r--client/src/app/+my-library/my-ownership/my-ownership.component.html7
-rw-r--r--client/src/app/+my-library/my-ownership/my-ownership.component.ts4
-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/modals/video-change-ownership.component.html7
-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/+page-not-found/page-not-found.component.html15
-rw-r--r--client/src/app/+page-not-found/page-not-found.component.ts9
-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.html141
-rw-r--r--client/src/app/+video-channels/video-channels.component.scss322
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts62
-rw-r--r--client/src/app/+video-channels/video-channels.module.ts8
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html4
-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-comment-add.component.html15
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comment-add.component.scss114
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts7
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comment.component.html12
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comment.component.scss244
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comment.component.ts4
-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.scss4
-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.scss3
-rw-r--r--client/src/app/+videos/+video-watch/player-styles.component.scss4
-rw-r--r--client/src/app/+videos/+video-watch/player-styles.component.ts15
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recent-videos-recommendation.service.ts2
-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)19
-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.html13
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.scss64
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts76
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.module.ts16
-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.scss109
-rw-r--r--client/src/app/app.module.ts2
-rw-r--r--client/src/app/core/core.module.ts5
-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.ts33
-rw-r--r--client/src/app/core/server/server.service.ts6
-rw-r--r--client/src/app/core/users/user.model.ts10
-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.scss5
-rw-r--r--client/src/app/menu/menu.component.html41
-rw-r--r--client/src/app/menu/menu.component.scss426
-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/confirm.component.html4
-rw-r--r--client/src/app/modal/confirm.component.scss2
-rw-r--r--client/src/app/modal/custom-modal.component.html6
-rw-r--r--client/src/app/modal/custom-modal.component.scss10
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.html7
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.scss12
-rw-r--r--client/src/app/modal/quick-settings-modal.component.scss24
-rw-r--r--client/src/app/modal/quick-settings-modal.component.ts3
-rw-r--r--client/src/app/modal/welcome-modal.component.html4
-rw-r--r--client/src/app/modal/welcome-modal.component.scss53
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-details.component.html14
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-details.component.ts4
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.html7
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts7
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html2
-rw-r--r--client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.html7
-rw-r--r--client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts4
-rw-r--r--client/src/app/shared/shared-account-avatar/account-avatar.component.html15
-rw-r--r--client/src/app/shared/shared-account-avatar/account-avatar.component.scss22
-rw-r--r--client/src/app/shared/shared-account-avatar/account-avatar.component.ts39
-rw-r--r--client/src/app/shared/shared-account-avatar/index.ts2
-rw-r--r--client/src/app/shared/shared-account-avatar/shared-account-avatar.module.ts23
-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/dynamic-form-field.component.html22
-rw-r--r--client/src/app/shared/shared-forms/dynamic-form-field.component.scss4
-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.ts6
-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.ts21
-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/feeds/feed.component.scss14
-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/plugins/index.ts1
-rw-r--r--client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts15
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts26
-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.ts8
-rw-r--r--client/src/app/shared/shared-moderation/account-blocklist.component.html9
-rw-r--r--client/src/app/shared/shared-moderation/account-blocklist.component.ts4
-rw-r--r--client/src/app/shared/shared-moderation/batch-domains-modal.component.html7
-rw-r--r--client/src/app/shared/shared-moderation/moderation.scss2
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/report.component.html5
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/report.component.scss2
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.html5
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.ts3
-rw-r--r--client/src/app/shared/shared-moderation/server-blocklist.component.html2
-rw-r--r--client/src/app/shared/shared-moderation/shared-moderation.module.ts4
-rw-r--r--client/src/app/shared/shared-moderation/user-ban-modal.component.html7
-rw-r--r--client/src/app/shared/shared-moderation/user-moderation-dropdown.component.html2
-rw-r--r--client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts1
-rw-r--r--client/src/app/shared/shared-moderation/video-block.component.html7
-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)6
-rw-r--r--client/src/app/shared/shared-support-modal/support-modal.component.ts (renamed from client/src/app/+videos/+video-watch/modal/video-support.component.ts)22
-rw-r--r--client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss122
-rw-r--r--client/src/app/shared/shared-video-comment/video-comment.model.ts4
-rw-r--r--client/src/app/shared/shared-video-live/live-stream-information.component.html5
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.html6
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.scss35
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.ts8
-rw-r--r--client/src/app/shared/shared-video-miniature/shared-video-miniature.module.ts4
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.html152
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.scss30
-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.html13
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.scss302
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.ts37
-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
233 files changed, 3691 insertions, 2658 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/+about/about-instance/contact-admin-modal.component.html b/client/src/app/+about/about-instance/contact-admin-modal.component.html
index 81e59d46a..343e5d649 100644
--- a/client/src/app/+about/about-instance/contact-admin-modal.component.html
+++ b/client/src/app/+about/about-instance/contact-admin-modal.component.html
@@ -45,14 +45,11 @@
45 45
46 <div class="form-group inputs"> 46 <div class="form-group inputs">
47 <input 47 <input
48 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 48 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
49 (click)="hide()" (key.enter)="hide()" 49 (click)="hide()" (key.enter)="hide()"
50 > 50 >
51 51
52 <input 52 <input type="submit" i18n-value value="Submit" class="peertube-button orange-button" [disabled]="!form.valid" />
53 type="submit" i18n-value value="Submit" class="action-button-submit"
54 [disabled]="!form.valid"
55 >
56 </div> 53 </div>
57 </form> 54 </form>
58 55
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.scss b/client/src/app/+about/about-instance/contact-admin-modal.component.scss
index 260d77888..6c1c89225 100644
--- a/client/src/app/+about/about-instance/contact-admin-modal.component.scss
+++ b/client/src/app/+about/about-instance/contact-admin-modal.component.scss
@@ -3,7 +3,6 @@
3 3
4input[type=text] { 4input[type=text] {
5 @include peertube-input-text(340px); 5 @include peertube-input-text(340px);
6 display: block;
7} 6}
8 7
9textarea { 8textarea {
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..ea7a317eb 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -1,57 +1,87 @@
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 <my-account-avatar [account]="account" size="120"></my-account-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 i18n-title [title]="'Created on ' + (account.createdAt | date)">{{ 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>
38 </div>
39 </div> 51 </div>
40 52
41 <div class="links w-100"> 53 <div *ngIf="hasShowMoreDescription()" class="show-more" role="button"
42 <ng-template #linkTemplate let-item="item"> 54 (click)="accountDescriptionExpanded = !accountDescriptionExpanded"
43 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> 55 title="Show the complete description" i18n-title i18n
44 </ng-template> 56 >
57 Show more...
58 </div>
45 59
46 <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> 60 <div class="buttons">
61 <a *ngIf="isManageable()" routerLink="/my-account" class="peertube-button-link orange-button" i18n>
62 Manage account
63 </a>
47 64
48 <simple-search-input (searchChanged)="searchChanged($event)" name="search-videos" i18n-placeholder placeholder="Search videos"></simple-search-input> 65 <my-subscribe-button *ngIf="hasVideoChannels() && !isManageable()" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button>
49 </div> 66 </div>
50 </div> 67 </div>
51 68
52 <div class="margin-content"> 69 <div class="links">
53 <router-outlet (activate)="onOutletLoaded($event)"></router-outlet> 70 <ng-template #linkTemplate let-item="item">
71 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
72 </ng-template>
73
74 <list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
75
76 <simple-search-input
77 [alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)"
78 (inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos"
79 i18n-iconTitle icon-title="Search account videos"
80 i18n-placeholder placeholder="Search account videos"
81 ></simple-search-input>
54 </div> 82 </div>
83
84 <router-outlet (activate)="onOutletLoaded($event)"></router-outlet>
55</div> 85</div>
56 86
57<ng-container *ngIf="prependModerationActions"> 87<ng-container *ngIf="prependModerationActions">
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index 40c6b6493..56927dea6 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,101 @@ 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 padding-bottom: var(--myGlobalTopPadding);
57 font-size: var(--myFontSize);
58}
59
60.account-avatar-row {
61 @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
62}
63
64.description {
65 grid-column: 1 / 3;
66 max-width: 1000px;
67 word-break: break-word;
68}
69
70.show-more {
71 @include show-more-description;
72
73 display: none;
74 text-align: center;
75}
76
77.buttons {
78 grid-column: 2;
79 grid-row: 1;
80
81 display: flex;
82 flex-wrap: wrap;
83 justify-content: flex-end;
84 align-content: flex-start;
85
86 > *:not(:last-child) {
87 margin-bottom: 15px;
88 }
89
90 > a {
91 white-space: nowrap;
92 }
93}
94
95@media screen and (max-width: $small-view) {
96 .root {
97 --myGlobalTopPadding: 45px;
98 --myChannelImgMargin: 15px;
99 }
100
101 .account-info {
102 display: block;
103 padding-bottom: 60px;
104 }
105
106 .description:not(.expanded) {
107 max-height: 70px;
108
109 @include fade-text(30px, pvar(--submenuBackgroundColor));
110 }
111
112 .show-more {
113 display: block;
114 }
115
116 .buttons {
117 justify-content: center;
118 }
65} 119}
66 120
67@media screen and (max-width: $mobile-view) { 121@media screen and (max-width: $mobile-view) {
68 .sub-menu { 122 .root {
69 .actor { 123 --myGlobalTopPadding: 15px;
70 flex-direction: column; 124 --myFontSize: 14px;
71 align-items: center; 125 --myGreyFontSize: 13px;
72 126 }
73 img, 127
74 .actor-info .actor-names .actor-display-name { 128 .account-info {
75 margin-right: 0; 129 display: block;
76 } 130 padding-bottom: 30px;
77 131 }
78 .actor-info { 132
79 .actor-names { 133 .links {
80 flex-direction: column; 134 margin: auto !important;
81 align-items: center; 135 width: min-content;
82 } 136 }
83 137
84 my-user-moderation-dropdown { 138 .show-more {
85 margin-left: 0; 139 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 } 140 }
98} 141}
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..22cdd0642 100644
--- a/client/src/app/+accounts/accounts.module.ts
+++ b/client/src/app/+accounts/accounts.module.ts
@@ -5,12 +5,12 @@ 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'
13import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/shared-account-avatar.module'
14 14
15@NgModule({ 15@NgModule({
16 imports: [ 16 imports: [
@@ -21,14 +21,14 @@ import { AccountsComponent } from './accounts.component'
21 SharedUserSubscriptionModule, 21 SharedUserSubscriptionModule,
22 SharedModerationModule, 22 SharedModerationModule,
23 SharedVideoMiniatureModule, 23 SharedVideoMiniatureModule,
24 SharedGlobalIconModule 24 SharedGlobalIconModule,
25 SharedAccountAvatarModule
25 ], 26 ],
26 27
27 declarations: [ 28 declarations: [
28 AccountsComponent, 29 AccountsComponent,
29 AccountVideosComponent, 30 AccountVideosComponent,
30 AccountVideoChannelsComponent, 31 AccountVideoChannelsComponent,
31 AccountAboutComponent,
32 AccountSearchComponent 32 AccountSearchComponent
33 ], 33 ],
34 34
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index fd648a425..8d1c3eadb 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'
@@ -38,6 +39,7 @@ import { JobService, LogsComponent, LogsService, SystemComponent } from './syste
38import { DebugComponent, DebugService } from './system/debug' 39import { DebugComponent, DebugService } from './system/debug'
39import { JobsComponent } from './system/jobs/jobs.component' 40import { JobsComponent } from './system/jobs/jobs.component'
40import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' 41import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users'
42import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/shared-account-avatar.module'
41 43
42@NgModule({ 44@NgModule({
43 imports: [ 45 imports: [
@@ -49,6 +51,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
49 SharedGlobalIconModule, 51 SharedGlobalIconModule,
50 SharedAbuseListModule, 52 SharedAbuseListModule,
51 SharedVideoCommentModule, 53 SharedVideoCommentModule,
54 SharedAccountAvatarModule,
55 SharedActorImageModule,
52 56
53 TableModule, 57 TableModule,
54 SelectButtonModule, 58 SelectButtonModule,
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html
index 622d464e4..633de9677 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.html
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html
@@ -24,7 +24,7 @@
24 24
25 <ng-template pTemplate="header"> 25 <ng-template pTemplate="header">
26 <tr> 26 <tr>
27 <th style="width: 150px;">Actions</th> 27 <th style="width: 150px;" i18n>Actions</th>
28 <th i18n>Follower handle</th> 28 <th i18n>Follower handle</th>
29 <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th> 29 <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
30 <th style="width: 100px;" i18n pSortableColumn="score">Score <p-sortIcon field="score"></p-sortIcon></th> 30 <th style="width: 100px;" i18n pSortableColumn="score">Score <p-sortIcon field="score"></p-sortIcon></th>
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html
index cf87ec05f..f4e6a60fe 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.html
+++ b/client/src/app/+admin/follows/following-list/following-list.component.html
@@ -31,7 +31,7 @@
31 31
32 <ng-template pTemplate="header"> 32 <ng-template pTemplate="header">
33 <tr> 33 <tr>
34 <th style="width: 150px;">Action</th> 34 <th style="width: 150px;" i18n>Action</th>
35 <th i18n>Host</th> 35 <th i18n>Host</th>
36 <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th> 36 <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
37 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 37 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
index a654e51a6..f3bcca497 100644
--- a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
@@ -24,7 +24,7 @@
24 <ng-template pTemplate="header"> 24 <ng-template pTemplate="header">
25 <tr> 25 <tr>
26 <th style="width: 40px;"></th> 26 <th style="width: 40px;"></th>
27 <th style="width: 150px;">Action</th> 27 <th style="width: 150px;" i18n>Action</th>
28 <th style="width: 160px;" i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th> 28 <th style="width: 160px;" i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th>
29 <th i18n pSortableColumn="name">Video <p-sortIcon field="name"></p-sortIcon></th > 29 <th i18n pSortableColumn="name">Video <p-sortIcon field="name"></p-sortIcon></th >
30 <th style="width: 100px;" i18n *ngIf="isDisplayingRemoteVideos()">Total size</th> 30 <th style="width: 100px;" i18n *ngIf="isDisplayingRemoteVideos()">Total size</th>
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
index 128f4962d..f5cf93adb 100644
--- a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
@@ -34,12 +34,7 @@
34 <td> 34 <td>
35 <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> 35 <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
36 <div class="chip two-lines"> 36 <div class="chip two-lines">
37 <img 37 <my-account-avatar [account]="accountBlock.blockedAccount"></my-account-avatar>
38 class="avatar"
39 [src]="accountBlock.blockedAccount.avatar?.path"
40 (error)="switchToDefaultAvatar($event)"
41 alt="Avatar"
42 >
43 <div> 38 <div>
44 {{ accountBlock.blockedAccount.displayName }} 39 {{ accountBlock.blockedAccount.displayName }}
45 <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span> 40 <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span>
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/moderation/video-comment-list/video-comment-list.component.html b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html
index fa4a8edfd..d360c3c51 100644
--- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html
+++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html
@@ -5,7 +5,7 @@
5 <my-feed [syndicationItems]="syndicationItems"></my-feed> 5 <my-feed [syndicationItems]="syndicationItems"></my-feed>
6</h1> 6</h1>
7 7
8<em>This view also shows comments from muted accounts.</em> 8<em i18n>This view also shows comments from muted accounts.</em>
9 9
10<p-table 10<p-table
11 [value]="comments" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" 11 [value]="comments" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
@@ -86,12 +86,8 @@
86 <td> 86 <td>
87 <a [href]="videoComment.account.localUrl" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> 87 <a [href]="videoComment.account.localUrl" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
88 <div class="chip two-lines"> 88 <div class="chip two-lines">
89 <img 89 <my-account-avatar [account]="videoComment.account"></my-account-avatar>
90 class="avatar" 90 <div>
91 [src]="videoComment.accountAvatarUrl"
92 alt=""
93 >
94 <div>
95 {{ videoComment.account.displayName }} 91 {{ videoComment.account.displayName }}
96 <span>{{ videoComment.by }}</span> 92 <span>{{ videoComment.by }}</span>
97 </div> 93 </div>
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.scss b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.scss
index d208944fe..c9262da09 100644
--- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.scss
+++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.scss
@@ -1,17 +1,9 @@
1@import 'mixins'; 1@import 'mixins';
2 2
3h1 { 3my-feed {
4 my-feed { 4 margin-left: 5px;
5 margin-left: 5px; 5 display: inline-block;
6 display: inline-block; 6 width: 15px;
7
8 ::ng-deep {
9 my-global-icon {
10 width: 15px !important;
11 top: 0 !important;
12 }
13 }
14 }
15} 7}
16 8
17my-global-icon { 9my-global-icon {
diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html
index 82a965313..9cbec03a1 100644
--- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html
+++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html
@@ -23,7 +23,7 @@
23 </a> 23 </a>
24 24
25 <div class="buttons"> 25 <div class="buttons">
26 <my-edit-button *ngIf="pluginType !== PluginType.THEME" [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label></my-edit-button> 26 <my-edit-button *ngIf="!isTheme(plugin)" [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label></my-edit-button>
27 27
28 <my-button class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)" 28 <my-button class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)"
29 [label]="getUpdateLabel(plugin)" icon="refresh" [attr.disabled]="isUpdating(plugin)" 29 [label]="getUpdateLabel(plugin)" icon="refresh" [attr.disabled]="isUpdating(plugin)"
diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
index c78b19585..1a95980ae 100644
--- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
+++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
@@ -31,8 +31,6 @@ export class PluginListInstalledComponent implements OnInit {
31 plugins: PeerTubePlugin[] = [] 31 plugins: PeerTubePlugin[] = []
32 updating: { [name: string]: boolean } = {} 32 updating: { [name: string]: boolean } = {}
33 33
34 PluginType = PluginType
35
36 onDataSubject = new Subject<any[]>() 34 onDataSubject = new Subject<any[]>()
37 35
38 constructor ( 36 constructor (
@@ -104,6 +102,10 @@ export class PluginListInstalledComponent implements OnInit {
104 return !!this.updating[this.getUpdatingKey(plugin)] 102 return !!this.updating[this.getUpdatingKey(plugin)]
105 } 103 }
106 104
105 isTheme (plugin: PeerTubePlugin) {
106 return plugin.type === PluginType.THEME
107 }
108
107 async uninstall (plugin: PeerTubePlugin) { 109 async uninstall (plugin: PeerTubePlugin) {
108 const res = await this.confirmService.confirm( 110 const res = await this.confirmService.confirm(
109 $localize`Do you really want to uninstall ${plugin.name}?`, 111 $localize`Do you really want to uninstall ${plugin.name}?`,
@@ -128,6 +130,16 @@ export class PluginListInstalledComponent implements OnInit {
128 const updatingKey = this.getUpdatingKey(plugin) 130 const updatingKey = this.getUpdatingKey(plugin)
129 if (this.updating[updatingKey]) return 131 if (this.updating[updatingKey]) return
130 132
133 if (this.isMajorUpgrade(plugin)) {
134 const res = await this.confirmService.confirm(
135 $localize`This is a major plugin upgrade. Please go on the plugin homepage to check potential release notes.`,
136 $localize`Upgrade`,
137 $localize`Proceed upgrade`
138 )
139
140 if (res === false) return
141 }
142
131 this.updating[updatingKey] = true 143 this.updating[updatingKey] = true
132 144
133 this.pluginApiService.update(plugin.name, plugin.type) 145 this.pluginApiService.update(plugin.name, plugin.type)
@@ -156,4 +168,13 @@ export class PluginListInstalledComponent implements OnInit {
156 private getUpdatingKey (plugin: PeerTubePlugin) { 168 private getUpdatingKey (plugin: PeerTubePlugin) {
157 return plugin.name + plugin.type 169 return plugin.name + plugin.type
158 } 170 }
171
172 private isMajorUpgrade (plugin: PeerTubePlugin) {
173 if (!plugin.latestVersion) return false
174
175 const latestMajor = plugin.latestVersion.split('.')[0]
176 const currentMajor = plugin.version.split('.')[0]
177
178 return latestMajor > currentMajor
179 }
159} 180}
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..727633399 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
@@ -48,6 +48,8 @@
48 <span *ngIf="plugin.installed" class="badge badge-success">Installed</span> 48 <span *ngIf="plugin.installed" class="badge badge-success">Installed</span>
49 49
50 <div class="buttons"> 50 <div class="buttons">
51 <my-edit-button *ngIf="plugin.installed === true && !isThemeSearch()" [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label></my-edit-button>
52
51 <my-button class="update-button" *ngIf="plugin.installed === false" (click)="install(plugin)" [loading]="isInstalling(plugin)" 53 <my-button class="update-button" *ngIf="plugin.installed === false" (click)="install(plugin)" [loading]="isInstalling(plugin)"
52 label="Install" icon="cloud-download" [attr.disabled]="isInstalling(plugin)" 54 label="Install" icon="cloud-download" [attr.disabled]="isInstalling(plugin)"
53 ></my-button> 55 ></my-button>
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts
index 4c571dee4..d2c179aba 100644
--- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts
+++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts
@@ -3,7 +3,7 @@ import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
3import { Component, OnInit } from '@angular/core' 3import { Component, OnInit } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' 5import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
6import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core' 6import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core'
7import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' 7import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model'
8import { PluginType } from '@shared/models/plugins/plugin.type' 8import { PluginType } from '@shared/models/plugins/plugin.type'
9 9
@@ -39,6 +39,7 @@ export class PluginSearchComponent implements OnInit {
39 private searchSubject = new Subject<string>() 39 private searchSubject = new Subject<string>()
40 40
41 constructor ( 41 constructor (
42 private pluginService: PluginService,
42 private pluginApiService: PluginApiService, 43 private pluginApiService: PluginApiService,
43 private notifier: Notifier, 44 private notifier: Notifier,
44 private confirmService: ConfirmService, 45 private confirmService: ConfirmService,
@@ -119,6 +120,14 @@ export class PluginSearchComponent implements OnInit {
119 return this.pluginApiService.getPluginOrThemeHref(this.pluginType, name) 120 return this.pluginApiService.getPluginOrThemeHref(this.pluginType, name)
120 } 121 }
121 122
123 getShowRouterLink (plugin: PeerTubePluginIndex) {
124 return [ '/admin', 'plugins', 'show', this.pluginService.nameToNpmName(plugin.name, this.pluginType) ]
125 }
126
127 isThemeSearch () {
128 return this.pluginType === PluginType.THEME
129 }
130
122 async install (plugin: PeerTubePluginIndex) { 131 async install (plugin: PeerTubePluginIndex) {
123 if (this.installing[plugin.npmName]) return 132 if (this.installing[plugin.npmName]) return
124 133
diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
index cb2894568..ad65293d4 100644
--- a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
+++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
@@ -7,7 +7,7 @@
7 7
8 <form *ngIf="hasRegisteredSettings()" role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 8 <form *ngIf="hasRegisteredSettings()" role="form" (ngSubmit)="formValidated()" [formGroup]="form">
9 <div class="form-group" *ngFor="let setting of registeredSettings"> 9 <div class="form-group" *ngFor="let setting of registeredSettings">
10 <my-dynamic-form-field [form]="form" [setting]="setting" [formErrors]="formErrors"></my-dynamic-form-field> 10 <my-dynamic-form-field [hidden]="isSettingHidden(setting)" [form]="form" [setting]="setting" [formErrors]="formErrors"></my-dynamic-form-field>
11 </div> 11 </div>
12 12
13 <input type="submit" i18n value="Update plugin settings" [disabled]="!form.valid"> 13 <input type="submit" i18n value="Update plugin settings" [disabled]="!form.valid">
diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts
index 4e60ca290..ca9ad9922 100644
--- a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts
+++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts
@@ -2,7 +2,7 @@ import { Subscription } from 'rxjs'
2import { map, switchMap } from 'rxjs/operators' 2import { map, switchMap } from 'rxjs/operators'
3import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ActivatedRoute } from '@angular/router' 4import { ActivatedRoute } from '@angular/router'
5import { Notifier } from '@app/core' 5import { Notifier, PluginService } from '@app/core'
6import { BuildFormArgument } from '@app/shared/form-validators' 6import { BuildFormArgument } from '@app/shared/form-validators'
7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
8import { PeerTubePlugin, RegisterServerSettingOptions } from '@shared/models' 8import { PeerTubePlugin, RegisterServerSettingOptions } from '@shared/models'
@@ -19,10 +19,12 @@ export class PluginShowInstalledComponent extends FormReactive implements OnInit
19 pluginTypeLabel: string 19 pluginTypeLabel: string
20 20
21 private sub: Subscription 21 private sub: Subscription
22 private npmName: string
22 23
23 constructor ( 24 constructor (
24 protected formValidatorService: FormValidatorService, 25 protected formValidatorService: FormValidatorService,
25 private pluginService: PluginApiService, 26 private pluginService: PluginService,
27 private pluginAPIService: PluginApiService,
26 private notifier: Notifier, 28 private notifier: Notifier,
27 private route: ActivatedRoute 29 private route: ActivatedRoute
28 ) { 30 ) {
@@ -32,9 +34,9 @@ export class PluginShowInstalledComponent extends FormReactive implements OnInit
32 ngOnInit () { 34 ngOnInit () {
33 this.sub = this.route.params.subscribe( 35 this.sub = this.route.params.subscribe(
34 routeParams => { 36 routeParams => {
35 const npmName = routeParams['npmName'] 37 this.npmName = routeParams['npmName']
36 38
37 this.loadPlugin(npmName) 39 this.loadPlugin(this.npmName)
38 } 40 }
39 ) 41 )
40 } 42 }
@@ -46,7 +48,7 @@ export class PluginShowInstalledComponent extends FormReactive implements OnInit
46 formValidated () { 48 formValidated () {
47 const settings = this.form.value 49 const settings = this.form.value
48 50
49 this.pluginService.updatePluginSettings(this.plugin.name, this.plugin.type, settings) 51 this.pluginAPIService.updatePluginSettings(this.plugin.name, this.plugin.type, settings)
50 .subscribe( 52 .subscribe(
51 () => { 53 () => {
52 this.notifier.success($localize`Settings updated.`) 54 this.notifier.success($localize`Settings updated.`)
@@ -60,18 +62,27 @@ export class PluginShowInstalledComponent extends FormReactive implements OnInit
60 return Array.isArray(this.registeredSettings) && this.registeredSettings.length !== 0 62 return Array.isArray(this.registeredSettings) && this.registeredSettings.length !== 0
61 } 63 }
62 64
65 isSettingHidden (setting: RegisterServerSettingOptions) {
66 const script = this.pluginService.getRegisteredSettingsScript(this.npmName)
67
68 if (!script?.isSettingHidden) return false
69
70 return script.isSettingHidden({ setting, formValues: this.form.value })
71 }
72
63 private loadPlugin (npmName: string) { 73 private loadPlugin (npmName: string) {
64 this.pluginService.getPlugin(npmName) 74 this.pluginAPIService.getPlugin(npmName)
65 .pipe(switchMap(plugin => { 75 .pipe(switchMap(plugin => {
66 return this.pluginService.getPluginRegisteredSettings(plugin.name, plugin.type) 76 return this.pluginAPIService.getPluginRegisteredSettings(plugin.name, plugin.type)
67 .pipe(map(data => ({ plugin, registeredSettings: data.registeredSettings }))) 77 .pipe(map(data => ({ plugin, registeredSettings: data.registeredSettings })))
68 })) 78 }))
69 .subscribe( 79 .subscribe(
70 ({ plugin, registeredSettings }) => { 80 async ({ plugin, registeredSettings }) => {
71 this.plugin = plugin 81 this.plugin = plugin
72 this.registeredSettings = registeredSettings
73 82
74 this.pluginTypeLabel = this.pluginService.getPluginTypeLabel(this.plugin.type) 83 this.registeredSettings = await this.translateSettings(registeredSettings)
84
85 this.pluginTypeLabel = this.pluginAPIService.getPluginTypeLabel(this.plugin.type)
75 86
76 this.buildSettingsForm() 87 this.buildSettingsForm()
77 }, 88 },
@@ -104,4 +115,27 @@ export class PluginShowInstalledComponent extends FormReactive implements OnInit
104 return registered.default 115 return registered.default
105 } 116 }
106 117
118 private async translateSettings (settings: RegisterServerSettingOptions[]) {
119 for (const setting of settings) {
120 for (const key of [ 'label', 'html', 'descriptionHTML' ]) {
121 if (setting[key]) setting[key] = await this.pluginService.translateBy(this.npmName, setting[key])
122 }
123
124 if (Array.isArray(setting.options)) {
125 const newOptions = []
126
127 for (const o of setting.options) {
128 newOptions.push({
129 value: o.value,
130 label: await this.pluginService.translateBy(this.npmName, o.label)
131 })
132 }
133
134 setting.options = newOptions
135 }
136 }
137
138 return settings
139 }
140
107} 141}
diff --git a/client/src/app/+admin/plugins/shared/plugin-api.service.ts b/client/src/app/+admin/plugins/shared/plugin-api.service.ts
index fad30576b..d91fccc09 100644
--- a/client/src/app/+admin/plugins/shared/plugin-api.service.ts
+++ b/client/src/app/+admin/plugins/shared/plugin-api.service.ts
@@ -94,7 +94,6 @@ export class PluginApiService {
94 94
95 return this.authHttp.get<RegisteredServerSettings>(path) 95 return this.authHttp.get<RegisteredServerSettings>(path)
96 .pipe( 96 .pipe(
97 switchMap(res => this.translateSettingsLabel(npmName, res)),
98 catchError(res => this.restExtractor.handleError(res)) 97 catchError(res => this.restExtractor.handleError(res))
99 ) 98 )
100 } 99 }
@@ -141,19 +140,4 @@ export class PluginApiService {
141 140
142 return `https://www.npmjs.com/package/peertube-${typeString}-${name}` 141 return `https://www.npmjs.com/package/peertube-${typeString}-${name}`
143 } 142 }
144
145 private translateSettingsLabel (npmName: string, res: RegisteredServerSettings): Observable<RegisteredServerSettings> {
146 return this.pluginService.translationsObservable
147 .pipe(
148 map(allTranslations => allTranslations[npmName]),
149 map(translations => {
150 const registeredSettings = res.registeredSettings
151 .map(r => {
152 return Object.assign({}, r, { label: peertubeTranslate(r.label, translations) })
153 })
154
155 return { registeredSettings }
156 })
157 )
158 }
159} 143}
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/system/debug/debug.component.html b/client/src/app/+admin/system/debug/debug.component.html
index 75f3df601..2dc509383 100644
--- a/client/src/app/+admin/system/debug/debug.component.html
+++ b/client/src/app/+admin/system/debug/debug.component.html
@@ -1,19 +1,19 @@
1<div class="root"> 1<div class="root">
2 <h4>IP</h4> 2 <h4 i18n>IP address</h4>
3 3
4 <p>PeerTube thinks your web browser public IP is <strong>{{ debug?.ip }}</strong>.</p> 4 <p i18n>PeerTube thinks your web browser public IP is <strong>{{ debug?.ip }}</strong>.</p>
5 5
6 <p>If this is not your correct public IP, please consider fixing it because:</p> 6 <p i18n>If this is not your correct public IP, please consider fixing it because:</p>
7 <ul> 7 <ul>
8 <li>Views may not be counted correctly (reduced compared to what they should be)</li> 8 <li i18n>Views may not be counted correctly (reduced compared to what they should be)</li>
9 <li>Anti brute force system could be overzealous</li> 9 <li i18n>Anti brute force system could be overzealous</li>
10 <li>P2P system could not work correctly</li> 10 <li i18n>P2P system could not work correctly</li>
11 </ul> 11 </ul>
12 12
13 <p>To fix it:<p> 13 <p i18n>To fix it:<p>
14 <ul> 14 <ul>
15 <li>Check the <code>trust_proxy</code> configuration key</li> 15 <li i18n>Check the <code>trust_proxy</code> configuration key</li>
16 <li>If you run PeerTube using Docker, check you run the <code>reverse-proxy</code> with <code>network_mode: "host"</code> 16 <li i18n>If you run PeerTube using Docker, check you run the <code>reverse-proxy</code> with <code>network_mode: "host"</code>
17 (see <a href="https://github.com/Chocobozzz/PeerTube/issues/1643#issuecomment-464789666">issue 1643</a>)</li> 17 (see <a href="https://github.com/Chocobozzz/PeerTube/issues/1643#issuecomment-464789666">issue 1643</a>)</li>
18 </ul> 18 </ul>
19</div> 19</div>
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/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index 4a09fb392..e114f3425 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -106,13 +106,8 @@
106 <td *ngIf="isSelected('username')"> 106 <td *ngIf="isSelected('username')">
107 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]"> 107 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
108 <div class="chip two-lines"> 108 <div class="chip two-lines">
109 <img 109 <my-account-avatar [account]="user?.account"></my-account-avatar>
110 class="avatar" 110 <div>
111 [src]="user?.account?.avatar?.path"
112 (error)="switchToDefaultAvatar($event)"
113 alt="Avatar"
114 >
115 <div>
116 <span class="user-table-primary-text">{{ user.account.displayName }}</span> 111 <span class="user-table-primary-text">{{ user.account.displayName }}</span>
117 <span class="text-muted">{{ user.username }}</span> 112 <span class="text-muted">{{ user.username }}</span>
118 </div> 113 </div>
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts
index 7875b74ad..339e18206 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/users/user-list/user-list.component.ts
@@ -163,10 +163,6 @@ export class UserListComponent extends RestTable implements OnInit {
163 this.loadData() 163 this.loadData()
164 } 164 }
165 165
166 switchToDefaultAvatar ($event: Event) {
167 ($event.target as HTMLImageElement).src = Account.GET_DEFAULT_AVATAR_URL()
168 }
169
170 async unbanUsers (users: User[]) { 166 async unbanUsers (users: User[]) {
171 const res = await this.confirmService.confirm($localize`Do you really want to unban ${users.length} users?`, $localize`Unban`) 167 const res = await this.confirmService.confirm($localize`Do you really want to unban ${users.length} users?`, $localize`Unban`)
172 if (res === false) return 168 if (res === false) return
diff --git a/client/src/app/+login/login.component.html b/client/src/app/+login/login.component.html
index 3171e5b0f..5f5b0f565 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
@@ -41,7 +41,7 @@
41 </div> 41 </div>
42 </div> 42 </div>
43 43
44 <input type="submit" i18n-value value="Login" [disabled]="!form.valid"> 44 <input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid">
45 45
46 <div class="additionnal-links"> 46 <div class="additionnal-links">
47 <a i18n class="forgot-password-button" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a> 47 <a i18n class="forgot-password-button" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a>
@@ -114,12 +114,12 @@
114 114
115 <div class="modal-footer inputs"> 115 <div class="modal-footer inputs">
116 <input 116 <input
117 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 117 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
118 (click)="hideForgotPasswordModal()" (key.enter)="hideForgotPasswordModal()" 118 (click)="hideForgotPasswordModal()" (key.enter)="hideForgotPasswordModal()"
119 > 119 >
120 120
121 <input 121 <input
122 type="submit" i18n-value="Password reset button" value="Reset" class="action-button-submit" 122 type="submit" i18n-value="Password reset button" value="Reset" class="peertube-button orange-button"
123 (click)="askResetPassword()" [disabled]="!forgotPasswordEmailInput.validity.valid" 123 (click)="askResetPassword()" [disabled]="!forgotPasswordEmailInput.validity.valid"
124 > 124 >
125 </div> 125 </div>
diff --git a/client/src/app/+login/login.component.scss b/client/src/app/+login/login.component.scss
index 3cc302aec..eddaff542 100644
--- a/client/src/app/+login/login.component.scss
+++ b/client/src/app/+login/login.component.scss
@@ -8,16 +8,9 @@ label {
8 display: block; 8 display: block;
9} 9}
10 10
11input:not([type=submit]) { 11input[type=text],
12input[type=email] {
12 @include peertube-input-text(340px); 13 @include peertube-input-text(340px);
13 display: inline-block;
14 margin-right: 5px;
15
16}
17
18input[type=submit] {
19 @include peertube-button;
20 @include orange-button;
21} 14}
22 15
23.modal-body { 16.modal-body {
@@ -28,13 +21,6 @@ input[type=submit] {
28 } 21 }
29} 22}
30 23
31.modal-footer.inputs {
32 .action-button.action-button-cancel {
33 width: auto !important;
34 display: inline-block;
35 }
36}
37
38@media screen and (max-width: #{map-get($container-max-widths, sm)}) { 24@media screen and (max-width: #{map-get($container-max-widths, sm)}) {
39 .modal-body { 25 .modal-body {
40 #forgot-password-email { 26 #forgot-password-email {
@@ -42,10 +28,8 @@ input[type=submit] {
42 } 28 }
43 } 29 }
44 30
45 .modal-footer.inputs { 31 .modal-footer .grey-button {
46 .action-button.action-button-cancel { 32 display: none;
47 display: none;
48 }
49 } 33 }
50} 34}
51 35
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..050cd4b34 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,8 +22,8 @@ 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'
26import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/shared-account-avatar.module'
25 27
26@NgModule({ 28@NgModule({
27 imports: [ 29 imports: [
@@ -37,7 +39,9 @@ import { MyAccountComponent } from './my-account.component'
37 SharedUserInterfaceSettingsModule, 39 SharedUserInterfaceSettingsModule,
38 SharedGlobalIconModule, 40 SharedGlobalIconModule,
39 SharedAbuseListModule, 41 SharedAbuseListModule,
40 SharedShareModal 42 SharedShareModal,
43 SharedAccountAvatarModule,
44 SharedActorImageModule
41 ], 45 ],
42 46
43 declarations: [ 47 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-library.module.ts b/client/src/app/+my-library/my-library.module.ts
index 5518cfd98..a1d706f0b 100644
--- a/client/src/app/+my-library/my-library.module.ts
+++ b/client/src/app/+my-library/my-library.module.ts
@@ -26,6 +26,7 @@ import { MyVideoPlaylistUpdateComponent } from './my-video-playlists/my-video-pl
26import { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlists.component' 26import { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlists.component'
27import { VideoChangeOwnershipComponent } from './my-videos/modals/video-change-ownership.component' 27import { VideoChangeOwnershipComponent } from './my-videos/modals/video-change-ownership.component'
28import { MyVideosComponent } from './my-videos/my-videos.component' 28import { MyVideosComponent } from './my-videos/my-videos.component'
29import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/shared-account-avatar.module'
29 30
30@NgModule({ 31@NgModule({
31 imports: [ 32 imports: [
@@ -45,7 +46,8 @@ import { MyVideosComponent } from './my-videos/my-videos.component'
45 SharedGlobalIconModule, 46 SharedGlobalIconModule,
46 SharedAbuseListModule, 47 SharedAbuseListModule,
47 SharedShareModal, 48 SharedShareModal,
48 SharedVideoLiveModule 49 SharedVideoLiveModule,
50 SharedAccountAvatarModule
49 ], 51 ],
50 52
51 declarations: [ 53 declarations: [
diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html
index 27aab13b6..088765b20 100644
--- a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html
+++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html
@@ -22,13 +22,13 @@
22 <div class="modal-footer inputs"> 22 <div class="modal-footer inputs">
23 <div class="inputs"> 23 <div class="inputs">
24 <input 24 <input
25 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 25 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
26 (click)="dismiss()" (key.enter)="dismiss()" 26 (click)="dismiss()" (key.enter)="dismiss()"
27 > 27 >
28 28
29 <input 29 <input
30 type="submit" i18n-value value="Accept" class="action-button-submit" 30 type="submit" i18n-value value="Accept" class="peertube-button orange-button"
31 (click)="close()" 31 (click)="close()"
32 > 32 >
33 </div> 33 </div>
34 </div> 34 </div>
diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.scss b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.scss
index c7357f62d..bf3770e56 100644
--- a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.scss
+++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.scss
@@ -1,14 +1,6 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4select {
5 display: block;
6}
7
8.peertube-select-container { 4.peertube-select-container {
9 @include peertube-select-container(350px); 5 @include peertube-select-container(350px);
10} 6}
11
12.form-group {
13 margin: 20px 0;
14} \ No newline at end of file
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.html b/client/src/app/+my-library/my-ownership/my-ownership.component.html
index 6bf562986..d0eff0521 100644
--- a/client/src/app/+my-library/my-ownership/my-ownership.component.html
+++ b/client/src/app/+my-library/my-ownership/my-ownership.component.html
@@ -37,12 +37,7 @@
37 <td> 37 <td>
38 <a [href]="videoChangeOwnership.initiatorAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> 38 <a [href]="videoChangeOwnership.initiatorAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
39 <div class="chip two-lines"> 39 <div class="chip two-lines">
40 <img 40 <my-account-avatar [account]="videoChangeOwnership.initiatorAccount"></my-account-avatar>
41 class="avatar"
42 [src]="videoChangeOwnership.initiatorAccount.avatar?.path"
43 (error)="switchToDefaultAvatar($event)"
44 alt="Avatar"
45 >
46 <div> 41 <div>
47 {{ videoChangeOwnership.initiatorAccount.displayName }} 42 {{ videoChangeOwnership.initiatorAccount.displayName }}
48 <span class="text-muted">{{ videoChangeOwnership.initiatorAccount.nameWithHost }}</span> 43 <span class="text-muted">{{ videoChangeOwnership.initiatorAccount.nameWithHost }}</span>
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.ts b/client/src/app/+my-library/my-ownership/my-ownership.component.ts
index 78c3d9192..a938023b4 100644
--- a/client/src/app/+my-library/my-ownership/my-ownership.component.ts
+++ b/client/src/app/+my-library/my-ownership/my-ownership.component.ts
@@ -43,10 +43,6 @@ export class MyOwnershipComponent extends RestTable implements OnInit {
43 } 43 }
44 } 44 }
45 45
46 switchToDefaultAvatar ($event: Event) {
47 ($event.target as HTMLImageElement).src = Account.GET_DEFAULT_AVATAR_URL()
48 }
49
50 openAcceptModal (videoChangeOwnership: VideoChangeOwnership) { 46 openAcceptModal (videoChangeOwnership: VideoChangeOwnership) {
51 this.myAccountAcceptOwnershipComponent.show(videoChangeOwnership) 47 this.myAccountAcceptOwnershipComponent.show(videoChangeOwnership)
52 } 48 }
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/modals/video-change-ownership.component.html b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html
index c7c5a0b69..955fd4884 100644
--- a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html
+++ b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html
@@ -19,14 +19,13 @@
19 <div class="modal-footer"> 19 <div class="modal-footer">
20 <div class="form-group inputs"> 20 <div class="form-group inputs">
21 <input 21 <input
22 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 22 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
23 (click)="dismiss()" (key.enter)="dismiss()" 23 (click)="dismiss()" (key.enter)="dismiss()"
24 > 24 >
25 25
26 <input 26 <input
27 type="submit" i18n-value value="Submit" class="action-button-submit" 27 type="submit" i18n-value value="Submit" class="peertube-button orange-button"
28 [disabled]="!form.valid" 28 [disabled]="!form.valid" (click)="close()"
29 (click)="close()"
30 /> 29 />
31 </div> 30 </div>
32 </div> 31 </div>
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/+page-not-found/page-not-found.component.html b/client/src/app/+page-not-found/page-not-found.component.html
index efd3cc9f9..1e25e4495 100644
--- a/client/src/app/+page-not-found/page-not-found.component.html
+++ b/client/src/app/+page-not-found/page-not-found.component.html
@@ -3,8 +3,9 @@
3 <strong>{{ status }}.</strong> 3 <strong>{{ status }}.</strong>
4 <span class="ml-1 text-muted" i18n>That's an error.</span> 4 <span class="ml-1 text-muted" i18n>That's an error.</span>
5 5
6 <div class="text mt-4" i18n> 6 <div class="text mt-4">
7 We couldn't find any {{ getRessourceName() }} tied to the URL {{ pathname }} you were looking for. 7 <ng-container *ngIf="type === 'video'" i18n>We couldn't find any video tied to the URL {{ pathname }} you were looking for.</ng-container>
8 <ng-container *ngIf="type !== 'video'" i18n>We couldn't find any resource tied to the URL {{ pathname }} you were looking for.</ng-container>
8 </div> 9 </div>
9 10
10 <div class="text-muted mt-4"> 11 <div class="text-muted mt-4">
@@ -12,7 +13,10 @@
12 13
13 <ul> 14 <ul>
14 <li i18n>You may have used an outdated or broken link</li> 15 <li i18n>You may have used an outdated or broken link</li>
15 <li i18n>The {{ getRessourceName() }} may have been moved or deleted</li> 16 <li>
17 <ng-container *ngIf="type === 'video'" i18n>The video may have been moved or deleted</ng-container>
18 <ng-container *ngIf="type !== 'video'" i18n>The resource may have been moved or deleted</ng-container>
19 </li>
16 <li i18n>You may have typed the address or URL incorrectly</li> 20 <li i18n>You may have typed the address or URL incorrectly</li>
17 </ul> 21 </ul>
18 </div> 22 </div>
@@ -22,8 +26,9 @@
22 <strong>{{ status }}.</strong> 26 <strong>{{ status }}.</strong>
23 <span class="ml-1 text-muted" i18n>You are not authorized here.</span> 27 <span class="ml-1 text-muted" i18n>You are not authorized here.</span>
24 28
25 <div class="text mt-4" i18n> 29 <div class="text mt-4">
26 You might need to check your account is allowed by the {{ getRessourceName() }} or instance owner. 30 <ng-container *ngIf="type === 'video'" i18n>You might need to check your account is allowed by the video or instance owner.</ng-container>
31 <ng-container *ngIf="type !== 'video'" i18n>You might need to check your account is allowed by the resource or instance owner.</ng-container>
27 </div> 32 </div>
28 </div> 33 </div>
29 34
diff --git a/client/src/app/+page-not-found/page-not-found.component.ts b/client/src/app/+page-not-found/page-not-found.component.ts
index 9302201ea..94b4c8d27 100644
--- a/client/src/app/+page-not-found/page-not-found.component.ts
+++ b/client/src/app/+page-not-found/page-not-found.component.ts
@@ -32,15 +32,6 @@ export class PageNotFoundComponent implements OnInit {
32 return window.location.pathname 32 return window.location.pathname
33 } 33 }
34 34
35 getRessourceName () {
36 switch (this.type) {
37 case 'video':
38 return $localize`video`
39 default:
40 return $localize`ressource`
41 }
42 }
43
44 getMascotName () { 35 getMascotName () {
45 switch (this.status) { 36 switch (this.status) {
46 case HttpStatusCode.I_AM_A_TEAPOT_418: 37 case HttpStatusCode.I_AM_A_TEAPOT_418:
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..9308d5bb6 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -1,50 +1,123 @@
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="section-label" i18n>OWNER ACCOUNT</div>
24
25 <div class="avatar-row">
26 <my-account-avatar [account]="videoChannel.ownerAccount" [internalHref]="getAccountUrl()" size="120"></my-account-avatar>
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>
44
45 <a class="view-account complete" [routerLink]="getAccountUrl()" i18n>
46 View owner account
47 </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 i18n-title [title]="'Channel created on ' + (videoChannel.createdAt | date)">{{ 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>
29 71
30 <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner"> 72 <div class="actor-counters">
31 <span class="d-inline-flex"><span i18n class="d-none d-sm-block mr-1">Created by</span>{{ videoChannel.ownerBy }}</span> 73 <span i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span>
32 <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> 74
33 </a> 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> 90 </div>
41 </ng-template> 91
92 <div *ngIf="hasShowMoreDescription()" class="show-more" role="button"
93 (click)="channelDescriptionExpanded = !channelDescriptionExpanded"
94 title="Show the complete description" i18n-title i18n
95 >
96 Show more...
97 </div>
98
99 <div class="channel-buttons bottom">
100 <ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template>
101 </div>
42 102
43 <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> 103 <div class="owner-card">
104 <ng-template *ngTemplateOutlet="ownerTemplate"></ng-template>
44 </div> 105 </div>
45 </div> 106 </div>
46 107
47 <div class="margin-content"> 108 <div class="bottom-owner">
48 <router-outlet></router-outlet> 109 <ng-template *ngTemplateOutlet="ownerTemplate"></ng-template>
49 </div> 110 </div>
111
112 <div class="links">
113 <ng-template #linkTemplate let-item="item">
114 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
115 </ng-template>
116
117 <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
118 </div>
119
120 <router-outlet></router-outlet>
50</div> 121</div>
122
123<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..e946707ef 100644
--- a/client/src/app/+video-channels/video-channels.component.scss
+++ b/client/src/app/+video-channels/video-channels.component.scss
@@ -1,89 +1,303 @@
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;
27 42
28 .actor-names { 43 background-color: pvar(--channelBackgroundColor);
29 grid-area: name; 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 padding-bottom: var(--myGlobalTopPadding);
61}
62
63.show-more {
64 @include show-more-description;
65
66 display: none;
67}
68
69.channel-buttons {
70 display: flex;
71 flex-wrap: wrap;
72
73 > *:not(:last-child) {
74 margin-right: 15px;
30 } 75 }
76}
77
78.channel-buttons.right {
79 margin-left: 45px;
80}
81
82// Only used by mobile
83.channel-buttons.bottom {
84 display: none;
85}
86
87.owner-card {
88 margin-left: 105px;
89 grid-column: 2;
90 // Takes all the column
91 grid-row: 1 / 3;
92 place-self: end;
93}
94
95// Only used on mobile
96.bottom-owner {
97 display: none;
98}
99
100.owner-block {
101 background-color: pvar(--mainBackgroundColor);
102 padding: 30px;
103 width: 300px;
104 font-size: var(--myFontSize);
105
106 .avatar-row {
107 display: flex;
108 margin-bottom: 15px;
31 109
32 .actor-name { 110 img {
33 flex-grow: 1; 111 @include avatar(48px);
112 }
113
114 .actor-info {
115 margin-left: 15px;
116 }
117
118 h4 {
119 font-size: 18px;
120 margin: 0;
121
122 a {
123 color: pvar(--mainForegroundColor);
124 }
125 }
34 126
35 .copy-button { 127 .actor-handle {
36 border: none; 128 font-size: var(--myGreyOwnerFontSize);
37 padding: 5px; 129 color: pvar(--greyForegroundColor);
38 margin-top: -2px;
39 } 130 }
40 } 131 }
132
133 .owner-description {
134 max-height: 140px;
135 word-break: break-word;
136
137 @include fade-text(120px, pvar(--mainBackgroundColor));
138 }
41} 139}
42 140
43.margin-content { 141.view-account.short {
44 // margin-content is required, but child views have their own margins 142 @include peertube-button-link;
45 // that match views outside the scope of accounts, so we only align 143 @include orange-button-inverted;
46 // them with the margins of .sub-menu when required. 144
47 margin: 0; 145 margin-top: 30px;
48} 146}
49 147
50.right-buttons { 148.view-account.complete {
51 display: flex; 149 display: none;
52 height: max-content; 150}
53 margin-left: auto; 151
54 margin-top: 10px; 152.copy-button {
153 border: none;
154}
55 155
56 grid-row: buttons-start / span buttons-end; 156@media screen and (max-width: 1400px) {
57 grid-column: buttons-start; 157 // Takes all the row width
158 .channel-avatar-row {
159 grid-column: 1 / 3;
160 }
161
162 .owner-card {
163 grid-row: 2;
164 margin-left: 60px;
165 }
166}
58 167
59 @include media-breakpoint-down(lg) { 168@media screen and (max-width: 1100px) {
60 flex-flow: column-reverse; 169 .root {
170 --myGlobalTopPadding: 45px;
171 --myChannelImgMargin: 15px;
172 }
173
174 .channel-info {
175 display: flex;
176 flex-direction: column;
177 margin-bottom: 0;
178 }
179
180 .channel-description:not(.expanded) {
181 max-height: 70px;
182
183 @include fade-text(30px, pvar(--channelBackgroundColor));
184 }
61 185
62 a { 186 .show-more {
63 margin-top: 0.25rem; 187 display: inline-block;
64 margin-right: 0 !important; 188 }
189
190 .channel-buttons.bottom {
191 display: flex;
192 justify-content: center;
193 margin-bottom: 30px;
194 }
195
196 .channel-buttons.right {
197 display: none;
198 }
199
200 .owner-card {
201 display: none;
202 }
203
204 .bottom-owner {
205 display: block;
206 width: 100%;
207 border-bottom: 2px solid $separator-border-color;
208 padding: var(--myGlobalTopPadding) 45px;
209 margin-bottom: 60px;
210 }
211
212 .owner-block {
213 display: grid;
214 width: 100%;
215 padding: 0;
216
217 .avatar-row {
218 grid-column: 1;
219 margin-right: 30px;
220 }
221
222 .owner-description {
223 grid-column: 2;
224 max-height: 70px;
225
226 @include fade-text(30px, pvar(--mainBackgroundColor));
227 }
228
229 .view-account {
230 grid-column: 2;
65 } 231 }
66 } 232 }
67 233
68 a { 234 .view-account.complete {
69 @include peertube-button-outline; 235 display: block;
70 line-height: 1.8; 236 text-align: right;
237 margin-top: 10px;
238 color: pvar(--mainColor);
71 } 239 }
72 240
73 my-subscribe-button { 241 .view-account.short {
74 height: min-content; 242 display: none;
75 } 243 }
76} 244}
77 245
78@media screen and (max-width: $mobile-view) { 246@media screen and (max-width: $mobile-view) {
79 .sub-menu { 247 .root {
80 .actor { 248 --myGlobalTopPadding: 15px;
81 flex-direction: column; 249 --myFontSize: 14px;
250 --myGreyChannelFontSize: 13px;
251 --myGreyOwnerFontSize: 13px;
252 }
253
254 .links {
255 margin: auto !important;
256 width: min-content;
257 }
258
259 .show-more {
260 margin-bottom: 30px;
261 }
262
263 .bottom-owner {
264 padding: 15px;
265 margin-bottom: 30px;
82 266
83 .actor-info .actor-names { 267 .section-label {
268 display: none;
269 }
270 }
271
272 .owner-block {
273 display: block;
274
275 .avatar-row {
276 display: flex;
277 flex-direction: row-reverse;
278 margin: 0;
279
280 h4 {
281 font-size: 16px;
282 }
283
284 .actor-info {
285 display: flex;
84 flex-direction: column; 286 flex-direction: column;
85 align-items: normal; 287 align-items: flex-end;
288 justify-content: flex-end;
289 margin-top: -5px;
290 }
291
292 img {
293 @include channel-avatar(64px);
294
295 margin: -30px 0 0 15px;
86 } 296 }
87 } 297 }
298
299 .owner-description {
300 display: none;
301 }
88 } 302 }
89} 303}
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..2e387f401 100644
--- a/client/src/app/+video-channels/video-channels.module.ts
+++ b/client/src/app/+video-channels/video-channels.module.ts
@@ -2,14 +2,15 @@ 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'
12import { VideoChannelsComponent } from './video-channels.component' 12import { VideoChannelsComponent } from './video-channels.component'
13import { SharedAccountAvatarModule } from '../shared/shared-account-avatar/shared-account-avatar.module'
13 14
14@NgModule({ 15@NgModule({
15 imports: [ 16 imports: [
@@ -20,13 +21,14 @@ import { VideoChannelsComponent } from './video-channels.component'
20 SharedVideoPlaylistModule, 21 SharedVideoPlaylistModule,
21 SharedVideoMiniatureModule, 22 SharedVideoMiniatureModule,
22 SharedUserSubscriptionModule, 23 SharedUserSubscriptionModule,
23 SharedGlobalIconModule 24 SharedGlobalIconModule,
25 SharedSupportModal,
26 SharedAccountAvatarModule
24 ], 27 ],
25 28
26 declarations: [ 29 declarations: [
27 VideoChannelsComponent, 30 VideoChannelsComponent,
28 VideoChannelVideosComponent, 31 VideoChannelVideosComponent,
29 VideoChannelAboutComponent,
30 VideoChannelPlaylistsComponent 32 VideoChannelPlaylistsComponent
31 ], 33 ],
32 34
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html
index 6a07dafa7..092952204 100644
--- a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html
+++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html
@@ -34,12 +34,12 @@
34 34
35 <div class="modal-footer inputs"> 35 <div class="modal-footer inputs">
36 <input 36 <input
37 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 37 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
38 (click)="hide()" (key.enter)="hide()" 38 (click)="hide()" (key.enter)="hide()"
39 > 39 >
40 40
41 <input 41 <input
42 type="submit" i18n-value value="Add this caption" class="action-button-submit" 42 type="submit" i18n-value value="Add this caption" class="peertube-button orange-button"
43 [disabled]="!form.valid" (click)="addCaption()" 43 [disabled]="!form.valid" (click)="addCaption()"
44 > 44 >
45 </div> 45 </div>
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-comment-add.component.html b/client/src/app/+videos/+video-watch/comment/video-comment-add.component.html
index fdefed09a..7bd9b7c90 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comment-add.component.html
+++ b/client/src/app/+videos/+video-watch/comment/video-comment-add.component.html
@@ -1,6 +1,6 @@
1<form novalidate [formGroup]="form" (ngSubmit)="formValidated()"> 1<form novalidate [formGroup]="form" (ngSubmit)="formValidated()">
2 <div class="avatar-and-textarea"> 2 <div class="avatar-and-textarea">
3 <img [src]="getAvatarUrl()" alt="Avatar" /> 3 <my-account-avatar [account]="user?.account" size="25"></my-account-avatar>
4 4
5 <div class="form-group"> 5 <div class="form-group">
6 <textarea i18n-placeholder placeholder="Add comment..." myAutoResize 6 <textarea i18n-placeholder placeholder="Add comment..." myAutoResize
@@ -8,8 +8,8 @@
8 (click)="openVisitorModal($event)" 8 (click)="openVisitorModal($event)"
9 formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }" 9 formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }"
10 (keyup.control.enter)="onValidKey()" (keyup.meta.enter)="onValidKey()" #textarea> 10 (keyup.control.enter)="onValidKey()" (keyup.meta.enter)="onValidKey()" #textarea>
11
12 </textarea> 11 </textarea>
12
13 <my-help class="markdown-guide" helpType="custom" iconName="markdown" tooltipPlacement="left auto" autoClose="true" i18n-title title="Markdown compatible"> 13 <my-help class="markdown-guide" helpType="custom" iconName="markdown" tooltipPlacement="left auto" autoClose="true" i18n-title title="Markdown compatible">
14 <ng-template ptTemplate="customHtml"> 14 <ng-template ptTemplate="customHtml">
15 <span i18n>Markdown compatible that supports:</span> 15 <span i18n>Markdown compatible that supports:</span>
@@ -41,10 +41,11 @@
41 </div> 41 </div>
42 42
43 <div class="comment-buttons"> 43 <div class="comment-buttons">
44 <button *ngIf="isAddButtonDisplayed()" class="cancel-button" (click)="cancelCommentReply()" type="button" i18n> 44 <button *ngIf="isAddButtonDisplayed()" class="peertube-button tertiary-button cancel-button" (click)="cancelCommentReply()" type="button" i18n>
45 Cancel 45 Cancel
46 </button> 46 </button>
47 <button *ngIf="isAddButtonDisplayed()" [ngClass]="{ disabled: !form.valid || addingComment }"> 47
48 <button *ngIf="isAddButtonDisplayed()" class="peertube-button orange-button" [ngClass]="{ disabled: !form.valid || addingComment }">
48 {{ addingCommentButtonValue }} 49 {{ addingCommentButtonValue }}
49 </button> 50 </button>
50 </div> 51 </div>
@@ -55,6 +56,7 @@
55 <h4 class="modal-title" id="modal-basic-title" i18n>You are one step away from commenting</h4> 56 <h4 class="modal-title" id="modal-basic-title" i18n>You are one step away from commenting</h4>
56 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideModals()"></my-global-icon> 57 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideModals()"></my-global-icon>
57 </div> 58 </div>
59
58 <div class="modal-body"> 60 <div class="modal-body">
59 <span i18n> 61 <span i18n>
60 You can comment using an account on any ActivityPub-compatible instance (PeerTube/Mastodon/Pleroma account for example). 62 You can comment using an account on any ActivityPub-compatible instance (PeerTube/Mastodon/Pleroma account for example).
@@ -62,14 +64,15 @@
62 64
63 <my-remote-subscribe [interact]="true" [uri]="getUri()"></my-remote-subscribe> 65 <my-remote-subscribe [interact]="true" [uri]="getUri()"></my-remote-subscribe>
64 </div> 66 </div>
67
65 <div class="modal-footer inputs"> 68 <div class="modal-footer inputs">
66 <input 69 <input
67 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 70 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
68 (click)="hideModals()" (key.enter)="hideModals()" 71 (click)="hideModals()" (key.enter)="hideModals()"
69 > 72 >
70 73
71 <input 74 <input
72 type="submit" i18n-value value="Login to comment" class="action-button-submit" 75 type="submit" i18n-value value="Login to comment" class="peertube-button orange-button"
73 (click)="gotoLogin()" 76 (click)="gotoLogin()"
74 > 77 >
75 </div> 78 </div>
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment-add.component.scss b/client/src/app/+videos/+video-watch/comment/video-comment-add.component.scss
index d938e2e28..1aa9255c2 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comment-add.component.scss
+++ b/client/src/app/+videos/+video-watch/comment/video-comment-add.component.scss
@@ -1,6 +1,10 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4$markdown-icon-height: 18px;
5$markdown-icon-width: 30px;
6$peertube-textarea-height: 60px;
7
4form { 8form {
5 margin-bottom: 30px; 9 margin-bottom: 30px;
6} 10}
@@ -9,9 +13,7 @@ form {
9 display: flex; 13 display: flex;
10 margin-bottom: 10px; 14 margin-bottom: 10px;
11 15
12 img { 16 my-account-avatar {
13 @include avatar(25px);
14
15 vertical-align: top; 17 vertical-align: top;
16 margin-right: 10px; 18 margin-right: 10px;
17 } 19 }
@@ -20,83 +22,55 @@ form {
20 flex-grow: 1; 22 flex-grow: 1;
21 margin: 0; 23 margin: 0;
22 position: relative; 24 position: relative;
25 }
23 26
24 $peertube-textarea-height: 60px; 27 textarea {
25 $markdown-icon-height: 18px; 28 @include peertube-textarea(100%, $peertube-textarea-height);
26 $markdown-icon-width: 30px; 29 @include button-focus(pvar(--mainColorLightest));
27
28 .markdown-guide {
29 position: absolute;
30 top: 5px;
31 right: 9px;
32
33 ::ng-deep .help-tooltip-button {
34 my-global-icon {
35 height: $markdown-icon-height;
36 width: $markdown-icon-width;
37
38 svg {
39 color: #C6C6C6;
40 fill: #C6C6C6;
41 border-radius: 3px;
42 }
43 }
44
45 &:focus, &:active, &:hover {
46 my-global-icon svg {
47 background-color: #C6C6C6;
48 color: pvar(--mainBackgroundColor);
49 fill: pvar(--mainBackgroundColor);
50 }
51 }
52 }
53 }
54
55 textarea {
56 @include peertube-textarea(100%, $peertube-textarea-height);
57 @include button-focus(pvar(--mainColorLightest));
58 30
59 min-height: calc(#{$peertube-textarea-height} - 15px * 2); 31 min-height: calc(#{$peertube-textarea-height} - 15px * 2);
60 padding-right: $markdown-icon-width + 15px !important; 32 padding-right: $markdown-icon-width + 15px !important;
61 33
62 @media screen and (max-width: 600px) { 34 @media screen and (max-width: 600px) {
63 padding-right: $markdown-icon-width + 19px !important; 35 padding-right: $markdown-icon-width + 19px !important;
64 } 36 }
65 37
66 &:focus::placeholder { 38 &:focus::placeholder {
67 opacity: 0; 39 opacity: 0;
68 }
69 } 40 }
70 } 41 }
71} 42}
72 43
73.comment-buttons { 44.markdown-guide {
74 display: flex; 45 position: absolute;
75 justify-content: flex-end; 46 top: 5px;
47 right: 9px;
76 48
77 button { 49 ::ng-deep .help-tooltip-button {
78 @include peertube-button; 50 my-global-icon {
79 @include disable-outline; 51 height: $markdown-icon-height;
80 @include disable-default-a-behaviour; 52 width: $markdown-icon-width;
81 53
82 &:not(:last-child) { 54 svg {
83 margin-right: .5rem; 55 color: #C6C6C6;
56 fill: #C6C6C6;
57 border-radius: 3px;
58 }
84 } 59 }
85 60
86 &:last-child { 61 &:focus, &:active, &:hover {
87 @include orange-button; 62 my-global-icon svg {
63 background-color: #C6C6C6;
64 color: pvar(--mainBackgroundColor);
65 fill: pvar(--mainBackgroundColor);
66 }
88 } 67 }
89 } 68 }
69}
90 70
91 .cancel-button { 71.comment-buttons {
92 @include tertiary-button; 72 display: flex;
93 73 justify-content: flex-end;
94 font-weight: $font-semibold;
95 display: inline-block;
96 padding: 0 10px 0 10px;
97 white-space: nowrap;
98 background: transparent;
99 }
100} 74}
101 75
102.emoji-flex { 76.emoji-flex {
@@ -119,7 +93,8 @@ form {
119} 93}
120 94
121@media screen and (max-width: 600px) { 95@media screen and (max-width: 600px) {
122 textarea, .comment-buttons button { 96 textarea,
97 .comment-buttons button {
123 font-size: 14px !important; 98 font-size: 14px !important;
124 } 99 }
125 100
@@ -129,12 +104,7 @@ form {
129} 104}
130 105
131.modal-body { 106.modal-body {
132 .btn { 107 > span {
133 @include peertube-button;
134 @include orange-button;
135 }
136
137 span {
138 float: left; 108 float: left;
139 margin-bottom: 20px; 109 margin-bottom: 20px;
140 } 110 }
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts
index f1f0dfeba..0e1362ad3 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts
+++ b/client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts
@@ -4,7 +4,7 @@ import { Router } from '@angular/router'
4import { Notifier, User } from '@app/core' 4import { Notifier, User } from '@app/core'
5import { VIDEO_COMMENT_TEXT_VALIDATOR } from '@app/shared/form-validators/video-comment-validators' 5import { VIDEO_COMMENT_TEXT_VALIDATOR } from '@app/shared/form-validators/video-comment-validators'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7import { Video, Account } from '@app/shared/shared-main' 7import { Video } from '@app/shared/shared-main'
8import { VideoComment, VideoCommentService } from '@app/shared/shared-video-comment' 8import { VideoComment, VideoCommentService } from '@app/shared/shared-video-comment'
9import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 9import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
10import { VideoCommentCreate } from '@shared/models' 10import { VideoCommentCreate } from '@shared/models'
@@ -143,11 +143,6 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
143 return window.location.href 143 return window.location.href
144 } 144 }
145 145
146 getAvatarUrl () {
147 if (this.user) return this.user.accountAvatarUrl
148 return Account.GET_DEFAULT_AVATAR_URL()
149 }
150
151 gotoLogin () { 146 gotoLogin () {
152 this.hideModals() 147 this.hideModals()
153 this.router.navigate([ '/login' ]) 148 this.router.navigate([ '/login' ])
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.html b/client/src/app/+videos/+video-watch/comment/video-comment.component.html
index ba41b6f48..4592c9c69 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comment.component.html
+++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.html
@@ -1,14 +1,6 @@
1<div *ngIf="isCommentDisplayed()" class="root-comment"> 1<div *ngIf="isCommentDisplayed()" class="root-comment">
2 <div class="left"> 2 <div class="left">
3 <a *ngIf="!comment.isDeleted" [href]="comment.account.url" target="_blank" rel="noopener noreferrer"> 3 <my-account-avatar *ngIf="!comment.isDeleted" [href]="comment.account.url" [account]="comment.account"></my-account-avatar>
4 <img
5 class="comment-avatar"
6 [src]="comment.accountAvatarUrl"
7 (error)="switchToDefaultAvatar($event)"
8 alt="Avatar"
9 />
10 </a>
11
12 <div class="vertical-border"></div> 4 <div class="vertical-border"></div>
13 </div> 5 </div>
14 6
@@ -46,7 +38,7 @@
46 <div *ngIf="isUserLoggedIn()" tabindex=0 (click)="onWantToReply()" class="comment-action-reply" i18n>Reply</div> 38 <div *ngIf="isUserLoggedIn()" tabindex=0 (click)="onWantToReply()" class="comment-action-reply" i18n>Reply</div>
47 39
48 <my-user-moderation-dropdown 40 <my-user-moderation-dropdown
49 [prependActions]="prependModerationActions" tabindex=0 41 [prependActions]="prependModerationActions" tabindex=0 [buttonStyled]="false"
50 buttonSize="small" [account]="commentAccount" [user]="commentUser" i18n-label label="Options" placement="bottom-left auto" 42 buttonSize="small" [account]="commentAccount" [user]="commentUser" i18n-label label="Options" placement="bottom-left auto"
51 ></my-user-moderation-dropdown> 43 ></my-user-moderation-dropdown>
52 </div> 44 </div>
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.scss b/client/src/app/+videos/+video-watch/comment/video-comment.component.scss
index f6ff376b9..cf33a5b0e 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comment.component.scss
+++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.scss
@@ -22,144 +22,140 @@
22 .right { 22 .right {
23 width: 100%; 23 width: 100%;
24 } 24 }
25}
25 26
26 .comment-avatar { 27.comment {
27 @include avatar(36px); 28 flex-grow: 1;
28 } 29 // Fix word-wrap with flex
29 30 min-width: 1px;
30 .comment { 31}
31 flex-grow: 1;
32 // Fix word-wrap with flex
33 min-width: 1px;
34
35 .highlighted-comment {
36 display: inline-block;
37 background-color: #F5F5F5;
38 color: #3d3d3d;
39 padding: 0 5px;
40 font-size: 13px;
41 margin-bottom: 5px;
42 font-weight: $font-semibold;
43 border-radius: 3px;
44 }
45 32
46 .comment-account-date { 33.highlighted-comment {
47 display: flex; 34 display: inline-block;
48 margin-bottom: 4px; 35 background-color: #F5F5F5;
49 36 color: #3d3d3d;
50 .video-author { 37 padding: 0 5px;
51 height: 20px; 38 font-size: 13px;
52 background-color: #888888; 39 margin-bottom: 5px;
53 border-radius: 12px; 40 font-weight: $font-semibold;
54 margin-bottom: 2px; 41 border-radius: 3px;
55 max-width: 100%; 42}
56 box-sizing: border-box;
57 flex-direction: row;
58 align-items: center;
59 display: inline-flex;
60 padding-right: 6px;
61 padding-left: 6px;
62 color: white !important;
63 }
64 43
65 .comment-account { 44.comment-account-date {
66 word-break: break-all; 45 display: flex;
67 font-weight: 600; 46 margin-bottom: 4px;
68 font-size: 90%; 47}
69 48
70 a { 49.video-author {
71 @include disable-default-a-behaviour; 50 height: 20px;
51 background-color: #888888;
52 border-radius: 12px;
53 margin-bottom: 2px;
54 max-width: 100%;
55 box-sizing: border-box;
56 flex-direction: row;
57 align-items: center;
58 display: inline-flex;
59 padding-right: 6px;
60 padding-left: 6px;
61 color: white !important;
62}
72 63
73 color: pvar(--mainForegroundColor); 64.comment-account {
65 word-break: break-all;
66 font-weight: 600;
67 font-size: 90%;
74 68
75 &:hover { 69 a {
76 text-decoration: underline; 70 @include disable-default-a-behaviour;
77 }
78 }
79 71
80 .comment-account-fid { 72 color: pvar(--mainForegroundColor);
81 opacity: .6;
82 }
83 }
84 73
85 .comment-date { 74 &:hover {
86 font-size: 90%; 75 text-decoration: underline;
87 color: pvar(--greyForegroundColor);
88 margin-left: 5px;
89 text-decoration: none;
90
91 &:hover {
92 text-decoration: underline;
93 }
94 }
95 } 76 }
77 }
96 78
97 .comment-html { 79 .comment-account-fid {
98 @include peertube-word-wrap; 80 opacity: .6;
81 }
82}
99 83
100 // Mentions 84.comment-date {
101 ::ng-deep a { 85 font-size: 90%;
86 color: pvar(--greyForegroundColor);
87 margin-left: 5px;
88 text-decoration: none;
102 89
103 &:not(.linkified-url) { 90 &:hover {
104 @include disable-default-a-behaviour; 91 text-decoration: underline;
92 }
93}
105 94
106 color: pvar(--mainForegroundColor); 95.comment-html {
96 @include peertube-word-wrap;
107 97
108 font-weight: $font-semibold; 98 // Mentions
109 } 99 ::ng-deep a {
110 100
111 } 101 &:not(.linkified-url) {
102 @include disable-default-a-behaviour;
112 103
113 // Paragraphs 104 color: pvar(--mainForegroundColor);
114 ::ng-deep p {
115 margin-bottom: .3rem;
116 }
117 105
118 &.comment-html-deleted { 106 font-weight: $font-semibold;
119 color: pvar(--greyForegroundColor);
120 margin-bottom: 1rem;
121 }
122 } 107 }
123 108
124 .comment-actions { 109 }
125 margin-bottom: 10px; 110
126 display: flex; 111 // Paragraphs
112 ::ng-deep p {
113 margin-bottom: .3rem;
114 }
127 115
128 ::ng-deep .dropdown-toggle, 116 &.comment-html-deleted {
129 .comment-action-reply { 117 color: pvar(--greyForegroundColor);
130 color: pvar(--greyForegroundColor); 118 margin-bottom: 1rem;
131 cursor: pointer; 119 }
132 margin-right: 10px; 120}
133 121
134 &:hover, &:active, &:focus, &:focus-visible { 122.comment-actions {
135 color: pvar(--mainForegroundColor); 123 margin-bottom: 10px;
136 } 124 display: flex;
137 }
138 125
139 ::ng-deep .action-button { 126 ::ng-deep .dropdown-toggle,
140 background-color: transparent; 127 .comment-action-reply {
141 padding: 0; 128 color: pvar(--greyForegroundColor);
142 font-weight: unset; 129 cursor: pointer;
143 } 130 margin-right: 10px;
144 }
145 131
146 my-video-comment-add { 132 &:hover, &:active, &:focus, &:focus-visible {
147 ::ng-deep form { 133 color: pvar(--mainForegroundColor);
148 margin-top: 1rem;
149 margin-bottom: 0;
150 }
151 } 134 }
152 } 135 }
153 136
154 .children { 137 ::ng-deep .action-button {
155 // Reduce avatars size for replies 138 background-color: transparent;
156 .comment-avatar { 139 padding: 0;
157 @include avatar(25px); 140 font-weight: unset;
158 } 141 }
142}
159 143
160 .left { 144my-video-comment-add {
161 margin-right: 6px; 145 ::ng-deep form {
162 } 146 margin-top: 1rem;
147 margin-bottom: 0;
148 }
149}
150
151.children {
152 // Reduce avatars size for replies
153 .comment-avatar {
154 @include avatar(25px);
155 }
156
157 .left {
158 margin-right: 6px;
163 } 159 }
164} 160}
165 161
@@ -170,27 +166,23 @@
170} 166}
171 167
172@media screen and (max-width: 600px) { 168@media screen and (max-width: 600px) {
173 .root-comment { 169 .children {
174 .children { 170 margin-left: -20px;
175 margin-left: -20px;
176 171
177 .left { 172 .left {
178 align-items: flex-start; 173 align-items: flex-start;
179 174
180 .vertical-border { 175 .vertical-border {
181 margin-left: 2px; 176 margin-left: 2px;
182 }
183 } 177 }
184 } 178 }
179 }
185 180
186 .comment { 181 .comment-account-date {
187 .comment-account-date { 182 flex-direction: column;
188 flex-direction: column;
189 183
190 .comment-date { 184 .comment-date {
191 margin-left: 0; 185 margin-left: 0;
192 }
193 }
194 } 186 }
195 } 187 }
196} 188}
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.ts b/client/src/app/+videos/+video-watch/comment/video-comment.component.ts
index 5c5d72b22..dd3db0c65 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comment.component.ts
+++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.ts
@@ -131,10 +131,6 @@ export class VideoCommentComponent implements OnInit, OnChanges {
131 ) 131 )
132 } 132 }
133 133
134 switchToDefaultAvatar ($event: Event) {
135 ($event.target as HTMLImageElement).src = Account.GET_DEFAULT_AVATAR_URL()
136 }
137
138 isCommentDisplayed () { 134 isCommentDisplayed () {
139 // Not deleted 135 // Not deleted
140 return !this.comment.isDeleted || 136 return !this.comment.isDeleted ||
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.scss b/client/src/app/+videos/+video-watch/comment/video-comments.component.scss
index df42fae73..e6778e1a9 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comments.component.scss
+++ b/client/src/app/+videos/+video-watch/comment/video-comments.component.scss
@@ -27,7 +27,11 @@
27 margin-left: 5px; 27 margin-left: 5px;
28 opacity: 0; 28 opacity: 0;
29 transition: ease-in .2s opacity; 29 transition: ease-in .2s opacity;
30 width: 12px;
31 position: relative;
32 top: -3px;
30 } 33 }
34
31 &:hover my-feed { 35 &:hover my-feed {
32 opacity: 1; 36 opacity: 1;
33 } 37 }
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.scss b/client/src/app/+videos/+video-watch/modal/video-support.component.scss
deleted file mode 100644
index 184e09027..000000000
--- a/client/src/app/+videos/+video-watch/modal/video-support.component.scss
+++ /dev/null
@@ -1,3 +0,0 @@
1.action-button-cancel {
2 margin-right: 0 !important;
3}
diff --git a/client/src/app/+videos/+video-watch/player-styles.component.scss b/client/src/app/+videos/+video-watch/player-styles.component.scss
new file mode 100644
index 000000000..7f1442a59
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/player-styles.component.scss
@@ -0,0 +1,4 @@
1@import 'node_modules/video.js/dist/video-js';
2
3$assets-path: '../../assets/';
4@import '../../../sass/player/index';
diff --git a/client/src/app/+videos/+video-watch/player-styles.component.ts b/client/src/app/+videos/+video-watch/player-styles.component.ts
new file mode 100644
index 000000000..9b1672a8c
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/player-styles.component.ts
@@ -0,0 +1,15 @@
1import { Component, ViewEncapsulation } from '@angular/core'
2
3/*
4* Allows to lazy load global player styles in the watch component
5*/
6
7@Component({
8 selector: 'my-player-styles',
9 template: '',
10 styleUrls: [ './player-styles.component.scss' ],
11 // tslint:disable:use-component-view-encapsulation
12 encapsulation: ViewEncapsulation.None
13})
14export class PlayerStylesComponent {
15}
diff --git a/client/src/app/+videos/+video-watch/recommendations/recent-videos-recommendation.service.ts b/client/src/app/+videos/+video-watch/recommendations/recent-videos-recommendation.service.ts
index 29fa268f4..2a851f13a 100644
--- a/client/src/app/+videos/+video-watch/recommendations/recent-videos-recommendation.service.ts
+++ b/client/src/app/+videos/+video-watch/recommendations/recent-videos-recommendation.service.ts
@@ -61,7 +61,7 @@ export class RecentVideosRecommendationService implements RecommendationService
61 componentPagination: pagination, 61 componentPagination: pagination,
62 advancedSearch: new AdvancedSearch({ 62 advancedSearch: new AdvancedSearch({
63 tagsOneOf: recommendation.tags.join(','), 63 tagsOneOf: recommendation.tags.join(','),
64 sort: '-createdAt', 64 sort: '-publishedAt',
65 searchTarget: 'local', 65 searchTarget: 'local',
66 nsfw: user.nsfwPolicy 66 nsfw: user.nsfwPolicy
67 ? this.videos.nsfwPolicyToParam(user.nsfwPolicy) 67 ? this.videos.nsfwPolicyToParam(user.nsfwPolicy)
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..a02373f2d 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,26 +1,21 @@
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 <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle"> 6
7 <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" /> 7 <my-account-avatar [account]="video.account" [title]="accountLinkTitle" [internalHref]="[ '/accounts', video.byAccount ]"></my-account-avatar>
8 </a> 8</ng-container>
9 </ng-container>
10 9
11 <ng-container *ngIf="!isChannelAvatarNull() && genericChannel"> 10 <ng-container *ngIf="!isChannelAvatarNull() && genericChannel">
12 <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle"> 11 <my-account-avatar [account]="video.account" [title]="accountLinkTitle" [internalHref]="[ '/accounts', video.byAccount ]"></my-account-avatar>
13 <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" />
14 </a>
15 12
16 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> 13 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
17 <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" /> 14 <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" class="channel-avatar" />
18 </a> 15 </a>
19 </ng-container> 16 </ng-container>
20 17
21 <ng-container *ngIf="isChannelAvatarNull()"> 18 <ng-container *ngIf="isChannelAvatarNull()">
22 <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle"> 19 <my-account-avatar [account]="video.account" [title]="accountLinkTitle" [internalHref]="[ '/accounts', video.byAccount ]"></my-account-avatar>
23 <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" />
24 </a>
25 </ng-container> 20 </ng-container>
26</div> 21</div>
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..eadb2148a 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch.component.html
@@ -16,6 +16,8 @@
16 [playlist]="playlist" class="playlist" 16 [playlist]="playlist" class="playlist"
17 (videoFound)="onPlaylistVideoFound($event)" 17 (videoFound)="onPlaylistVideoFound($event)"
18 ></my-video-watch-playlist> 18 ></my-video-watch-playlist>
19
20 <my-plugin-placeholder pluginId="player-next"></my-plugin-placeholder>
19 </div> 21 </div>
20 22
21 <div class="row"> 23 <div class="row">
@@ -142,7 +144,7 @@
142 <ng-container *ngIf="isUserLoggedIn()"> 144 <ng-container *ngIf="isUserLoggedIn()">
143 <my-video-actions-dropdown 145 <my-video-actions-dropdown
144 placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" [videoCaptions]="videoCaptions" 146 placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" [videoCaptions]="videoCaptions"
145 [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()" (modalOpened)="onModalOpened()" 147 [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()"
146 ></my-video-actions-dropdown> 148 ></my-video-actions-dropdown>
147 </ng-container> 149 </ng-container>
148 </div> 150 </div>
@@ -230,8 +232,8 @@
230 </div> 232 </div>
231 233
232 <div *ngIf="video.isLocal === false" class="video-attribute"> 234 <div *ngIf="video.isLocal === false" class="video-attribute">
233 <span i18n class="video-attribute-label">Origin instance</span> 235 <span i18n class="video-attribute-label">Origin</span>
234 <a class="video-attribute-value" target="_blank" rel="noopener noreferrer" [href]="video.originInstanceUrl">{{ video.originInstanceHost }}</a> 236 <a class="video-attribute-value" target="_blank" rel="noopener noreferrer" [href]="getVideoUrl()">{{ video.originInstanceHost }}</a>
235 </div> 237 </div>
236 238
237 <div *ngIf="!!video.originallyPublishedAt" class="video-attribute"> 239 <div *ngIf="!!video.originallyPublishedAt" class="video-attribute">
@@ -289,6 +291,7 @@
289 </div> 291 </div>
290 292
291 <my-recommended-videos 293 <my-recommended-videos
294 [displayAsRow]="displayOtherVideosAsRow()"
292 [inputRecommendation]="{ uuid: video.uuid, tags: video.tags }" 295 [inputRecommendation]="{ uuid: video.uuid, tags: video.tags }"
293 [playlist]="playlist" 296 [playlist]="playlist"
294 (gotRecommendations)="onRecommendations($event)" 297 (gotRecommendations)="onRecommendations($event)"
@@ -313,6 +316,8 @@
313</div> 316</div>
314 317
315<ng-container *ngIf="video !== null"> 318<ng-container *ngIf="video !== null">
316 <my-video-support #videoSupportModal [video]="video"></my-video-support> 319 <my-support-modal #supportModal [video]="video"></my-support-modal>
317 <my-video-share #videoShareModal [video]="video" [videoCaptions]="videoCaptions" [playlist]="playlist"></my-video-share> 320 <my-video-share #videoShareModal [video]="video" [videoCaptions]="videoCaptions" [playlist]="playlist"></my-video-share>
318</ng-container> 321</ng-container>
322
323<my-player-styles></my-player-styles>
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..e8ad10a11 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 {
@@ -212,11 +206,6 @@ $video-info-margin-left: 44px;
212 } 206 }
213 } 207 }
214 208
215 my-feed {
216 margin-left: 5px;
217 margin-top: 1px;
218 }
219
220 .video-actions-rates { 209 .video-actions-rates {
221 margin: 0 0 10px 0; 210 margin: 0 0 10px 0;
222 align-items: start; 211 align-items: start;
@@ -413,37 +402,12 @@ $video-info-margin-left: 44px;
413 } 402 }
414 } 403 }
415 } 404 }
405}
416 406
417 ::ng-deep .other-videos { 407my-recommended-videos {
418 padding-left: 15px; 408 display: block;
419 min-width: $video-miniature-width; 409 padding-left: 15px;
420 410 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} 411}
448 412
449my-video-comments { 413my-video-comments {
@@ -537,6 +501,7 @@ my-video-comments {
537 } 501 }
538} 502}
539 503
504// Use the same breakpoint than in the typescript component to display the other video miniatures as row
540@media screen and (max-width: 1100px) { 505@media screen and (max-width: 1100px) {
541 #video-wrapper { 506 #video-wrapper {
542 flex-direction: column; 507 flex-direction: column;
@@ -549,15 +514,10 @@ my-video-comments {
549 514
550 .video-bottom { 515 .video-bottom {
551 flex-direction: column; 516 flex-direction: column;
517 }
552 518
553 ::ng-deep .other-videos { 519 my-recommended-videos {
554 padding-left: 0 !important; 520 padding-left: 0;
555
556 ::ng-deep .video-miniature {
557 flex-direction: row;
558 width: auto;
559 }
560 }
561 } 521 }
562} 522}
563 523
@@ -579,10 +539,6 @@ my-video-comments {
579 } 539 }
580 } 540 }
581 541
582 ::ng-deep .other-videos .video-miniature {
583 flex-direction: column;
584 }
585
586 .privacy-concerns { 542 .privacy-concerns {
587 width: 100%; 543 width: 100%;
588 } 544 }
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..366e9bb57 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
@@ -301,6 +295,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
301 return this.authService.isLoggedIn() 295 return this.authService.isLoggedIn()
302 } 296 }
303 297
298 getVideoUrl () {
299 if (!this.video.url) {
300 return this.video.originInstanceUrl + VideoDetails.buildClientUrl(this.video.uuid)
301 }
302 return this.video.url
303 }
304
304 getVideoTags () { 305 getVideoTags () {
305 if (!this.video || Array.isArray(this.video.tags) === false) return [] 306 if (!this.video || Array.isArray(this.video.tags) === false) return []
306 307
@@ -316,10 +317,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
316 } 317 }
317 } 318 }
318 319
319 onModalOpened () {
320 this.pausePlayer()
321 }
322
323 onVideoRemoved () { 320 onVideoRemoved () {
324 this.redirectService.redirectToHomepage() 321 this.redirectService.redirectToHomepage()
325 } 322 }
@@ -396,6 +393,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
396 this.loadVideo(videoId) 393 this.loadVideo(videoId)
397 } 394 }
398 395
396 displayOtherVideosAsRow () {
397 // Use the same value as in the SASS file
398 return this.screenService.getWindowInnerWidth() <= 1100
399 }
400
399 private loadVideo (videoId: string) { 401 private loadVideo (videoId: string) {
400 // Video did not change 402 // Video did not change
401 if (this.video && this.video.uuid === videoId) return 403 if (this.video && this.video.uuid === videoId) return
@@ -570,7 +572,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
570 this.setOpenGraphTags() 572 this.setOpenGraphTags()
571 this.checkUserRating() 573 this.checkUserRating()
572 574
573 this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', { videojs }) 575 const hookOptions = {
576 videojs,
577 video: this.video,
578 playlist: this.playlist
579 }
580 this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions)
574 } 581 }
575 582
576 private async buildPlayer (urlOptions: URLOptions) { 583 private async buildPlayer (urlOptions: URLOptions) {
@@ -768,9 +775,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
768 const getStartTime = () => { 775 const getStartTime = () => {
769 const byUrl = urlOptions.startTime !== undefined 776 const byUrl = urlOptions.startTime !== undefined
770 const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined) 777 const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined)
778 const byLocalStorage = getStoredVideoWatchHistory(video.uuid)
771 779
772 if (byUrl) return timeToInt(urlOptions.startTime) 780 if (byUrl) return timeToInt(urlOptions.startTime)
773 if (byHistory) return video.userHistory.currentTime 781 if (byHistory) return video.userHistory.currentTime
782 if (byLocalStorage) return byLocalStorage.duration
774 783
775 return 0 784 return 0
776 } 785 }
@@ -815,6 +824,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
815 ? this.videoService.getVideoViewUrl(video.uuid) 824 ? this.videoService.getVideoViewUrl(video.uuid)
816 : null, 825 : null,
817 embedUrl: video.embedUrl, 826 embedUrl: video.embedUrl,
827 embedTitle: video.name,
818 828
819 isLive: video.isLive, 829 isLive: video.isLive,
820 830
@@ -827,7 +837,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
827 837
828 serverUrl: environment.apiUrl, 838 serverUrl: environment.apiUrl,
829 839
830 videoCaptions: playerCaptions 840 videoCaptions: playerCaptions,
841
842 videoUUID: video.uuid
831 }, 843 },
832 844
833 webtorrent: { 845 webtorrent: {
@@ -867,24 +879,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
867 return { playerMode: mode, playerOptions: options } 879 return { playerMode: mode, playerOptions: options }
868 } 880 }
869 881
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) { 882 private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) {
889 if (!this.liveVideosSub) { 883 if (!this.liveVideosSub) {
890 this.liveVideosSub = this.buildLiveEventsSubscription() 884 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..cf6afd852 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,12 +14,14 @@ 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 { PlayerStylesComponent } from './player-styles.component'
17import { RecommendationsModule } from './recommendations/recommendations.module' 18import { RecommendationsModule } from './recommendations/recommendations.module'
18import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive' 19import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive'
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'
23import { SharedAccountAvatarModule } from '../../shared/shared-account-avatar/shared-account-avatar.module'
24import { VideoAvatarChannelComponent } from './video-avatar-channel.component'
22 25
23@NgModule({ 26@NgModule({
24 imports: [ 27 imports: [
@@ -34,20 +37,25 @@ import { VideoWatchComponent } from './video-watch.component'
34 SharedGlobalIconModule, 37 SharedGlobalIconModule,
35 SharedVideoCommentModule, 38 SharedVideoCommentModule,
36 SharedShareModal, 39 SharedShareModal,
37 SharedVideoModule 40 SharedVideoModule,
41 SharedSupportModal,
42 SharedAccountAvatarModule
38 ], 43 ],
39 44
40 declarations: [ 45 declarations: [
41 VideoWatchComponent, 46 VideoWatchComponent,
42 VideoWatchPlaylistComponent, 47 VideoWatchPlaylistComponent,
43 48
44 VideoSupportComponent,
45 VideoCommentsComponent, 49 VideoCommentsComponent,
46 VideoCommentAddComponent, 50 VideoCommentAddComponent,
47 VideoCommentComponent, 51 VideoCommentComponent,
52 VideoAvatarChannelComponent,
53
54 VideoAvatarChannelComponent,
48 55
49 TimestampRouteTransformerDirective, 56 TimestampRouteTransformerDirective,
50 TimestampRouteTransformerDirective 57
58 PlayerStylesComponent
51 ], 59 ],
52 60
53 exports: [ 61 exports: [
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..e7d05369b 100644
--- a/client/src/app/app.component.scss
+++ b/client/src/app/app.component.scss
@@ -3,8 +3,6 @@
3@import '~bootstrap/scss/functions'; 3@import '~bootstrap/scss/functions';
4@import '~bootstrap/scss/variables'; 4@import '~bootstrap/scss/variables';
5 5
6$assets-path: '../assets';
7
8.peertube-container { 6.peertube-container {
9 padding-bottom: 20px; 7 padding-bottom: 20px;
10} 8}
@@ -28,68 +26,64 @@ $assets-path: '../assets';
28 z-index: z(header); 26 z-index: z(header);
29 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16); 27 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16);
30 display: flex; 28 display: flex;
29}
31 30
32 .top-left-block { 31.top-left-block {
33 z-index: 1; 32 z-index: 1;
34 height: $header-height; 33 height: $header-height;
35 display: flex; 34 display: flex;
36 align-items: center; 35 align-items: center;
37 min-width: 0; 36 min-width: 0;
38 37
39 .icon { 38 .icon {
40 @include icon(24px); 39 @include icon(24px);
41 40 }
42 &.icon-menu {
43 background-color: pvar(--mainForegroundColor);
44 mask-image: url('#{$assets-path}/images/misc/menu.svg');
45 margin: 0 18px 0 20px;
46 }
47 }
48 41
49 .peertube-title { 42 .icon-menu {
50 @include disable-default-a-behaviour; 43 background-color: pvar(--mainForegroundColor);
44 mask-image: url('../assets/images/misc/menu.svg');
45 margin: 0 18px 0 20px;
51 46
52 font-size: 20px; 47 @media screen and (max-width: $mobile-view) {
53 font-weight: $font-bold; 48 margin: 0 10px;
54 color: inherit !important; 49 }
55 display: flex; 50 }
56 align-items: center; 51}
57 overflow: hidden;
58 52
59 .instance-name { 53.header-right {
60 @include ellipsis; 54 height: $header-height;
55 display: flex;
56 align-items: center;
57 justify-content: flex-end;
58 white-space: nowrap;
59 flex: 1;
60}
61 61
62 width: 100%; 62.peertube-title {
63 } 63 @include disable-default-a-behaviour;
64 64
65 .icon.icon-logo { 65 font-size: 20px;
66 display: inline-block; 66 font-weight: $font-bold;
67 width: 23px; 67 color: inherit !important;
68 height: 24px; 68 display: flex;
69 margin-right: .5rem; 69 align-items: center;
70 } 70 overflow: hidden;
71 }
72 71
73 @media screen and (max-width: $mobile-view) { 72 .instance-name {
74 width: 70px; 73 @include ellipsis;
75 74
76 .peertube-title { 75 width: 100%;
77 display: none; 76 }
78 }
79 }
80 77
81 @media screen and (max-width: 350px) { 78 .icon.icon-logo {
82 flex: auto; 79 display: inline-block;
83 } 80 width: 23px;
81 height: 24px;
82 margin-right: .5rem;
84 } 83 }
85 84
86 .header-right { 85 @media screen and (max-width: $mobile-view) {
87 height: $header-height; 86 display: none;
88 display: flex;
89 align-items: center;
90 justify-content: flex-end;
91 white-space: nowrap;
92 flex: 1;
93 } 87 }
94} 88}
95 89
@@ -106,18 +100,9 @@ $assets-path: '../assets';
106 justify-self: center; 100 justify-self: center;
107 align-self: center; 101 align-self: center;
108 cursor: pointer; 102 cursor: pointer;
109
110 width: 20px; 103 width: 20px;
111 } 104 }
112 105
113 @each $color, $value in $theme-colors {
114 &.alert-#{$color} {
115 my-global-icon {
116 @include apply-svg-color(theme-color-level($color, $alert-color-level));
117 }
118 }
119 }
120
121 ::ng-deep { 106 ::ng-deep {
122 p { 107 p {
123 font-size: 16px; 108 font-size: 16px;
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index f790a6848..41c59cc86 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -24,6 +24,7 @@ import { SharedGlobalIconModule } from './shared/shared-icons'
24import { SharedInstanceModule } from './shared/shared-instance' 24import { SharedInstanceModule } from './shared/shared-instance'
25import { SharedMainModule } from './shared/shared-main' 25import { SharedMainModule } from './shared/shared-main'
26import { SharedUserInterfaceSettingsModule } from './shared/shared-user-settings' 26import { SharedUserInterfaceSettingsModule } from './shared/shared-user-settings'
27import { SharedAccountAvatarModule } from './shared/shared-account-avatar/shared-account-avatar.module'
27 28
28registerLocaleData(localeOc, 'oc') 29registerLocaleData(localeOc, 'oc')
29 30
@@ -59,6 +60,7 @@ registerLocaleData(localeOc, 'oc')
59 SharedUserInterfaceSettingsModule, 60 SharedUserInterfaceSettingsModule,
60 SharedGlobalIconModule, 61 SharedGlobalIconModule,
61 SharedInstanceModule, 62 SharedInstanceModule,
63 SharedAccountAvatarModule,
62 64
63 MetaModule.forRoot({ 65 MetaModule.forRoot({
64 provide: MetaLoader, 66 provide: MetaLoader,
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index 2392a234c..3152a7003 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -5,8 +5,7 @@ import { CommonModule } from '@angular/common'
5import { NgModule, Optional, SkipSelf } from '@angular/core' 5import { NgModule, Optional, SkipSelf } from '@angular/core'
6import { BrowserAnimationsModule } from '@angular/platform-browser/animations' 6import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
7import { PeerTubeSocket } from '@app/core/notification/peertube-socket.service' 7import { PeerTubeSocket } from '@app/core/notification/peertube-socket.service'
8import { HooksService } from '@app/core/plugins/hooks.service' 8import { HooksService, PluginService } from '@app/core/plugins'
9import { PluginService } from '@app/core/plugins/plugin.service'
10import { AuthService } from './auth' 9import { AuthService } from './auth'
11import { ConfirmService } from './confirm' 10import { ConfirmService } from './confirm'
12import { CheatSheetComponent } from './hotkeys' 11import { CheatSheetComponent } from './hotkeys'
@@ -15,7 +14,7 @@ import { throwIfAlreadyLoaded } from './module-import-guard'
15import { Notifier } from './notification' 14import { Notifier } from './notification'
16import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer' 15import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
17import { RestExtractor, RestService } from './rest' 16import { RestExtractor, RestService } from './rest'
18import { LoginGuard, RedirectService, UserRightGuard, UnloggedGuard } from './routing' 17import { LoginGuard, RedirectService, UnloggedGuard, UserRightGuard } from './routing'
19import { CanDeactivateGuard } from './routing/can-deactivate-guard.service' 18import { CanDeactivateGuard } from './routing/can-deactivate-guard.service'
20import { ServerConfigResolver } from './routing/server-config-resolver.service' 19import { ServerConfigResolver } from './routing/server-config-resolver.service'
21import { ScopedTokensService } from './scoped-tokens' 20import { ScopedTokensService } from './scoped-tokens'
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..1243bac67 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -19,6 +19,7 @@ import {
19 PluginTranslation, 19 PluginTranslation,
20 PluginType, 20 PluginType,
21 PublicServerSetting, 21 PublicServerSetting,
22 RegisterClientSettingsScript,
22 ServerConfigPlugin 23 ServerConfigPlugin
23} from '@shared/models' 24} from '@shared/models'
24import { environment } from '../../../environments/environment' 25import { environment } from '../../../environments/environment'
@@ -46,7 +47,10 @@ export class PluginService implements ClientHook {
46 customModal: CustomModalComponent 47 customModal: CustomModalComponent
47 48
48 private plugins: ServerConfigPlugin[] = [] 49 private plugins: ServerConfigPlugin[] = []
50 private helpers: { [ npmName: string ]: RegisterClientHelpers } = {}
51
49 private scopes: { [ scopeName: string ]: PluginInfo[] } = {} 52 private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
53
50 private loadedScripts: { [ script: string ]: boolean } = {} 54 private loadedScripts: { [ script: string ]: boolean } = {}
51 private loadedScopes: PluginClientScope[] = [] 55 private loadedScopes: PluginClientScope[] = []
52 private loadingScopes: { [id in PluginClientScope]?: boolean } = {} 56 private loadingScopes: { [id in PluginClientScope]?: boolean } = {}
@@ -55,6 +59,7 @@ export class PluginService implements ClientHook {
55 private formFields: FormFields = { 59 private formFields: FormFields = {
56 video: [] 60 video: []
57 } 61 }
62 private settingsScripts: { [ npmName: string ]: RegisterClientSettingsScript } = {}
58 63
59 constructor ( 64 constructor (
60 private authService: AuthService, 65 private authService: AuthService,
@@ -197,13 +202,33 @@ export class PluginService implements ClientHook {
197 return this.formFields.video.filter(f => f.videoFormOptions.type === type) 202 return this.formFields.video.filter(f => f.videoFormOptions.type === type)
198 } 203 }
199 204
205 getRegisteredSettingsScript (npmName: string) {
206 return this.settingsScripts[npmName]
207 }
208
209 translateBy (npmName: string, toTranslate: string) {
210 const helpers = this.helpers[npmName]
211 if (!helpers) {
212 console.error('Unknown helpers to translate %s from %s.', toTranslate, npmName)
213 return toTranslate
214 }
215
216 return helpers.translate(toTranslate)
217 }
218
200 private loadPlugin (pluginInfo: PluginInfo) { 219 private loadPlugin (pluginInfo: PluginInfo) {
201 return this.zone.runOutsideAngular(() => { 220 return this.zone.runOutsideAngular(() => {
221 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
222
223 const helpers = this.buildPeerTubeHelpers(pluginInfo)
224 this.helpers[npmName] = helpers
225
202 return loadPlugin({ 226 return loadPlugin({
203 hooks: this.hooks, 227 hooks: this.hooks,
204 formFields: this.formFields, 228 formFields: this.formFields,
229 onSettingsScripts: options => this.settingsScripts[npmName] = options,
205 pluginInfo, 230 pluginInfo,
206 peertubeHelpersFactory: pluginInfo => this.buildPeerTubeHelpers(pluginInfo) 231 peertubeHelpersFactory: () => helpers
207 }) 232 })
208 }) 233 })
209 } 234 }
@@ -235,6 +260,12 @@ export class PluginService implements ClientHook {
235 .toPromise() 260 .toPromise()
236 }, 261 },
237 262
263 getServerConfig: () => {
264 return this.server.getConfig()
265 .pipe(catchError(res => this.restExtractor.handleError(res)))
266 .toPromise()
267 },
268
238 isLoggedIn: () => { 269 isLoggedIn: () => {
239 return this.authService.isLoggedIn() 270 return this.authService.isLoggedIn()
240 }, 271 },
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..7d03e1c40 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,
@@ -111,12 +111,6 @@ export class User implements UserServerModel {
111 } 111 }
112 } 112 }
113 113
114 get accountAvatarUrl () {
115 if (!this.account) return ''
116
117 return this.account.avatarUrl
118 }
119
120 hasRight (right: UserRight) { 114 hasRight (right: UserRight) {
121 return hasUserRight(this.role, right) 115 return hasUserRight(this.role, right)
122 } 116 }
@@ -131,7 +125,7 @@ export class User implements UserServerModel {
131 } 125 }
132 } 126 }
133 127
134 updateAccountAvatar (newAccountAvatar?: Avatar) { 128 updateAccountAvatar (newAccountAvatar?: ActorImage) {
135 if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar) 129 if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar)
136 else this.account.resetAvatar() 130 else this.account.resetAvatar()
137 } 131 }
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..c754a99d1 100644
--- a/client/src/app/header/search-typeahead.component.scss
+++ b/client/src/app/header/search-typeahead.component.scss
@@ -5,6 +5,7 @@
5 5
6#search-video { 6#search-video {
7 @include peertube-input-text($search-input-width); 7 @include peertube-input-text($search-input-width);
8
8 padding-left: 10px; 9 padding-left: 10px;
9 padding-right: 40px; // For the search icon 10 padding-right: 40px; // For the search icon
10 font-size: 14px; 11 font-size: 14px;
@@ -14,7 +15,7 @@
14 } 15 }
15} 16}
16 17
17.icon.icon-search { 18.icon-search {
18 @include icon(25px); 19 @include icon(25px);
19 height: 18px; 20 height: 18px;
20 21
@@ -86,7 +87,7 @@ li.suggestion {
86 flex: 1; 87 flex: 1;
87 88
88 input { 89 input {
89 width: unset; 90 width: 70px;
90 } 91 }
91 } 92 }
92 93
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index 9aa397edd..df5c7971d 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -5,7 +5,7 @@
5 <div> 5 <div>
6 <div class="logged-in-more" ngbDropdown #dropdown="ngbDropdown" placement="bottom-left" [container]="dropdownContainer" (openChange)="onDropdownOpenChange($event)" autoClose="outside"> 6 <div class="logged-in-more" ngbDropdown #dropdown="ngbDropdown" placement="bottom-left" [container]="dropdownContainer" (openChange)="onDropdownOpenChange($event)" autoClose="outside">
7 <div ngbDropdownToggle> 7 <div ngbDropdownToggle>
8 <img [src]="user.accountAvatarUrl" alt="Avatar" /> 8 <my-account-avatar [account]="user.account" size="34"></my-account-avatar>
9 <div class="logged-in-info"> 9 <div class="logged-in-info">
10 <div class="logged-in-display-name">{{ user.account?.displayName }}</div> 10 <div class="logged-in-display-name">{{ user.account?.displayName }}</div>
11 11
@@ -40,9 +40,10 @@
40 40
41 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item settings-sensitive" routerLink="/my-account/settings" 41 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item settings-sensitive" routerLink="/my-account/settings"
42 fragment="video-sensitive-content-policy" #settingsSensitiveContentPolicy 42 fragment="video-sensitive-content-policy" #settingsSensitiveContentPolicy
43 (click)="onActiveLinkScrollToAnchor(settingsSensitiveContentPolicy)"> 43 (click)="onActiveLinkScrollToAnchor(settingsSensitiveContentPolicy)"
44 <my-global-icon class="hover-display-toggle" [ngClass]="{ 'not-displayed': user.nsfwPolicy === 'display' }" iconName="sensitive" aria-hidden="true"></my-global-icon> 44 >
45 <my-global-icon class="hover-display-toggle" [ngClass]="{ 'not-displayed': user.nsfwPolicy !== 'display' }" iconName="unsensitive" aria-hidden="true"></my-global-icon> 45 <my-global-icon class="hover-display-toggle" [hidden]="user.nsfwPolicy === 'display'" iconName="sensitive" aria-hidden="true"></my-global-icon>
46 <my-global-icon class="hover-display-toggle" [hidden]="user.nsfwPolicy !== 'display'" iconName="unsensitive" aria-hidden="true"></my-global-icon>
46 <span i18n>Sensitive:</span> 47 <span i18n>Sensitive:</span>
47 <span class="ml-auto text-muted">{{ nsfwPolicy }}</span> 48 <span class="ml-auto text-muted">{{ nsfwPolicy }}</span>
48 </a> 49 </a>
@@ -72,17 +73,17 @@
72 </div> 73 </div>
73 74
74 <div class="logged-in-menu"> 75 <div class="logged-in-menu">
75 <a routerLink="/my-account" routerLinkActive="active" #settingsLink (click)="onActiveLinkScrollToAnchor(settingsLink)"> 76 <a class="menu-link" routerLink="/my-account" routerLinkActive="active" #settingsLink (click)="onActiveLinkScrollToAnchor(settingsLink)">
76 <my-global-icon iconName="user" aria-hidden="true"></my-global-icon> 77 <my-global-icon iconName="user" aria-hidden="true"></my-global-icon>
77 <ng-container i18n>My account</ng-container> 78 <ng-container i18n>My account</ng-container>
78 </a> 79 </a>
79 80
80 <a routerLink="/my-library" routerLinkActive="active" #libraryLink (click)="onActiveLinkScrollToAnchor(libraryLink)"> 81 <a class="menu-link" routerLink="/my-library" routerLinkActive="active" #libraryLink (click)="onActiveLinkScrollToAnchor(libraryLink)">
81 <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon> 82 <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon>
82 <ng-container i18n>My library</ng-container> 83 <ng-container i18n>My library</ng-container>
83 </a> 84 </a>
84 85
85 <a *ngIf="userHasAdminAccess" [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active"> 86 <a class="menu-link" *ngIf="userHasAdminAccess" [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
86 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> 87 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
87 <ng-container i18n>Administration</ng-container> 88 <ng-container i18n>Administration</ng-container>
88 </a> 89 </a>
@@ -90,29 +91,29 @@
90 </div> 91 </div>
91 92
92 <div *ngIf="!isLoggedIn" class="login-buttons-block"> 93 <div *ngIf="!isLoggedIn" class="login-buttons-block">
93 <a i18n routerLink="/login" class="login-button">Login</a> 94 <a i18n routerLink="/login" class="peertube-button-link orange-button">Login</a>
94 <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="create-account-button">Create an account</a> 95 <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link">Create an account</a>
95 </div> 96 </div>
96 97
97 <div *ngIf="isLoggedIn" class="in-my-library"> 98 <div *ngIf="isLoggedIn" class="in-my-library">
98 <div i18n class="block-title">IN MY LIBRARY</div> 99 <div i18n class="block-title">IN MY LIBRARY</div>
99 100
100 <a *ngIf="user.canSeeVideosLink" routerLink="/my-library/videos" routerLinkActive="active"> 101 <a *ngIf="user.canSeeVideosLink" class="menu-link" routerLink="/my-library/videos" routerLinkActive="active">
101 <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon> 102 <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon>
102 <ng-container i18n>Videos</ng-container> 103 <ng-container i18n>Videos</ng-container>
103 </a> 104 </a>
104 105
105 <a routerLink="/my-library/video-playlists" routerLinkActive="active"> 106 <a class="menu-link" routerLink="/my-library/video-playlists" routerLinkActive="active">
106 <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> 107 <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon>
107 <ng-container i18n>Playlists</ng-container> 108 <ng-container i18n>Playlists</ng-container>
108 </a> 109 </a>
109 110
110 <a routerLink="/videos/subscriptions" routerLinkActive="active"> 111 <a class="menu-link" routerLink="/videos/subscriptions" routerLinkActive="active">
111 <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon> 112 <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon>
112 <ng-container i18n>Subscriptions</ng-container> 113 <ng-container i18n>Subscriptions</ng-container>
113 </a> 114 </a>
114 115
115 <a routerLink="/my-library/history/videos" routerLinkActive="active"> 116 <a class="menu-link" routerLink="/my-library/history/videos" routerLinkActive="active">
116 <my-global-icon iconName="history" aria-hidden="true"></my-global-icon> 117 <my-global-icon iconName="history" aria-hidden="true"></my-global-icon>
117 <ng-container i18n>History</ng-container> 118 <ng-container i18n>History</ng-container>
118 </a> 119 </a>
@@ -122,22 +123,22 @@
122 <div class="on-instance"> 123 <div class="on-instance">
123 <div i18n class="block-title">ON {{instanceName}}</div> 124 <div i18n class="block-title">ON {{instanceName}}</div>
124 125
125 <a routerLink="/videos/overview" routerLinkActive="active"> 126 <a class="menu-link" routerLink="/videos/overview" routerLinkActive="active">
126 <my-global-icon iconName="globe" aria-hidden="true"></my-global-icon> 127 <my-global-icon iconName="globe" aria-hidden="true"></my-global-icon>
127 <ng-container i18n>Discover</ng-container> 128 <ng-container i18n>Discover</ng-container>
128 </a> 129 </a>
129 130
130 <a routerLink="/videos/trending" routerLinkActive="active"> 131 <a class="menu-link" routerLink="/videos/trending" routerLinkActive="active">
131 <my-global-icon iconName="trending" aria-hidden="true"></my-global-icon> 132 <my-global-icon iconName="trending" aria-hidden="true"></my-global-icon>
132 <ng-container i18n>Trending</ng-container> 133 <ng-container i18n>Trending</ng-container>
133 </a> 134 </a>
134 135
135 <a routerLink="/videos/recently-added" routerLinkActive="active"> 136 <a class="menu-link" routerLink="/videos/recently-added" routerLinkActive="active">
136 <my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon> 137 <my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon>
137 <ng-container i18n>Recently added</ng-container> 138 <ng-container i18n>Recently added</ng-container>
138 </a> 139 </a>
139 140
140 <a routerLink="/videos/local" routerLinkActive="active"> 141 <a class="menu-link" routerLink="/videos/local" routerLinkActive="active">
141 <my-global-icon iconName="home" aria-hidden="true"></my-global-icon> 142 <my-global-icon iconName="home" aria-hidden="true"></my-global-icon>
142 <ng-container i18n>Local videos</ng-container> 143 <ng-container i18n>Local videos</ng-container>
143 </a> 144 </a>
@@ -146,18 +147,18 @@
146 147
147 <div class="footer"> 148 <div class="footer">
148 <div class="footer-block"> 149 <div class="footer-block">
149 <a *ngIf="!isLoggedIn" (click)="openQuickSettings()"> 150 <a *ngIf="!isLoggedIn" class="menu-link" (click)="openQuickSettings()">
150 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> 151 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
151 <ng-container i18n>My settings</ng-container> 152 <ng-container i18n>My settings</ng-container>
152 </a> 153 </a>
153 154
154 <a routerLink="/about" routerLinkActive="active"> 155 <a class="menu-link" routerLink="/about" routerLinkActive="active">
155 <my-global-icon iconName="help" aria-hidden="true"></my-global-icon> 156 <my-global-icon iconName="help" aria-hidden="true"></my-global-icon>
156 <ng-container i18n>About</ng-container> 157 <ng-container i18n>About</ng-container>
157 </a> 158 </a>
158 </div> 159 </div>
159 160
160 <div class="bottom-links"> 161 <div class="footer-bottom">
161 162
162 <div class="footer-links"> 163 <div class="footer-links">
163 <div *ngIf="isLoggedIn === false"> 164 <div *ngIf="isLoggedIn === false">
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index 2ea66e57d..00d1a1f69 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -3,8 +3,11 @@
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;
7
8.menu-link {
9 @include disable-default-a-behaviour;
6 10
7@mixin menu-link {
8 display: flex; 11 display: flex;
9 align-items: center; 12 align-items: center;
10 padding-left: $menu-lateral-padding; 13 padding-left: $menu-lateral-padding;
@@ -90,169 +93,158 @@ menu {
90 display: flex; 93 display: flex;
91 align-items: center; 94 align-items: center;
92 justify-content: left; 95 justify-content: left;
96 }
97}
93 98
94 .logged-in-more { 99my-notification {
95 $main-radius: 25px; 100 margin-left: auto;
101 margin-right: 15px;
102}
96 103
97 flex: 1; 104.logged-in-more {
98 margin-left: 13px; 105 @mixin display-hints($is-mobile: false) {
99 border-radius: $main-radius; 106 background-color: rgba(255, 255, 255, 0.15);
100 transition: all .1s ease-in-out;
101 cursor: pointer;
102 107
103 *, & { 108 @if $is-mobile {
104 line-height: 1; 109 .dropdown-toggle-indicator {
110 display: inherit !important;
105 } 111 }
106 112 .dropdown-toggle:first-child {
107 &.show { 113 padding-right: 30px !important;
108 background-color: rgba(255, 255, 255, 0.20);
109 box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325);
110 } 114 }
115 }
116 }
111 117
112 @mixin display-hints($is-mobile: false) { 118 $main-radius: 25px;
113 background-color: rgba(255, 255, 255, 0.15);
114
115 @if $is-mobile {
116 .dropdown-toggle-indicator {
117 display: inherit !important;
118 }
119 .dropdown-toggle:first-child {
120 padding-right: 30px !important;
121 }
122 }
123 }
124 119
125 &:hover { 120 flex: 1;
126 @include display-hints; 121 margin-left: 13px;
127 } 122 border-radius: $main-radius;
123 transition: all .1s ease-in-out;
124 cursor: pointer;
125 line-height: 1;
128 126
129 /* smartphones and touchscreens */ 127 &.show {
130 @media (hover: none) and (pointer: coarse) { 128 background-color: rgba(255, 255, 255, 0.20);
131 @include display-hints($is-mobile: true); 129 box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325);
130 }
132 131
133 /* fill space when on mobile */ 132 &:hover {
134 max-width: calc(100% - 80px); 133 @include display-hints;
135 .dropdown-toggle { 134 }
136 max-width: 100%;
137 }
138 .logged-in-info {
139 max-width: calc(100% - 45px) !important;
140 }
141 135
142 } 136 /* smartphones and touchscreens */
137 @media (hover: none) and (pointer: coarse) {
138 @include display-hints($is-mobile: true);
143 139
144 .dropdown-toggle-indicator { 140 /* fill space when on mobile */
145 position: relative; 141 max-width: calc(100% - 80px);
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 142
158 .dropdown-toggle { 143 .dropdown-toggle {
159 &::after { 144 max-width: 100%;
160 border: none; 145 }
161 }
162 }
163 146
164 .dropdown-toggle:first-child { 147 .logged-in-info {
165 display: flex; 148 max-width: calc(100% - 45px) !important;
166 align-items: center; 149 }
167 padding: 5px 7px; 150 }
168 border-radius: $main-radius;
169 }
170 151
171 img { 152 .dropdown-toggle-indicator {
172 @include avatar(34px); 153 position: relative;
154 width: 0;
155 display: none;
173 156
174 margin-right: 10px; 157 span {
175 } 158 position: absolute;
159 right: -35px;
160 top: -8px;
161 color: grey;
162 width: $main-radius;
163 }
164 }
176 165
177 .logged-in-info { 166 .dropdown-toggle {
178 max-width: 105px; 167 &::after {
168 border: none;
169 }
170 }
179 171
180 flex-grow: 1; 172 .dropdown-toggle:first-child {
173 display: flex;
174 align-items: center;
175 padding: 5px 7px;
176 border-radius: $main-radius;
177 }
178}
181 179
182 .logged-in-display-name, 180my-account-avatar {
183 .logged-in-username { 181 margin-right: 10px;
184 @include ellipsis; 182}
185 }
186 183
187 .logged-in-display-name { 184.logged-in-info {
188 font-size: 16px; 185 max-width: 105px;
189 font-weight: $font-semibold;
190 color: pvar(--menuForegroundColor);
191 186
192 @include disable-default-a-behaviour; 187 flex-grow: 1;
193 } 188}
194 189
195 .logged-in-username { 190.logged-in-display-name,
196 font-size: 13px; 191.logged-in-username {
197 color: #C6C6C6; 192 @include ellipsis;
198 margin-top: 3px; 193}
199 }
200 }
201 }
202 194
203 my-notification { 195.logged-in-display-name {
204 margin-left: auto; 196 font-size: 16px;
205 margin-right: 15px; 197 font-weight: $font-semibold;
206 } 198 color: pvar(--menuForegroundColor);
207 }
208 199
209 .logged-in-menu { 200 @include disable-default-a-behaviour;
210 display: flex; 201}
211 flex-direction: column;
212 align-items: flex-start;
213 border-top: 1px solid var(--greyForegroundColor);
214 line-height: $line-height-normal;
215 202
216 a { 203.logged-in-username {
217 @include menu-link; 204 font-size: 13px;
218 @include disable-default-a-behaviour; 205 color: #C6C6C6;
206 margin-top: 3px;
207}
219 208
220 $icon-size: 13px; 209.logged-in-menu {
221 $additional-margin: ($menu-link-icon-size - $icon-size) / 2; 210 display: flex;
211 flex-direction: column;
212 align-items: flex-start;
213 border-top: 1px solid var(--greyForegroundColor);
214 line-height: $line-height-normal;
222 215
223 font-size: 14px; 216 a {
224 width: 100%; 217 $icon-size: 13px;
225 min-height: 35px; 218 $additional-margin: ($menu-link-icon-size - $icon-size) / 2;
226 219
227 my-global-icon { 220 font-size: 14px;
228 width: $icon-size; 221 width: 100%;
229 height: $icon-size; 222 min-height: 35px;
230 223
231 // Keep aligned with other icons 224 my-global-icon {
232 margin-left: $additional-margin; 225 width: $icon-size;
226 height: $icon-size;
233 227
234 &[iconName="channel"] { 228 // Keep aligned with other icons
235 margin-top: -2px; 229 margin-left: $additional-margin;
236 } 230 }
237 }
238 231
239 &.active, 232 &.active,
240 &:hover, 233 &:hover,
241 &:focus-visible { 234 &:focus-visible {
242 my-global-icon { 235 my-global-icon {
243 @include apply-svg-color(var(--menuForegroundColor)); 236 @include apply-svg-color(var(--menuForegroundColor));
244 }
245 } 237 }
238 }
246 239
247 &.active { 240 &.active {
248 $border-left-width: 4px; 241 $border-left-width: 4px;
249 242
250 font-weight: $font-semibold; 243 font-weight: $font-semibold;
251 border-left: $border-left-width solid var(--mainColor); 244 border-left: $border-left-width solid var(--mainColor);
252 245
253 my-global-icon { 246 my-global-icon {
254 margin-left: $additional-margin - $border-left-width; 247 margin-left: $additional-margin - $border-left-width;
255 }
256 } 248 }
257 } 249 }
258 } 250 }
@@ -261,27 +253,22 @@ menu {
261.login-buttons-block { 253.login-buttons-block {
262 margin: 30px 25px 35px 25px; 254 margin: 30px 25px 35px 25px;
263 255
264 .login-button { 256 > a {
265 @include peertube-button-link;
266 @include orange-button;
267
268 display: block; 257 display: block;
269 width: 100%; 258 width: 100%;
270 margin-bottom: 10px;
271 }
272 259
273 .create-account-button { 260 :not(:last-child) {
274 @include peertube-button-link; 261 margin-bottom: 10px;
275 262 }
276 display: block; 263 }
277 width: 100%; 264}
278 265
279 color: #fff; 266.create-account-button {
280 background-color: rgba(255, 255, 255, 0.25); 267 color: #fff;
268 background-color: rgba(255, 255, 255, 0.25);
281 269
282 &:hover { 270 &:hover {
283 background-color: rgba(255, 255, 255, 0.28); 271 background-color: rgba(255, 255, 255, 0.28);
284 }
285 } 272 }
286} 273}
287 274
@@ -291,92 +278,57 @@ menu {
291 margin-bottom: 15px; 278 margin-bottom: 15px;
292 279
293 .block-title { 280 .block-title {
281 @include ellipsis;
282
294 text-transform: uppercase; 283 text-transform: uppercase;
295 font-weight: $font-bold; // Bold 284 font-weight: $font-bold; // Bold
296 font-size: 13px; 285 font-size: 13px;
297 margin-bottom: 25px; 286 margin-bottom: 25px;
298 margin-left: 26px; 287 margin-left: 26px;
299
300 @include ellipsis;
301
302 margin-right: 30px; 288 margin-right: 30px;
303 } 289 }
304 290
305 a { 291 a {
306 @include menu-link;
307 @include disable-default-a-behaviour;
308
309 min-height: 40px; 292 min-height: 40px;
310
311 my-global-icon {
312 &[iconName="playlists"] {
313 height: 24px;
314 width: 24px;
315
316 margin-right: 16px;
317 }
318
319 &[iconName="videos"] {
320 position: relative;
321 right: -1px;
322 }
323 }
324 } 293 }
325} 294}
326 295
327.footer { 296.footer {
328 width: $menu-width; 297 width: $menu-width;
329 padding-bottom: 15px; 298 padding-bottom: 15px;
299}
300
301.footer-bottom {
302 display: flex;
303 flex-direction: column;
304 padding: 0 $menu-lateral-padding;
305}
330 306
331 .bottom-links { 307.footer-links {
308 &, > div {
332 display: flex; 309 display: flex;
333 flex-direction: column; 310 flex-wrap: wrap;
334 padding: 0 $menu-lateral-padding;
335 } 311 }
336 312
337 $footer-links-base-opacity: .8; 313 a,
338 314 span[role=button] {
339 .footer-links { 315 display: inline-block;
340 &, > div { 316 text-decoration: none;
341 display: flex; 317 color: pvar(--menuForegroundColor);
342 flex-wrap: wrap; 318 opacity: $footer-links-base-opacity;
343 } 319 white-space: nowrap;
344 320 font-size: 90%;
345 a, span[role=button] { 321 font-weight: 500;
346 display: inline-block; 322 line-height: 1.4rem;
347 text-decoration: none; 323 margin-right: 8px;
348 color: pvar(--menuForegroundColor);
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;
359 white-space: nowrap;
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 }
371 }
372 } 324 }
325}
373 326
374 .footer-copyleft small a { 327.footer-copyleft small a {
375 @include disable-default-a-behaviour; 328 @include disable-default-a-behaviour;
376 329
377 color: pvar(--menuForegroundColor); 330 color: pvar(--menuForegroundColor);
378 opacity: $footer-links-base-opacity - .2; 331 opacity: $footer-links-base-opacity - .2;
379 }
380} 332}
381 333
382.dropdown { 334.dropdown {
@@ -398,32 +350,13 @@ menu {
398 opacity: .4; 350 opacity: .4;
399 } 351 }
400 352
401 my-global-icon {
402 &[iconName="cog"],
403 &[iconName="sign-out"] {
404 position: relative;
405 right: -2px;
406 height: 20px;
407 width: 20px;
408 }
409 }
410
411 my-global-icon.not-displayed {
412 display: none;
413 }
414
415 &:hover { 353 &:hover {
416 my-global-icon.hover-display-toggle.not-displayed { 354 .hover-display-toggle {
417 display: inherit;
418 }
419 my-global-icon.hover-display-toggle {
420 display: none; 355 display: none;
421 } 356 }
422 357
423 &.settings-sensitive { 358 .hover-display-toggle[hidden] {
424 my-global-icon ::ng-deep svg { 359 display: inherit !important;
425 margin-top: 2px !important;
426 }
427 } 360 }
428 } 361 }
429} 362}
@@ -443,7 +376,8 @@ menu {
443 } 376 }
444 } 377 }
445 378
446 .top-menu, .footer { 379 .top-menu,
380 .footer {
447 width: 100% !important; 381 width: 100% !important;
448 } 382 }
449 383
@@ -451,9 +385,35 @@ menu {
451 width: calc(100vw - 30px); 385 width: calc(100vw - 30px);
452 } 386 }
453 387
454 .dropdown-item:hover, .dropdown-item:active { 388 .dropdown-item:hover,
389 .dropdown-item:active {
455 &.settings-sensitive my-global-icon ::ng-deep svg { 390 &.settings-sensitive my-global-icon ::ng-deep svg {
456 margin-top: 0px !important; 391 margin-top: 0px !important;
457 } 392 }
458 } 393 }
459} 394}
395
396my-global-icon {
397 &[iconName="playlists"] {
398 height: 24px;
399 width: 24px;
400
401 margin-right: 16px;
402 }
403
404 &[iconName="videos"] {
405 position: relative;
406 right: -1px;
407 }
408
409 &[iconName="channel"] {
410 margin-top: -2px;
411 }
412
413 &[iconName="sign-out"] {
414 position: relative;
415 right: -2px;
416 height: 20px;
417 width: 20px;
418 }
419}
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/confirm.component.html b/client/src/app/modal/confirm.component.html
index dbc8c23e3..f07501726 100644
--- a/client/src/app/modal/confirm.component.html
+++ b/client/src/app/modal/confirm.component.html
@@ -17,13 +17,13 @@
17 17
18 <div class="modal-footer inputs"> 18 <div class="modal-footer inputs">
19 <input 19 <input
20 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 20 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
21 (click)="dismiss()" (key.enter)="dismiss()" 21 (click)="dismiss()" (key.enter)="dismiss()"
22 > 22 >
23 23
24 <input 24 <input
25 ngbAutofocus 25 ngbAutofocus
26 type="submit" [value]="confirmButtonText" class="action-button-submit" [disabled]="isConfirmationDisabled()" 26 type="submit" [value]="confirmButtonText" class="peertube-button orange-button" [disabled]="isConfirmationDisabled()"
27 (click)="close()" (key.enter)="confirm()" 27 (click)="close()" (key.enter)="confirm()"
28 > 28 >
29 </div> 29 </div>
diff --git a/client/src/app/modal/confirm.component.scss b/client/src/app/modal/confirm.component.scss
index ed226bc09..69978f212 100644
--- a/client/src/app/modal/confirm.component.scss
+++ b/client/src/app/modal/confirm.component.scss
@@ -17,5 +17,3 @@ input[type=text] {
17.form-group { 17.form-group {
18 margin: 20px 0; 18 margin: 20px 0;
19} 19}
20
21
diff --git a/client/src/app/modal/custom-modal.component.html b/client/src/app/modal/custom-modal.component.html
index 06ecc2743..cdfbfbb6a 100644
--- a/client/src/app/modal/custom-modal.component.html
+++ b/client/src/app/modal/custom-modal.component.html
@@ -3,17 +3,17 @@
3 <h4 class="modal-title">{{title}}</h4> 3 <h4 class="modal-title">{{title}}</h4>
4 <my-global-icon *ngIf="close" iconName="cross" aria-label="Close" role="button" (click)="onCloseClick()"></my-global-icon> 4 <my-global-icon *ngIf="close" iconName="cross" aria-label="Close" role="button" (click)="onCloseClick()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body" [innerHTML]="content"></div> 7 <div class="modal-body" [innerHTML]="content"></div>
8 8
9 <div *ngIf="hasCancel() || hasConfirm()" class="modal-footer inputs"> 9 <div *ngIf="hasCancel() || hasConfirm()" class="modal-footer inputs">
10 <input 10 <input
11 *ngIf="hasCancel()" type="button" role="button" value="{{cancel.value}}" class="action-button action-button-cancel" 11 *ngIf="hasCancel()" type="button" role="button" value="{{cancel.value}}" class="peertube-button grey-button"
12 (click)="onCancelClick()" (key.enter)="onCancelClick()" 12 (click)="onCancelClick()" (key.enter)="onCancelClick()"
13 > 13 >
14 14
15 <input 15 <input
16 *ngIf="hasConfirm()" type="button" role="button" value="{{confirm.value}}" class="action-button action-button-confirm" 16 *ngIf="hasConfirm()" type="button" role="button" value="{{confirm.value}}" class="peertube-button orange-button"
17 (click)="onConfirmClick()" (key.enter)="onConfirmClick()" 17 (click)="onConfirmClick()" (key.enter)="onConfirmClick()"
18 > 18 >
19 </div> 19 </div>
diff --git a/client/src/app/modal/custom-modal.component.scss b/client/src/app/modal/custom-modal.component.scss
index a7fa30cf5..d6ef772b2 100644
--- a/client/src/app/modal/custom-modal.component.scss
+++ b/client/src/app/modal/custom-modal.component.scss
@@ -8,13 +8,3 @@
8li { 8li {
9 margin-bottom: 10px; 9 margin-bottom: 10px;
10} 10}
11
12.action-button-cancel {
13 @include peertube-button;
14 @include grey-button;
15}
16
17.action-button-confirm {
18 @include peertube-button;
19 @include orange-button;
20}
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..f085aa9de 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>
@@ -35,10 +35,11 @@
35 </my-peertube-checkbox> 35 </my-peertube-checkbox>
36 36
37 <input 37 <input
38 type="button" role="button" i18n-value value="Close" class="action-button action-button-cancel" 38 type="button" role="button" i18n-value value="Close" class="peertube-button grey-button"
39 (click)="hide()" (key.enter)="hide()" 39 (click)="hide()" (key.enter)="hide()"
40 > 40 >
41 <a i18n class="action-button action-button-configure" ngbAutofocus 41
42 <a i18n class="peertube-button-link orange-button" ngbAutofocus
42 href="/admin/config/edit-custom" target="_blank" rel="noopener noreferrer"> 43 href="/admin/config/edit-custom" target="_blank" rel="noopener noreferrer">
43 Configure 44 Configure
44 </a> 45 </a>
diff --git a/client/src/app/modal/instance-config-warning-modal.component.scss b/client/src/app/modal/instance-config-warning-modal.component.scss
index cc97d64e4..8d734c628 100644
--- a/client/src/app/modal/instance-config-warning-modal.component.scss
+++ b/client/src/app/modal/instance-config-warning-modal.component.scss
@@ -1,10 +1,6 @@
1@import '_mixins'; 1@import '_mixins';
2@import '_variables'; 2@import '_variables';
3 3
4.action-button-cancel {
5 margin-right: 0 !important;
6}
7
8.modal-body { 4.modal-body {
9 font-size: 15px; 5 font-size: 15px;
10} 6}
@@ -18,11 +14,3 @@ li {
18 margin: 0 auto 50px; 14 margin: 0 auto 50px;
19 width: 25%; 15 width: 25%;
20} 16}
21
22.action-button-configure {
23 display: inline-block;
24
25 @include peertube-button;
26 @include orange-button;
27 @include disable-default-a-behaviour;
28}
diff --git a/client/src/app/modal/quick-settings-modal.component.scss b/client/src/app/modal/quick-settings-modal.component.scss
deleted file mode 100644
index b0e256744..000000000
--- a/client/src/app/modal/quick-settings-modal.component.scss
+++ /dev/null
@@ -1,24 +0,0 @@
1@import '_mixins';
2
3.modal-button {
4 @include disable-default-a-behaviour;
5 transform: translateY(2px);
6
7 button {
8 @include peertube-button;
9 @include grey-button;
10 @include button-with-icon(18px, 4px, -1px);
11
12 my-global-icon {
13 @include apply-svg-color(#585858);
14 }
15 }
16
17 & + .modal-button {
18 margin-left: 1rem;
19 }
20}
21
22.quick-settings-title {
23 @include in-content-small-title;
24}
diff --git a/client/src/app/modal/quick-settings-modal.component.ts b/client/src/app/modal/quick-settings-modal.component.ts
index 95726ab63..99859a1a5 100644
--- a/client/src/app/modal/quick-settings-modal.component.ts
+++ b/client/src/app/modal/quick-settings-modal.component.ts
@@ -8,8 +8,7 @@ import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
8 8
9@Component({ 9@Component({
10 selector: 'my-quick-settings', 10 selector: 'my-quick-settings',
11 templateUrl: './quick-settings-modal.component.html', 11 templateUrl: './quick-settings-modal.component.html'
12 styleUrls: [ './quick-settings-modal.component.scss' ]
13}) 12})
14export class QuickSettingsModalComponent extends FormReactive implements OnInit { 13export class QuickSettingsModalComponent extends FormReactive implements OnInit {
15 @ViewChild('modal', { static: true }) modal: NgbModal 14 @ViewChild('modal', { static: true }) modal: NgbModal
diff --git a/client/src/app/modal/welcome-modal.component.html b/client/src/app/modal/welcome-modal.component.html
index 19bf3a1ea..f5d2b8799 100644
--- a/client/src/app/modal/welcome-modal.component.html
+++ b/client/src/app/modal/welcome-modal.component.html
@@ -71,12 +71,12 @@
71 71
72 <div class="modal-footer inputs"> 72 <div class="modal-footer inputs">
73 <input 73 <input
74 type="button" role="button" i18n-value value="Remind me later" class="action-button action-button-understood" 74 type="button" role="button" i18n-value value="Remind me later" class="peertube-button grey-button"
75 (click)="hide()" (key.enter)="hide()" 75 (click)="hide()" (key.enter)="hide()"
76 > 76 >
77 77
78 <a i18n (click)="doNotOpenAgain(); hide()" (key.enter)="doNotOpenAgain(); hide()" 78 <a i18n (click)="doNotOpenAgain(); hide()" (key.enter)="doNotOpenAgain(); hide()"
79 class="configure-instance-button" href="/admin/config/edit-custom" target="_blank" 79 class="peertube-button-link orange-button" href="/admin/config/edit-custom" target="_blank"
80 rel="noopener noreferrer" ngbAutofocus> 80 rel="noopener noreferrer" ngbAutofocus>
81 Configure my instance 81 Configure my instance
82 </a> 82 </a>
diff --git a/client/src/app/modal/welcome-modal.component.scss b/client/src/app/modal/welcome-modal.component.scss
index a93dbcef9..28d5dc49c 100644
--- a/client/src/app/modal/welcome-modal.component.scss
+++ b/client/src/app/modal/welcome-modal.component.scss
@@ -47,43 +47,30 @@ li {
47 47
48.columns { 48.columns {
49 display: flex; 49 display: flex;
50}
50 51
51 .link-block { 52.link-block {
52 @include disable-default-a-behaviour; 53 @include disable-default-a-behaviour;
53
54 color: pvar(--mainForegroundColor);
55 padding: 10px;
56 transition: background-color 0.2s ease-in;
57 flex-basis: 33%;
58
59 &:hover {
60 background-color: rgba(0, 0, 0, 0.05);
61 }
62 54
63 .link-title { 55 color: pvar(--mainForegroundColor);
64 font-size: 16px; 56 padding: 10px;
65 font-weight: $font-semibold; 57 transition: background-color 0.2s ease-in;
66 display: flex; 58 flex-basis: 33%;
67 justify-content: center;
68 margin-bottom: 5px;
69 }
70 59
71 .link-title, 60 &:hover {
72 div { 61 background-color: rgba(0, 0, 0, 0.05);
73 text-align: center;
74 }
75 } 62 }
76}
77 63
78.configure-instance-button { 64 .link-title {
79 @include peertube-button; 65 font-size: 16px;
80 @include orange-button; 66 font-weight: $font-semibold;
81 @include disable-default-a-behaviour; 67 display: flex;
82 68 justify-content: center;
83 display: inline-block; 69 margin-bottom: 5px;
84} 70 }
85 71
86.action-button-understood { 72 .link-title,
87 @include peertube-button; 73 div {
88 @include grey-button; 74 text-align: center;
75 }
89} 76}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
index fb8366f4c..658d42537 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-details.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
@@ -10,12 +10,7 @@
10 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" 10 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
11 class="chip" 11 class="chip"
12 > 12 >
13 <img 13 <my-account-avatar [account]="abuse.reporterAccount"></my-account-avatar>
14 class="avatar"
15 [src]="abuse.reporterAccount.avatar?.path"
16 (error)="switchToDefaultAvatar($event)"
17 alt="Avatar"
18 >
19 <div> 14 <div>
20 <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span> 15 <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
21 </div> 16 </div>
@@ -35,12 +30,7 @@
35 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }" 30 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
36 class="chip" 31 class="chip"
37 > 32 >
38 <img 33 <my-account-avatar [account]="abuse.flaggedAccount"></my-account-avatar>
39 class="avatar"
40 [src]="abuse.flaggedAccount?.avatar?.path"
41 (error)="switchToDefaultAvatar($event)"
42 alt="Avatar"
43 >
44 <div> 34 <div>
45 <span class="text-muted">{{ abuse.flaggedAccount ? abuse.flaggedAccount.nameWithHost : '' }}</span> 35 <span class="text-muted">{{ abuse.flaggedAccount ? abuse.flaggedAccount.nameWithHost : '' }}</span>
46 </div> 36 </div>
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.ts b/client/src/app/shared/shared-abuse-list/abuse-details.component.ts
index 31cf3389d..e8ce7e678 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-details.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.ts
@@ -45,8 +45,4 @@ export class AbuseDetailsComponent {
45 label: this.predefinedReasonsTranslations[r] 45 label: this.predefinedReasonsTranslations[r]
46 })) 46 }))
47 } 47 }
48
49 switchToDefaultAvatar ($event: Event) {
50 ($event.target as HTMLImageElement).src = Account.GET_DEFAULT_AVATAR_URL()
51 }
52} 48}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
index 8428032bf..29b51f09c 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
@@ -65,12 +65,7 @@
65 <td *ngIf="isAdminView()"> 65 <td *ngIf="isAdminView()">
66 <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> 66 <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
67 <div class="chip two-lines"> 67 <div class="chip two-lines">
68 <img 68 <my-account-avatar [account]="abuse.reporterAccount"></my-account-avatar>
69 class="avatar"
70 [src]="abuse.reporterAccount.avatar?.path"
71 (error)="switchToDefaultAvatar($event)"
72 alt="Avatar"
73 >
74 <div> 69 <div>
75 {{ abuse.reporterAccount.displayName }} 70 {{ abuse.reporterAccount.displayName }}
76 <span>{{ abuse.reporterAccount.nameWithHost }}</span> 71 <span>{{ abuse.reporterAccount.nameWithHost }}</span>
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..8b5771237 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,14 +117,11 @@ 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
124 switchToDefaultAvatar ($event: Event) {
125 ($event.target as HTMLImageElement).src = Account.GET_DEFAULT_AVATAR_URL()
126 }
127
128 async removeAbuse (abuse: AdminAbuse) { 125 async removeAbuse (abuse: AdminAbuse) {
129 const res = await this.confirmService.confirm($localize`Do you really want to delete this abuse report?`, $localize`Delete`) 126 const res = await this.confirmService.confirm($localize`Do you really want to delete this abuse report?`, $localize`Delete`)
130 if (res === false) return 127 if (res === false) return
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html
index 17e9ce4cf..ab6967f28 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html
@@ -41,7 +41,7 @@
41 </div> 41 </div>
42 42
43 <div class="form-group inputs"> 43 <div class="form-group inputs">
44 <input type="submit" i18n-value value="Add a message" class="action-button-submit" [disabled]="!form.valid || sendingMessage"> 44 <input type="submit" i18n-value value="Add a message" class="peertube-button orange-button" [disabled]="!form.valid || sendingMessage">
45 </div> 45 </div>
46 </form> 46 </form>
47 47
diff --git a/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.html b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.html
index 8082e93f4..cc7bb6c92 100644
--- a/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.html
+++ b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.html
@@ -23,14 +23,11 @@
23 23
24 <div class="form-group inputs"> 24 <div class="form-group inputs">
25 <input 25 <input
26 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 26 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
27 (click)="hide()" (key.enter)="hide()" 27 (click)="hide()" (key.enter)="hide()"
28 > 28 >
29 29
30 <input 30 <input type="submit" i18n-value value="Update this comment" class="peertube-button orange-button" [disabled]="!form.valid" />
31 type="submit" i18n-value value="Update this comment" class="action-button-submit"
32 [disabled]="!form.valid"
33 >
34 </div> 31 </div>
35 </form> 32 </form>
36 </div> 33 </div>
diff --git a/client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts b/client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts
index 663cd902b..19b6d456d 100644
--- a/client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts
+++ b/client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts
@@ -10,6 +10,7 @@ import { AbuseDetailsComponent } from './abuse-details.component'
10import { AbuseListTableComponent } from './abuse-list-table.component' 10import { AbuseListTableComponent } from './abuse-list-table.component'
11import { AbuseMessageModalComponent } from './abuse-message-modal.component' 11import { AbuseMessageModalComponent } from './abuse-message-modal.component'
12import { ModerationCommentModalComponent } from './moderation-comment-modal.component' 12import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
13import { SharedAccountAvatarModule } from '../shared-account-avatar/shared-account-avatar.module'
13 14
14@NgModule({ 15@NgModule({
15 imports: [ 16 imports: [
@@ -19,7 +20,8 @@ import { ModerationCommentModalComponent } from './moderation-comment-modal.comp
19 SharedFormModule, 20 SharedFormModule,
20 SharedModerationModule, 21 SharedModerationModule,
21 SharedGlobalIconModule, 22 SharedGlobalIconModule,
22 SharedVideoCommentModule 23 SharedVideoCommentModule,
24 SharedAccountAvatarModule
23 ], 25 ],
24 26
25 declarations: [ 27 declarations: [
diff --git a/client/src/app/shared/shared-account-avatar/account-avatar.component.html b/client/src/app/shared/shared-account-avatar/account-avatar.component.html
new file mode 100644
index 000000000..ca4ceb12f
--- /dev/null
+++ b/client/src/app/shared/shared-account-avatar/account-avatar.component.html
@@ -0,0 +1,15 @@
1<ng-template #img>
2 <img [class]="class" [src]="avatarUrl" i18n-alt alt="Account avatar" />
3</ng-template>
4
5<a *ngIf="account && href" [href]="href" target="_blank" rel="noopener noreferrer" [title]="title">
6 <ng-template *ngTemplateOutlet="img"></ng-template>
7</a>
8
9<a *ngIf="account && internalHref" [routerLink]="internalHref" [title]="title">
10 <ng-template *ngTemplateOutlet="img"></ng-template>
11</a>
12
13<ng-container *ngIf="!account || (!href && !internalHref)">
14 <ng-template *ngTemplateOutlet="img"></ng-template>
15</ng-container>
diff --git a/client/src/app/shared/shared-account-avatar/account-avatar.component.scss b/client/src/app/shared/shared-account-avatar/account-avatar.component.scss
new file mode 100644
index 000000000..bb941d712
--- /dev/null
+++ b/client/src/app/shared/shared-account-avatar/account-avatar.component.scss
@@ -0,0 +1,22 @@
1@import '_variables';
2@import '_mixins';
3
4.avatar-25 {
5 @include avatar(25px);
6}
7
8.avatar-34 {
9 @include avatar(34px);
10}
11
12.avatar-36 {
13 @include avatar(36px);
14}
15
16.avatar-40 {
17 @include avatar(40px);
18}
19
20.avatar-120 {
21 @include avatar(120px);
22} \ No newline at end of file
diff --git a/client/src/app/shared/shared-account-avatar/account-avatar.component.ts b/client/src/app/shared/shared-account-avatar/account-avatar.component.ts
new file mode 100644
index 000000000..02a0a18bf
--- /dev/null
+++ b/client/src/app/shared/shared-account-avatar/account-avatar.component.ts
@@ -0,0 +1,39 @@
1import { Component, Input } from '@angular/core'
2import { Account } from '../shared-main/account/account.model'
3
4@Component({
5 selector: 'my-account-avatar',
6 styleUrls: [ './account-avatar.component.scss' ],
7 templateUrl: './account-avatar.component.html'
8})
9export class AccountAvatarComponent {
10 @Input() account: {
11 name: string
12 avatar?: { url?: string, path: string }
13 url: string
14 }
15 @Input() size: '25' | '34' | '36' | '40' | '120' = '36'
16
17 // Use an external link
18 @Input() href: string
19 // Use routerLink
20 @Input() internalHref: string | string[]
21
22 @Input() set title (value) {
23 this._title = value
24 }
25
26 private _title: string
27
28 get title () {
29 return this._title || $localize`${this.account.name} (account page)`
30 }
31
32 get class () {
33 return `avatar avatar-${this.size}`
34 }
35
36 get avatarUrl () {
37 return Account.GET_ACTOR_AVATAR_URL(this.account)
38 }
39}
diff --git a/client/src/app/shared/shared-account-avatar/index.ts b/client/src/app/shared/shared-account-avatar/index.ts
new file mode 100644
index 000000000..40c742ba5
--- /dev/null
+++ b/client/src/app/shared/shared-account-avatar/index.ts
@@ -0,0 +1,2 @@
1export * from './account-avatar.component'
2export * from './shared-account-avatar.module' \ No newline at end of file
diff --git a/client/src/app/shared/shared-account-avatar/shared-account-avatar.module.ts b/client/src/app/shared/shared-account-avatar/shared-account-avatar.module.ts
new file mode 100644
index 000000000..17b27589f
--- /dev/null
+++ b/client/src/app/shared/shared-account-avatar/shared-account-avatar.module.ts
@@ -0,0 +1,23 @@
1
2import { NgModule } from '@angular/core'
3import { SharedGlobalIconModule } from '../shared-icons'
4import { SharedMainModule } from '../shared-main/shared-main.module'
5import { AccountAvatarComponent } from './account-avatar.component'
6
7@NgModule({
8 imports: [
9 SharedMainModule,
10 SharedGlobalIconModule
11 ],
12
13 declarations: [
14 AccountAvatarComponent
15 ],
16
17 exports: [
18 AccountAvatarComponent
19 ],
20
21 providers: [ ]
22})
23export class SharedAccountAvatarModule { }
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/dynamic-form-field.component.html b/client/src/app/shared/shared-forms/dynamic-form-field.component.html
index c358cb119..c228069b5 100644
--- a/client/src/app/shared/shared-forms/dynamic-form-field.component.html
+++ b/client/src/app/shared/shared-forms/dynamic-form-field.component.html
@@ -1,10 +1,23 @@
1<div [formGroup]="form"> 1<div [formGroup]="form">
2 <label *ngIf="setting.type !== 'input-checkbox'" [attr.for]="setting.name" [innerHTML]="setting.label"></label> 2 <label *ngIf="setting.label && setting.type !== 'input-checkbox'" [attr.for]="setting.name" [innerHTML]="setting.label"></label>
3
4 <my-peertube-checkbox
5 *ngIf="setting.type === 'input-checkbox'"
6 [inputName]="setting.name"
7 [formControlName]="setting.name"
8 [labelInnerHTML]="setting.label"
9 ></my-peertube-checkbox>
3 10
4 <div *ngIf="setting.descriptionHTML" class="label-small-info" [innerHTML]="setting.descriptionHTML"></div> 11 <div *ngIf="setting.descriptionHTML" class="label-small-info" [innerHTML]="setting.descriptionHTML"></div>
5 12
6 <input *ngIf="setting.type === 'input'" type="text" [id]="setting.name" [formControlName]="setting.name" /> 13 <input *ngIf="setting.type === 'input'" type="text" [id]="setting.name" [formControlName]="setting.name" />
7 14
15 <div *ngIf="setting.type === 'select'" class="peertube-select-container">
16 <select [id]="setting.name" [formControlName]="setting.name" class="form-control">
17 <option *ngFor="let option of setting.options" [value]="option.value">{{ option.label }}</option>
18 </select>
19 </div>
20
8 <my-input-toggle-hidden *ngIf="setting.type === 'input-password'" [formControlName]="setting.name" [inputId]="setting.name"></my-input-toggle-hidden> 21 <my-input-toggle-hidden *ngIf="setting.type === 'input-password'" [formControlName]="setting.name" [inputId]="setting.name"></my-input-toggle-hidden>
9 22
10 <textarea *ngIf="setting.type === 'input-textarea'" type="text" [id]="setting.name" [formControlName]="setting.name"></textarea> 23 <textarea *ngIf="setting.type === 'input-textarea'" type="text" [id]="setting.name" [formControlName]="setting.name"></textarea>
@@ -25,12 +38,7 @@
25 [classes]="{ 'input-error': formErrors['settings.name'] }" 38 [classes]="{ 'input-error': formErrors['settings.name'] }"
26 ></my-markdown-textarea> 39 ></my-markdown-textarea>
27 40
28 <my-peertube-checkbox 41 <div *ngIf="setting.type === 'html'" [innerHTML]="setting.html"></div>
29 *ngIf="setting.type === 'input-checkbox'"
30 [inputName]="setting.name"
31 [formControlName]="setting.name"
32 [labelInnerHTML]="setting.label"
33 ></my-peertube-checkbox>
34 42
35 <div *ngIf="formErrors[setting.name]" class="form-error"> 43 <div *ngIf="formErrors[setting.name]" class="form-error">
36 {{ formErrors[setting.name] }} 44 {{ formErrors[setting.name] }}
diff --git a/client/src/app/shared/shared-forms/dynamic-form-field.component.scss b/client/src/app/shared/shared-forms/dynamic-form-field.component.scss
index 89193ed85..45ba28951 100644
--- a/client/src/app/shared/shared-forms/dynamic-form-field.component.scss
+++ b/client/src/app/shared/shared-forms/dynamic-form-field.component.scss
@@ -22,3 +22,7 @@ textarea {
22 margin-bottom: 10px; 22 margin-bottom: 10px;
23 font-size: 13px; 23 font-size: 13px;
24} 24}
25
26my-peertube-checkbox + .label-small-info {
27 margin-top: 5px;
28}
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..65e6798d4 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, Actor as ServerActor, 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 {
@@ -13,7 +13,7 @@ export class Account extends Actor implements ServerAccount {
13 13
14 userId?: number 14 userId?: number
15 15
16 static GET_ACTOR_AVATAR_URL (actor: object) { 16 static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) {
17 return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL() 17 return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL()
18 } 18 }
19 19
@@ -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..4b036341f 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'
2import { getAbsoluteAPIUrl } from '@app/helpers' 1import { getAbsoluteAPIUrl } from '@app/helpers'
2import { Actor as ServerActor, ActorImage } from '@shared/models'
3 3
4export abstract class Actor implements ActorServer { 4export abstract class Actor implements ServerActor {
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<ServerActor>) {
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/feeds/feed.component.scss b/client/src/app/shared/shared-main/feeds/feed.component.scss
index 333d59440..b655ee708 100644
--- a/client/src/app/shared/shared-main/feeds/feed.component.scss
+++ b/client/src/app/shared/shared-main/feeds/feed.component.scss
@@ -2,19 +2,17 @@
2@import '_mixins'; 2@import '_mixins';
3 3
4.feed { 4.feed {
5 width: min-content; 5 width: 100%;
6 6
7 a { 7 a {
8 color: black; 8 color: black;
9 display: block; 9 display: block;
10 } 10 }
11}
11 12
12 my-global-icon { 13my-global-icon {
13 cursor: pointer; 14 cursor: pointer;
14 width: 12px; 15 width: 100%;
15 position: relative;
16 top: -2px;
17 16
18 @include apply-svg-color(pvar(--mainForegroundColor)) 17 @include apply-svg-color(pvar(--mainForegroundColor))
19 }
20} 18}
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/plugins/index.ts b/client/src/app/shared/shared-main/plugins/index.ts
new file mode 100644
index 000000000..f36dab624
--- /dev/null
+++ b/client/src/app/shared/shared-main/plugins/index.ts
@@ -0,0 +1 @@
export * from './plugin-placeholder.component'
diff --git a/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts
new file mode 100644
index 000000000..93ba9fb9b
--- /dev/null
+++ b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts
@@ -0,0 +1,15 @@
1import { Component, Input } from '@angular/core'
2import { PluginElementPlaceholder } from '@shared/models'
3
4@Component({
5 selector: 'my-plugin-placeholder',
6 template: '<div [id]="getId()"></div>'
7})
8
9export class PluginPlaceholderComponent {
10 @Input() pluginId: PluginElementPlaceholder
11
12 getId () {
13 return 'plugin-placeholder-' + this.pluginId
14 }
15}
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..772198cb2 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,8 @@ 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'
36import { PluginPlaceholderComponent } from './plugins'
35import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' 37import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
36import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' 38import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
37import { VideoCaptionService } from './video-caption' 39import { VideoCaptionService } from './video-caption'
@@ -64,13 +66,11 @@ import { VideoChannelService } from './video-channel'
64 ], 66 ],
65 67
66 declarations: [ 68 declarations: [
67 VideoAvatarChannelComponent,
68 ActorAvatarInfoComponent,
69
70 FromNowPipe, 69 FromNowPipe,
71 NumberFormatterPipe, 70 NumberFormatterPipe,
72 BytesPipe, 71 BytesPipe,
73 DurationFormatterPipe, 72 DurationFormatterPipe,
73 AutofocusDirective,
74 74
75 InfiniteScrollerDirective, 75 InfiniteScrollerDirective,
76 PeerTubeTemplateDirective, 76 PeerTubeTemplateDirective,
@@ -93,7 +93,9 @@ import { VideoChannelService } from './video-channel'
93 SimpleSearchInputComponent, 93 SimpleSearchInputComponent,
94 94
95 UserQuotaComponent, 95 UserQuotaComponent,
96 UserNotificationsComponent 96 UserNotificationsComponent,
97
98 PluginPlaceholderComponent
97 ], 99 ],
98 100
99 exports: [ 101 exports: [
@@ -118,13 +120,11 @@ import { VideoChannelService } from './video-channel'
118 120
119 PrimeSharedModule, 121 PrimeSharedModule,
120 122
121 VideoAvatarChannelComponent,
122 ActorAvatarInfoComponent,
123
124 FromNowPipe, 123 FromNowPipe,
125 BytesPipe, 124 BytesPipe,
126 NumberFormatterPipe, 125 NumberFormatterPipe,
127 DurationFormatterPipe, 126 DurationFormatterPipe,
127 AutofocusDirective,
128 128
129 InfiniteScrollerDirective, 129 InfiniteScrollerDirective,
130 PeerTubeTemplateDirective, 130 PeerTubeTemplateDirective,
@@ -147,7 +147,9 @@ import { VideoChannelService } from './video-channel'
147 SimpleSearchInputComponent, 147 SimpleSearchInputComponent,
148 148
149 UserQuotaComponent, 149 UserQuotaComponent,
150 UserNotificationsComponent 150 UserNotificationsComponent,
151
152 PluginPlaceholderComponent
151 ], 153 ],
152 154
153 providers: [ 155 providers: [
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..14c507295 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,
@@ -20,7 +20,6 @@ export class Video implements VideoServerModel {
20 byVideoChannel: string 20 byVideoChannel: string
21 byAccount: string 21 byAccount: string
22 22
23 accountAvatarUrl: string
24 videoChannelAvatarUrl: string 23 videoChannelAvatarUrl: string
25 24
26 createdAt: Date 25 createdAt: Date
@@ -72,7 +71,7 @@ export class Video implements VideoServerModel {
72 displayName: string 71 displayName: string
73 url: string 72 url: string
74 host: string 73 host: string
75 avatar?: Avatar 74 avatar?: ActorImage
76 } 75 }
77 76
78 channel: { 77 channel: {
@@ -81,7 +80,7 @@ export class Video implements VideoServerModel {
81 displayName: string 80 displayName: string
82 url: string 81 url: string
83 host: string 82 host: string
84 avatar?: Avatar 83 avatar?: ActorImage
85 } 84 }
86 85
87 userHistory?: { 86 userHistory?: {
@@ -144,7 +143,6 @@ export class Video implements VideoServerModel {
144 143
145 this.byAccount = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host) 144 this.byAccount = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host)
146 this.byVideoChannel = Actor.CREATE_BY_STRING(hash.channel.name, hash.channel.host) 145 this.byVideoChannel = Actor.CREATE_BY_STRING(hash.channel.name, hash.channel.host)
147 this.accountAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.account)
148 this.videoChannelAvatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this.channel) 146 this.videoChannelAvatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this.channel)
149 147
150 this.category.label = peertubeTranslate(this.category.label, translations) 148 this.category.label = peertubeTranslate(this.category.label, translations)
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.html b/client/src/app/shared/shared-moderation/account-blocklist.component.html
index 7eca6411e..3f2f55559 100644
--- a/client/src/app/shared/shared-moderation/account-blocklist.component.html
+++ b/client/src/app/shared/shared-moderation/account-blocklist.component.html
@@ -24,7 +24,7 @@
24 24
25 <ng-template pTemplate="header"> 25 <ng-template pTemplate="header">
26 <tr> 26 <tr>
27 <th style="width: 150px;">Action</th> <!-- column for action buttons --> 27 <th style="width: 150px;" i18n>Action</th> <!-- column for action buttons -->
28 <th style="width: calc(100% - 300px);" i18n>Account</th> 28 <th style="width: calc(100% - 300px);" i18n>Account</th>
29 <th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th> 29 <th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
30 </tr> 30 </tr>
@@ -38,12 +38,7 @@
38 <td> 38 <td>
39 <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> 39 <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
40 <div class="chip two-lines"> 40 <div class="chip two-lines">
41 <img 41 <my-account-avatar [account]="accountBlock.blockedAccount"></my-account-avatar>
42 class="avatar"
43 [src]="accountBlock.blockedAccount.avatar?.path"
44 (error)="switchToDefaultAvatar($event)"
45 alt="Avatar"
46 >
47 <div> 42 <div>
48 {{ accountBlock.blockedAccount.displayName }} 43 {{ accountBlock.blockedAccount.displayName }}
49 <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span> 44 <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span>
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.ts b/client/src/app/shared/shared-moderation/account-blocklist.component.ts
index 3de9587b8..1bce65bf0 100644
--- a/client/src/app/shared/shared-moderation/account-blocklist.component.ts
+++ b/client/src/app/shared/shared-moderation/account-blocklist.component.ts
@@ -30,10 +30,6 @@ export class GenericAccountBlocklistComponent extends RestTable implements OnIni
30 this.initialize() 30 this.initialize()
31 } 31 }
32 32
33 switchToDefaultAvatar ($event: Event) {
34 ($event.target as HTMLImageElement).src = Account.GET_DEFAULT_AVATAR_URL()
35 }
36
37 unblockAccount (accountBlock: AccountBlock) { 33 unblockAccount (accountBlock: AccountBlock) {
38 const blockedAccount = accountBlock.blockedAccount 34 const blockedAccount = accountBlock.blockedAccount
39 const operation = this.mode === BlocklistComponentType.Account 35 const operation = this.mode === BlocklistComponentType.Account
diff --git a/client/src/app/shared/shared-moderation/batch-domains-modal.component.html b/client/src/app/shared/shared-moderation/batch-domains-modal.component.html
index 1b85c8f48..6a3c65721 100644
--- a/client/src/app/shared/shared-moderation/batch-domains-modal.component.html
+++ b/client/src/app/shared/shared-moderation/batch-domains-modal.component.html
@@ -28,14 +28,11 @@
28 28
29 <div class="form-group inputs"> 29 <div class="form-group inputs">
30 <input 30 <input
31 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 31 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
32 (click)="hide()" (key.enter)="hide()" 32 (click)="hide()" (key.enter)="hide()"
33 > 33 >
34 34
35 <input 35 <input type="submit" [value]="action" class="peertube-button orange-button" [disabled]="!form.valid" />
36 type="submit" [value]="action" class="action-button-submit"
37 [disabled]="!form.valid"
38 >
39 </div> 36 </div>
40 </form> 37 </form>
41 </div> 38 </div>
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.html b/client/src/app/shared/shared-moderation/report-modals/report.component.html
index bda62312f..6c99180ef 100644
--- a/client/src/app/shared/shared-moderation/report-modals/report.component.html
+++ b/client/src/app/shared/shared-moderation/report-modals/report.component.html
@@ -51,10 +51,11 @@
51 51
52 <div class="form-group inputs"> 52 <div class="form-group inputs">
53 <input 53 <input
54 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 54 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
55 (click)="hide()" (key.enter)="hide()" 55 (click)="hide()" (key.enter)="hide()"
56 > 56 >
57 <input type="submit" i18n-value value="Submit" class="action-button-submit" [disabled]="!form.valid"> 57
58 <input type="submit" i18n-value value="Submit" class="peertube-button orange-button" [disabled]="!form.valid">
58 </div> 59 </div>
59 60
60 </form> 61 </form>
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.html b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
index 4947088d1..1aae64bff 100644
--- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
@@ -89,10 +89,11 @@
89 89
90 <div class="form-group inputs"> 90 <div class="form-group inputs">
91 <input 91 <input
92 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 92 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
93 (click)="hide()" (key.enter)="hide()" 93 (click)="hide()" (key.enter)="hide()"
94 > 94 >
95 <input type="submit" i18n-value value="Submit" class="action-button-submit" [disabled]="!form.valid"> 95
96 <input type="submit" i18n-value value="Submit" class="peertube-button orange-button" [disabled]="!form.valid">
96 </div> 97 </div>
97 98
98 </form> 99 </form>
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-moderation/server-blocklist.component.html b/client/src/app/shared/shared-moderation/server-blocklist.component.html
index a6e974b36..537186f05 100644
--- a/client/src/app/shared/shared-moderation/server-blocklist.component.html
+++ b/client/src/app/shared/shared-moderation/server-blocklist.component.html
@@ -31,7 +31,7 @@
31 31
32 <ng-template pTemplate="header"> 32 <ng-template pTemplate="header">
33 <tr> 33 <tr>
34 <th style="width: 150px;">Action</th> <!-- column for action buttons --> 34 <th style="width: 150px;" i18n>Action</th> <!-- column for action buttons -->
35 <th style="width: calc(100% - 300px);" i18n>Instance</th> 35 <th style="width: calc(100% - 300px);" i18n>Instance</th>
36 <th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th> 36 <th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
37 </tr> 37 </tr>
diff --git a/client/src/app/shared/shared-moderation/shared-moderation.module.ts b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
index b1b98f8d0..c7e201792 100644
--- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts
+++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
@@ -13,13 +13,15 @@ import { UserBanModalComponent } from './user-ban-modal.component'
13import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' 13import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
14import { VideoBlockComponent } from './video-block.component' 14import { VideoBlockComponent } from './video-block.component'
15import { VideoBlockService } from './video-block.service' 15import { VideoBlockService } from './video-block.service'
16import { SharedAccountAvatarModule } from '../shared-account-avatar/shared-account-avatar.module'
16 17
17@NgModule({ 18@NgModule({
18 imports: [ 19 imports: [
19 SharedMainModule, 20 SharedMainModule,
20 SharedFormModule, 21 SharedFormModule,
21 SharedGlobalIconModule, 22 SharedGlobalIconModule,
22 SharedVideoCommentModule 23 SharedVideoCommentModule,
24 SharedAccountAvatarModule
23 ], 25 ],
24 26
25 declarations: [ 27 declarations: [
diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.html b/client/src/app/shared/shared-moderation/user-ban-modal.component.html
index 365eb1938..7129b00ca 100644
--- a/client/src/app/shared/shared-moderation/user-ban-modal.component.html
+++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.html
@@ -23,14 +23,11 @@
23 23
24 <div class="form-group inputs"> 24 <div class="form-group inputs">
25 <input 25 <input
26 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 26 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
27 (click)="hide()" (key.enter)="hide()" 27 (click)="hide()" (key.enter)="hide()"
28 > 28 >
29 29
30 <input 30 <input type="submit" i18n-value value="Ban this user" class="peertube-button orange-button" [disabled]="!form.valid" />
31 type="submit" i18n-value value="Ban this user" class="action-button-submit"
32 [disabled]="!form.valid"
33 >
34 </div> 31 </div>
35 </form> 32 </form>
36 </div> 33 </div>
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.html b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.html
index 4d562387a..f1680e385 100644
--- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.html
+++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.html
@@ -4,6 +4,6 @@
4 <my-action-dropdown 4 <my-action-dropdown
5 [actions]="userActions" [entry]="{ user: user, account: account }" 5 [actions]="userActions" [entry]="{ user: user, account: account }"
6 [buttonSize]="buttonSize" [placement]="placement" [label]="label" 6 [buttonSize]="buttonSize" [placement]="placement" [label]="label"
7 [container]="container" 7 [container]="container" [buttonStyled]="buttonStyled"
8 ></my-action-dropdown> 8 ></my-action-dropdown>
9</ng-container> 9</ng-container>
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
index f59910d1c..f510a82f9 100644
--- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
@@ -18,6 +18,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
18 @Input() prependActions: DropdownAction<{ user: User, account: Account }>[] 18 @Input() prependActions: DropdownAction<{ user: User, account: Account }>[]
19 19
20 @Input() buttonSize: 'normal' | 'small' = 'normal' 20 @Input() buttonSize: 'normal' | 'small' = 'normal'
21 @Input() buttonStyled = true
21 @Input() placement = 'right-top right-bottom auto' 22 @Input() placement = 'right-top right-bottom auto'
22 @Input() label: string 23 @Input() label: string
23 @Input() container: 'body' | undefined = undefined 24 @Input() container: 'body' | undefined = undefined
diff --git a/client/src/app/shared/shared-moderation/video-block.component.html b/client/src/app/shared/shared-moderation/video-block.component.html
index e982c4d77..5e9e8493c 100644
--- a/client/src/app/shared/shared-moderation/video-block.component.html
+++ b/client/src/app/shared/shared-moderation/video-block.component.html
@@ -35,14 +35,11 @@
35 35
36 <div class="form-group inputs"> 36 <div class="form-group inputs">
37 <input 37 <input
38 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 38 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
39 (click)="hide()" (key.enter)="hide()" 39 (click)="hide()" (key.enter)="hide()"
40 > 40 >
41 41
42 <input 42 <input type="submit" i18n-value value="Submit" class="peertube-button orange-button" [disabled]="!form.valid" />
43 type="submit" i18n-value value="Submit" class="action-button-submit"
44 [disabled]="!form.valid"
45 >
46 </div> 43 </div>
47 </form> 44 </form>
48 45
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..289adcb6a 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,14 +1,14 @@
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
11 type="button" role="button" i18n-value value="Maybe later" class="action-button action-button-cancel" 11 type="button" role="button" i18n-value value="Maybe later" class="peertube-button grey-button"
12 (click)="hide()" (key.enter)="hide()" 12 (click)="hide()" (key.enter)="hide()"
13 > 13 >
14 </div> 14 </div>
diff --git a/client/src/app/+videos/+video-watch/modal/video-support.component.ts b/client/src/app/shared/shared-support-modal/support-modal.component.ts
index bd5290a72..a0b9fada6 100644
--- a/client/src/app/+videos/+video-watch/modal/video-support.component.ts
+++ b/client/src/app/shared/shared-support-modal/support-modal.component.ts
@@ -2,18 +2,20 @@ import { Component, Input, ViewChild } from '@angular/core'
2import { MarkdownService } from '@app/core' 2import { MarkdownService } from '@app/core'
3import { VideoDetails } from '@app/shared/shared-main' 3import { VideoDetails } from '@app/shared/shared-main'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { VideoChannel } from '@shared/models'
5 6
6@Component({ 7@Component({
7 selector: 'my-video-support', 8 selector: 'my-support-modal',
8 templateUrl: './video-support.component.html', 9 templateUrl: './support-modal.component.html'
9 styleUrls: [ './video-support.component.scss' ]
10}) 10})
11export class VideoSupportComponent { 11export class SupportModalComponent {
12 @Input() video: VideoDetails = null 12 @Input() video: VideoDetails = null
13 @Input() videoChannel: VideoChannel = null
13 14
14 @ViewChild('modal', { static: true }) modal: NgbModal 15 @ViewChild('modal', { static: true }) modal: NgbModal
15 16
16 videoHTMLSupport = '' 17 htmlSupport = ''
18 displayName = ''
17 19
18 constructor ( 20 constructor (
19 private markdownService: MarkdownService, 21 private markdownService: MarkdownService,
@@ -23,8 +25,14 @@ export class VideoSupportComponent {
23 show () { 25 show () {
24 const modalRef = this.modalService.open(this.modal, { centered: true }) 26 const modalRef = this.modalService.open(this.modal, { centered: true })
25 27
26 this.markdownService.enhancedMarkdownToHTML(this.video.support) 28 const support = this.video?.support || this.videoChannel.support
27 .then(r => this.videoHTMLSupport = r) 29
30 this.markdownService.enhancedMarkdownToHTML(support)
31 .then(r => this.htmlSupport = r)
32
33 this.displayName = this.video
34 ? this.video.channel.displayName
35 : this.videoChannel.displayName
28 36
29 return modalRef 37 return modalRef
30 } 38 }
diff --git a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss
index 2b723a15a..ea59ab346 100644
--- a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss
+++ b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss
@@ -4,82 +4,82 @@
4 4
5.video-thumbnail { 5.video-thumbnail {
6 @include miniature-thumbnail; 6 @include miniature-thumbnail;
7}
7 8
8 .progress-bar { 9.progress-bar {
9 height: 3px; 10 height: 3px;
10 width: 100%; 11 width: 100%;
11 position: absolute; 12 position: absolute;
12 bottom: 0; 13 bottom: 0;
13 background-color: rgba(0, 0, 0, 0.20); 14 background-color: rgba(0, 0, 0, 0.20);
14 15
15 div { 16 div {
16 height: 100%; 17 height: 100%;
17 background-color: pvar(--mainColor); 18 background-color: pvar(--mainColor);
18 }
19 } 19 }
20}
20 21
21 .video-thumbnail-watch-later-overlay, 22.video-thumbnail-watch-later-overlay,
22 .video-thumbnail-label-overlay, 23.video-thumbnail-label-overlay,
23 .video-thumbnail-duration-overlay, 24.video-thumbnail-duration-overlay,
24 .video-thumbnail-live-overlay { 25.video-thumbnail-live-overlay {
25 @include static-thumbnail-overlay; 26 @include static-thumbnail-overlay;
26 27
27 border-radius: 3px; 28 border-radius: 3px;
28 font-size: 12px; 29 font-size: 12px;
29 font-weight: $font-semibold; 30 font-weight: $font-semibold;
30 line-height: 1.1; 31 line-height: 1.1;
31 z-index: z(miniature); 32 z-index: z(miniature);
32 } 33}
33 34
34 .video-thumbnail-label-overlay { 35.video-thumbnail-label-overlay {
35 position: absolute; 36 position: absolute;
36 padding: 0 5px; 37 padding: 0 5px;
37 left: 5px; 38 left: 5px;
38 top: 5px; 39 top: 5px;
39 font-weight: $font-bold; 40 font-weight: $font-bold;
40 41
41 &.warning { background-color: orange; } 42 &.warning { background-color: orange; }
42 &.danger { background-color: red; } 43 &.danger { background-color: red; }
43 } 44}
44 45
45 .video-thumbnail-duration-overlay, 46.video-thumbnail-duration-overlay,
46 .video-thumbnail-live-overlay { 47.video-thumbnail-live-overlay {
47 position: absolute; 48 position: absolute;
48 padding: 0 3px; 49 padding: 0 3px;
49 right: 5px; 50 right: 5px;
50 bottom: 5px; 51 bottom: 5px;
51 } 52}
52 53
53 .video-thumbnail-live-overlay { 54.video-thumbnail-live-overlay {
54 font-weight: $font-semibold; 55 font-weight: $font-semibold;
55 color: #fff; 56 color: #fff;
56 57
57 &:not(.live-ended) { 58 &:not(.live-ended) {
58 background-color: rgba(224, 8, 8, 0.7); 59 background-color: rgba(224, 8, 8, 0.7);
59 }
60 } 60 }
61}
61 62
62 .video-thumbnail-actions-overlay { 63.video-thumbnail-actions-overlay {
63 position: absolute; 64 position: absolute;
64 display: flex; 65 display: flex;
65 flex-direction: column; 66 flex-direction: column;
66 right: 5px; 67 right: 5px;
67 top: 5px; 68 top: 5px;
68 opacity: 0; 69 opacity: 0;
69 70
70 div:not(:first-child) { 71 div:not(:first-child) {
71 margin-top: 2px; 72 margin-top: 2px;
72 } 73 }
74}
73 75
74 .video-thumbnail-watch-later-overlay { 76.video-thumbnail-watch-later-overlay {
75 padding: 3px; 77 padding: 3px;
76 78
77 my-global-icon { 79 my-global-icon {
78 width: 22px; 80 width: 22px;
79 height: 22px; 81 height: 22px;
80 82
81 @include apply-svg-color(#fff); 83 @include apply-svg-color(#fff);
82 }
83 }
84 } 84 }
85} 85}
diff --git a/client/src/app/shared/shared-video-comment/video-comment.model.ts b/client/src/app/shared/shared-video-comment/video-comment.model.ts
index bf718ae08..9a4e3954e 100644
--- a/client/src/app/shared/shared-video-comment/video-comment.model.ts
+++ b/client/src/app/shared/shared-video-comment/video-comment.model.ts
@@ -17,7 +17,6 @@ export class VideoComment implements VideoCommentServerModel {
17 totalRepliesFromVideoAuthor: number 17 totalRepliesFromVideoAuthor: number
18 totalReplies: number 18 totalReplies: number
19 by: string 19 by: string
20 accountAvatarUrl: string
21 20
22 isLocal: boolean 21 isLocal: boolean
23 22
@@ -38,7 +37,6 @@ export class VideoComment implements VideoCommentServerModel {
38 37
39 if (this.account) { 38 if (this.account) {
40 this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host) 39 this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host)
41 this.accountAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.account)
42 40
43 const absoluteAPIUrl = getAbsoluteAPIUrl() 41 const absoluteAPIUrl = getAbsoluteAPIUrl()
44 const thisHost = new URL(absoluteAPIUrl).host 42 const thisHost = new URL(absoluteAPIUrl).host
@@ -70,7 +68,6 @@ export class VideoCommentAdmin implements VideoCommentAdminServerModel {
70 } 68 }
71 69
72 by: string 70 by: string
73 accountAvatarUrl: string
74 71
75 constructor (hash: VideoCommentAdminServerModel, textHtml: string) { 72 constructor (hash: VideoCommentAdminServerModel, textHtml: string) {
76 this.id = hash.id 73 this.id = hash.id
@@ -97,7 +94,6 @@ export class VideoCommentAdmin implements VideoCommentAdminServerModel {
97 94
98 if (this.account) { 95 if (this.account) {
99 this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host) 96 this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host)
100 this.accountAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.account)
101 97
102 this.account.localUrl = '/accounts/' + this.by 98 this.account.localUrl = '/accounts/' + this.by
103 } 99 }
diff --git a/client/src/app/shared/shared-video-live/live-stream-information.component.html b/client/src/app/shared/shared-video-live/live-stream-information.component.html
index 57920239d..d6ee67ba9 100644
--- a/client/src/app/shared/shared-video-live/live-stream-information.component.html
+++ b/client/src/app/shared/shared-video-live/live-stream-information.component.html
@@ -30,10 +30,7 @@
30 30
31 <div class="modal-footer"> 31 <div class="modal-footer">
32 <div class="form-group inputs"> 32 <div class="form-group inputs">
33 <input 33 <input type="button" role="button" i18n-value value="Close" class="peertube-button grey-button" (click)="dismiss()" />
34 type="button" role="button" i18n-value value="Close" class="action-button action-button-cancel"
35 (click)="dismiss()"
36 >
37 34
38 <my-edit-button 35 <my-edit-button
39 i18n-label label="Update live settings" 36 i18n-label label="Update live settings"
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..9ffeac5e8 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
@@ -4,6 +4,7 @@
4 4
5 <div class="action-block"> 5 <div class="action-block">
6 <my-feed *ngIf="syndicationItems" [syndicationItems]="syndicationItems"></my-feed> 6 <my-feed *ngIf="syndicationItems" [syndicationItems]="syndicationItems"></my-feed>
7
7 <ng-container *ngFor="let action of actions"> 8 <ng-container *ngFor="let action of actions">
8 <a *ngIf="action.routerLink" class="ml-2" [routerLink]="action.routerLink" routerLinkActive="active"> 9 <a *ngIf="action.routerLink" class="ml-2" [routerLink]="action.routerLink" routerLinkActive="active">
9 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container> 10 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container>
@@ -43,7 +44,7 @@
43 <div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div> 44 <div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div>
44 <div 45 <div
45 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" 46 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()"
46 class="videos" 47 class="videos" [ngClass]="{ 'display-as-row': displayAsRow() }"
47 > 48 >
48 <ng-container *ngFor="let video of videos; trackBy: videoById;"> 49 <ng-container *ngFor="let video of videos; trackBy: videoById;">
49 <h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)"> 50 <h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)">
@@ -52,8 +53,7 @@
52 53
53 <div class="video-wrapper"> 54 <div class="video-wrapper">
54 <my-video-miniature 55 <my-video-miniature
55 [fitWidth]="true" 56 [video]="video" [user]="userMiniature" [displayAsRow]="displayAsRow()"
56 [video]="video" [user]="userMiniature" [ownerDisplayType]="ownerDisplayType"
57 [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" 57 [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions"
58 (videoBlocked)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" 58 (videoBlocked)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)"
59 > 59 >
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..467ca1d2c 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
@@ -15,18 +15,9 @@ $iconSize: 16px;
15 justify-content: space-between; 15 justify-content: space-between;
16 align-items: center; 16 align-items: center;
17 17
18 .action-block { 18 my-feed {
19 ::ng-deep my-feed { 19 display: inline-block;
20 my-global-icon { 20 width: calc(#{$iconSize} - 2px);
21 width: calc(#{$iconSize} - 2px);
22 }
23 }
24
25 a button {
26 @include peertube-button;
27 @include grey-button;
28 @include button-with-icon($iconSize, 3px, -1px);
29 }
30 } 21 }
31 22
32 .moderation-block { 23 .moderation-block {
@@ -34,21 +25,12 @@ $iconSize: 16px;
34 my-global-icon { 25 my-global-icon {
35 position: relative; 26 position: relative;
36 width: $iconSize; 27 width: $iconSize;
37 top: -2px;
38 } 28 }
39 29
40 margin-left: .4rem; 30 margin-left: .4rem;
41 display: flex; 31 display: flex;
42 justify-content: flex-end; 32 justify-content: flex-end;
43 align-items: center; 33 align-items: center;
44
45 .dropdown-item {
46 padding: 0;
47
48 ::ng-deep my-peertube-checkbox label {
49 padding: 3px 15px;
50 }
51 }
52 } 34 }
53} 35}
54 36
@@ -69,7 +51,16 @@ $iconSize: 16px;
69} 51}
70 52
71.margin-content { 53.margin-content {
72 @include fluid-videos-miniature-layout; 54 @include grid-videos-miniature-layout;
55}
56
57.display-as-row.videos {
58 margin-left: pvar(--horizontalMarginContent);
59 margin-right: pvar(--horizontalMarginContent);
60
61 .video-wrapper {
62 margin-bottom: 15px;
63 }
73} 64}
74 65
75@media screen and (max-width: $mobile-view) { 66@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/shared-video-miniature.module.ts b/client/src/app/shared/shared-video-miniature/shared-video-miniature.module.ts
index 7a7868853..32cfdfd68 100644
--- a/client/src/app/shared/shared-video-miniature/shared-video-miniature.module.ts
+++ b/client/src/app/shared/shared-video-miniature/shared-video-miniature.module.ts
@@ -13,6 +13,7 @@ import { VideoDownloadComponent } from './video-download.component'
13import { VideoMiniatureComponent } from './video-miniature.component' 13import { VideoMiniatureComponent } from './video-miniature.component'
14import { VideosSelectionComponent } from './videos-selection.component' 14import { VideosSelectionComponent } from './videos-selection.component'
15import { VideoListHeaderComponent } from './video-list-header.component' 15import { VideoListHeaderComponent } from './video-list-header.component'
16import { SharedAccountAvatarModule } from '../shared-account-avatar/shared-account-avatar.module'
16 17
17@NgModule({ 18@NgModule({
18 imports: [ 19 imports: [
@@ -23,7 +24,8 @@ import { VideoListHeaderComponent } from './video-list-header.component'
23 SharedThumbnailModule, 24 SharedThumbnailModule,
24 SharedGlobalIconModule, 25 SharedGlobalIconModule,
25 SharedVideoLiveModule, 26 SharedVideoLiveModule,
26 SharedVideoModule 27 SharedVideoModule,
28 SharedAccountAvatarModule
27 ], 29 ],
28 30
29 declarations: [ 31 declarations: [
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..4ac74c106 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
@@ -4,10 +4,10 @@
4 <ng-container i18n>Download</ng-container> 4 <ng-container i18n>Download</ng-container>
5 5
6 <div *ngIf="videoCaptions" ngbDropdown class="d-inline-block ml-1"> 6 <div *ngIf="videoCaptions" ngbDropdown class="d-inline-block ml-1">
7 <span id="dropdownDownloadType" ngbDropdownToggle> 7 <span id="dropdown-download-type" ngbDropdownToggle>
8 {{ type }} 8 {{ type }}
9 </span> 9 </span>
10 <div ngbDropdownMenu aria-labelledby="dropdownDownloadType"> 10 <div ngbDropdownMenu aria-labelledby="dropdown-download-type">
11 <button *ngIf="type === 'video'" (click)="switchToType('subtitles')" ngbDropdownItem i18n>subtitles</button> 11 <button *ngIf="type === 'video'" (click)="switchToType('subtitles')" ngbDropdownItem i18n>subtitles</button>
12 <button *ngIf="type === 'subtitles'" (click)="switchToType('video')" ngbDropdownItem i18n>video</button> 12 <button *ngIf="type === 'subtitles'" (click)="switchToType('video')" ngbDropdownItem i18n>video</button>
13 </div> 13 </div>
@@ -17,96 +17,124 @@
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>
50 <ng-template ngbNavContent>
51 <div class="file-metadata">
52 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue">
53 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
54 <span class="metadata-attribute-value">{{ item.value.value }}</span>
55 </div>
56 </div>
57 </ng-template>
58 </ng-container>
59 40
60 <ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined">
61 <a ngbNavLink i18n>Video stream</a>
62 <ng-template ngbNavContent> 41 <ng-template ngbNavContent>
63 <div class="file-metadata"> 42 <div class="nav-content">
64 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue"> 43 <div class="input-group input-group-sm">
65 <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()" />
66 <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>
67 </div> 50 </div>
68 </div> 51 </div>
69 </ng-template> 52 </ng-template>
70 </ng-container> 53 </ng-container>
54 </div>
55 <div [ngbNavOutlet]="resolutionNav"></div>
71 56
72 <ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined"> 57 <div class="advanced-filters collapse-transition" [ngbCollapse]="isAdvancedCustomizationCollapsed">
73 <a ngbNavLink i18n>Audio stream</a> 58 <ng-container *ngIf="videoFile?.metadata">
74 <ng-template ngbNavContent> 59 <div ngbNav #nav="ngbNav" class="nav-tabs nav-metadata">
75 <div class="file-metadata"> 60 <ng-container ngbNavItem>
76 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue"> 61 <a ngbNavLink i18n>Format</a>
77 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> 62 <ng-template ngbNavContent>
78 <span class="metadata-attribute-value">{{ item.value.value }}</span> 63 <div class="file-metadata">
79 </div> 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>
80 </div> 102 </div>
81 </ng-template> 103 <div class="peertube-radio-container">
104 <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent">
105 <label i18n for="download-torrent">Torrent (.torrent file)</label>
106 </div>
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">
113 <ng-container *ngIf="isAdvancedCustomizationCollapsed">
114 <span class="glyphicon glyphicon-menu-down"></span>
87 115
88 <div class="download-type" *ngIf="type === 'video'"> 116 <ng-container i18n>
89 <div class="peertube-radio-container"> 117 Advanced
90 <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct"> 118 </ng-container>
91 <label i18n for="download-direct">Direct download</label> 119 </ng-container>
92 </div> 120
121 <ng-container *ngIf="!isAdvancedCustomizationCollapsed">
122 <span class="glyphicon glyphicon-menu-up"></span>
93 123
94 <div class="peertube-radio-container"> 124 <ng-container i18n>
95 <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent"> 125 Simple
96 <label i18n for="download-torrent">Torrent (.torrent file)</label> 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">
102 <input 133 <input
103 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 134 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
104 (click)="hide()" (key.enter)="hide()" 135 (click)="hide()" (key.enter)="hide()"
105 > 136 >
106 137
107 <input 138 <input type="submit" i18n-value value="Download" class="peertube-button orange-button" (click)="download()" />
108 type="submit" i18n-value value="Download" class="action-button-submit"
109 (click)="download()"
110 >
111 </div> 139 </div>
112</ng-template> 140</ng-template>
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.scss b/client/src/app/shared/shared-video-miniature/video-download.component.scss
index d407e9531..7f6e03c87 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,12 @@
15 } 37 }
16} 38}
17 39
18#dropdownDownloadType { 40#dropdown-download-type {
19 cursor: pointer; 41 cursor: pointer;
20} 42}
21 43
22.download-type { 44.download-type {
23 margin-top: 30px; 45 margin-top: 20px;
24 46
25 .peertube-radio-container { 47 .peertube-radio-container {
26 @include peertube-radio-container; 48 @include peertube-radio-container;
@@ -30,6 +52,10 @@
30 } 52 }
31} 53}
32 54
55.nav-metadata {
56 margin-top: 20px;
57}
58
33.file-metadata { 59.file-metadata {
34 padding: 1rem; 60 padding: 1rem;
35} 61}
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..bc19127aa 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,11 +9,16 @@
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 && displayOwnerVideoChannel()" class="channel-avatar" [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
14 <img [src]="getAvatarUrl()" alt="" /> 14 <img [src]="getAvatarUrl()" alt="" />
15 </a> 15 </a>
16 16
17 <my-account-avatar
18 *ngIf="displayOptions.avatar && displayOwnerAccount()" [title]="channelLinkTitle"
19 [account]="video.account" size="40" [internalHref]="'/video-channels/' + video.byVideoChannel"
20 ></my-account-avatar>
21
17 <div class="w-100 d-flex flex-column"> 22 <div class="w-100 d-flex flex-column">
18 <a *ngIf="!videoHref" tabindex="-1" class="video-miniature-name" 23 <a *ngIf="!videoHref" tabindex="-1" class="video-miniature-name"
19 [routerLink]="videoRouterLink" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }" 24 [routerLink]="videoRouterLink" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }"
@@ -33,7 +38,7 @@
33 </span> 38 </span>
34 </span> 39 </span>
35 40
36 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]"> 41 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
37 {{ video.byAccount }} 42 {{ video.byAccount }}
38 </a> 43 </a>
39 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> 44 <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..f6f2925f0 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,202 @@
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 { 15my-account-avatar,
18 width: $video-miniature-width - $more-button-width - $more-margin-right; 16.channel-avatar {
19 line-height: normal; 17 margin: 10px 10px 0 0;
18}
20 19
21 .avatar { 20.channel-avatar img{
22 margin: 10px 10px 0 0; 21 @include channel-avatar(40px);
22}
23 23
24 img { 24.video-miniature-created-at-views {
25 @include avatar(40px); 25 font-size: 13px;
26 } 26}
27 }
28 27
29 .video-miniature-name { 28.video-miniature-account,
30 @include miniature-name; 29.video-miniature-channel {
31 width: calc(100% - #{$more-button-width}); 30 @include disable-default-a-behaviour;
32 } 31 @include ellipsis;
33 32
34 .video-miniature-meta { 33 display: block;
35 width: calc(100% + #{$more-button-width}); 34 font-size: 13px;
36 overflow: hidden; 35 color: pvar(--greyForegroundColor);
37 }
38 36
39 .video-miniature-created-at-views { 37 &:hover {
40 display: block; 38 color: $grey-foreground-hover-color;
41 font-size: 13px; 39 }
42 } 40}
43 41
44 .video-miniature-account, 42.video-info-privacy,
45 .video-miniature-channel { 43.video-info-blocked .blocked-label,
46 @include disable-default-a-behaviour; 44.video-info-nsfw {
47 @include ellipsis; 45 font-weight: $font-semibold;
46}
48 47
49 display: block; 48.video-info-blocked {
50 font-size: 13px; 49 color: red;
51 color: pvar(--greyForegroundColor);
52 50
53 &:hover { 51 .blocked-reason::before {
54 color: $grey-foreground-hover-color; 52 content: ' - ';
55 } 53 }
56 } 54}
57 55
58 .video-info-privacy, 56.video-info-nsfw {
59 .video-info-blocked .blocked-label, 57 color: red;
60 .video-info-nsfw { 58}
61 font-weight: $font-semibold;
62 }
63 59
64 .video-info-blocked { 60.video-actions {
65 color: red; 61 width: $more-button-width;
62 height: 30px;
66 63
67 .blocked-reason::before { 64 ::ng-deep .dropdown-root:not(.show) {
68 content: ' - '; 65 opacity: 0;
69 } 66 }
70 }
71 67
72 .video-info-nsfw { 68 ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root {
73 color: red; 69 opacity: 1;
74 } 70 }
75 }
76 71
77 .video-actions { 72 ::ng-deep .more-icon {
78 margin-top: 3px; 73 opacity: .6;
79 width: $more-button-width;
80 height: 30px;
81 74
82 ::ng-deep .dropdown-root:not(.show) { 75 &:hover {
83 opacity: 0; 76 opacity: 1;
84 } 77 }
78 }
79}
85 80
86 ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root { 81.video-miniature:hover {
87 opacity: 1; 82 ::ng-deep .video-thumbnail-actions-overlay,
88 } 83 .video-actions ::ng-deep .dropdown-root {
84 opacity: 1 !important;
85 }
86}
89 87
90 ::ng-deep .more-icon { 88// Grid mode
91 opacity: .6; 89// Takes all the width on mobile
90.video-miniature:not(.display-as-row) {
91 display: flex;
92 flex-direction: column;
93 padding-bottom: $video-miniature-margin-bottom;
94 width: 100%;
92 95
93 &:hover { 96 my-video-thumbnail {
94 opacity: 1; 97 @include block-ratio($selector: '::ng-deep .video-thumbnail');
95 } 98 }
96 }
97 }
98 99
99 @media screen and (max-width: $small-view) { 100 .video-bottom {
100 .video-miniature-information { 101 display: flex;
101 margin: 0 10px; 102 width: 100%;
102 } 103 }
103 104
104 .video-actions { 105 .video-miniature-name {
105 margin: 0; 106 margin-top: 10px;
106 top: -3px; 107 margin-bottom: 5px;
108 }
107 109
108 ::ng-deep .dropdown-root { 110 .video-miniature-created-at-views {
109 opacity: 1 !important; 111 display: block;
110 }
111 }
112 }
113 } 112 }
114 113
115 &:hover ::ng-deep .video-thumbnail .video-thumbnail-actions-overlay, 114 .video-actions {
116 &:hover .video-bottom .video-actions ::ng-deep .dropdown-root { 115 margin-top: 3px;
117 opacity: 1;
118 } 116 }
119 117
120 &.fit-width { 118 @media screen and (max-width: $small-view) {
121 width: 100%; 119 width: 100%;
120 margin-bottom: 25px;
121
122 .video-miniature-information {
123 margin: 0 10px;
124
125 width: 100%;
126 text-align: left;
127 }
122 128
123 .video-bottom { 129 .video-actions {
124 width: 100% !important; 130 margin: 0;
131 top: -3px;
125 132
126 .video-miniature-information { 133 ::ng-deep .dropdown-root {
127 width: calc(100% - #{$more-button-width}) !important; 134 opacity: 1 !important;
128 } 135 }
129 } 136 }
130 137
131 my-video-thumbnail { 138 ::ng-deep .video-thumbnail {
132 @include large-screen-ratio($selector: '::ng-deep .video-thumbnail'); 139 border-radius: 0;
133 } 140 }
134 } 141 }
142}
143
144.video-miniature.display-as-row {
145 --rowThumbnailWidth: #{$video-thumbnail-width};
146 --rowThumbnailHeight: #{$video-thumbnail-height};
135 147
136 &.display-as-row { 148 display: flex;
137 flex-direction: row; 149 flex-direction: row;
138 padding-bottom: 0; 150
139 height: auto; 151 .video-bottom {
140 display: flex; 152 display: flex;
141 flex-grow: 1; 153 }
142 154
143 my-video-thumbnail { 155 // We don't display avatar in row mode
144 margin-right: 10px; 156 .channel-avatar {
145 } 157 display: none;
158 }
146 159
147 .video-bottom { 160 my-video-thumbnail {
148 .video-miniature-information { 161 min-width: var(--rowThumbnailWidth);
149 @media screen and (min-width: $small-view) { 162 max-width: var(--rowThumbnailWidth);
150 width: auto; 163 height: var(--rowThumbnailHeight);
151 min-width: 500px; 164 margin-right: 10px;
152 } 165 }
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 166
181 .video-actions { 167 .video-miniature-name {
182 margin: 0; 168 @include ellipsis-multiline($video-miniature-row-name-font-size, 2);
183 top: -3px; 169 }
184 }
185 }
186 170
187 @media screen and (max-width: $small-view) { 171 .video-miniature-created-at-views,
188 flex-direction: column; 172 .video-miniature-account,
189 height: auto; 173 .video-miniature-channel {
174 font-size: $video-miniature-row-info-font-size;
175 }
190 176
191 my-video-thumbnail { 177 .video-actions {
192 margin-right: 0; 178 margin-top: -3px;
193 } 179 }
180}
194 181
195 .video-miniature-information { 182@include on-small-main-col {
196 min-width: initial; 183 .video-miniature.display-as-row {
197 } 184 --rowThumbnailWidth: #{$video-thumbnail-medium-width};
185 --rowThumbnailHeight: #{$video-thumbnail-medium-height};
186 }
187}
188
189@include on-mobile-main-col {
190 .video-miniature.display-as-row {
191 --rowThumbnailWidth: #{$video-thumbnail-small-width};
192 --rowThumbnailHeight: #{$video-thumbnail-small-height};
193
194 .video-miniature-name {
195 font-size: $video-miniature-row-info-font-size;
196 }
197
198 .video-miniature-created-at-views,
199 .video-miniature-account,
200 .video-miniature-channel {
201 font-size: $video-miniature-row-mobile-info-font-size;
198 } 202 }
199 } 203 }
200} 204}
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..8d66aaee2 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,8 +181,8 @@ 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.account.avatar?.url
188 } 186 }
189 187
190 return this.video.videoChannelAvatarUrl 188 return this.video.videoChannelAvatarUrl
@@ -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 ]