aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.html3
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.scss1
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html6
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.scss13
-rw-r--r--client/src/app/+about/about-instance/contact-admin-modal.component.html2
-rw-r--r--client/src/app/+about/about-instance/contact-admin-modal.component.scss1
-rw-r--r--client/src/app/+about/about-peertube/about-peertube.component.html21
-rw-r--r--client/src/app/+about/about-peertube/about-peertube.component.scss16
-rw-r--r--client/src/app/+about/about.component.html11
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.html8
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.scss6
-rw-r--r--client/src/app/+accounts/accounts.component.html12
-rw-r--r--client/src/app/+accounts/accounts.component.scss10
-rw-r--r--client/src/app/+accounts/accounts.component.ts10
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html15
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html96
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts17
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts8
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html4
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss30
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html10
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html84
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html29
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html52
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html10
-rw-r--r--client/src/app/+admin/follows/following-list/follow-modal.component.ts9
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html8
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html2
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.html6
-rw-r--r--client/src/app/+admin/overview/comments/video-comment-list.component.html4
-rw-r--r--client/src/app/+admin/overview/comments/video-comment-list.component.ts9
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.component.html228
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.component.scss7
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.ts4
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-password.component.html27
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-password.component.scss12
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.html20
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.scss12
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.ts91
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.html26
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.scss6
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.ts41
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.html6
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss5
-rw-r--r--client/src/app/+admin/plugins/shared/plugin-card.component.scss3
-rw-r--r--client/src/app/+admin/system/debug/debug.component.scss10
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.html8
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.scss8
-rw-r--r--client/src/app/+login/login.component.html88
-rw-r--r--client/src/app/+login/login.component.scss156
-rw-r--r--client/src/app/+login/login.component.ts4
-rw-r--r--client/src/app/+manage/video-channel-edit/video-channel-edit.component.html47
-rw-r--r--client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss42
-rw-r--r--client/src/app/+my-account/my-account-applications/my-account-applications.component.html13
-rw-r--r--client/src/app/+my-account/my-account-applications/my-account-applications.component.scss5
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html6
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss6
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html26
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.scss20
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.html36
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.scss11
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html4
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss3
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.scss9
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.html52
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.scss3
-rw-r--r--client/src/app/+my-account/my-account.component.html2
-rw-r--r--client/src/app/+my-account/my-account.component.scss2
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.html12
-rw-r--r--client/src/app/+my-library/my-follows/my-followers.component.html8
-rw-r--r--client/src/app/+my-library/my-follows/my-followers.component.scss10
-rw-r--r--client/src/app/+my-library/my-follows/my-subscriptions.component.html6
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.html5
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.scss5
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.ts18
-rw-r--r--client/src/app/+my-library/my-library.component.html2
-rw-r--r--client/src/app/+my-library/my-library.component.scss2
-rw-r--r--client/src/app/+my-library/my-ownership/my-ownership.component.html4
-rw-r--r--client/src/app/+my-library/my-ownership/my-ownership.component.scss7
-rw-r--r--client/src/app/+my-library/my-video-imports/my-video-imports.component.html2
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html16
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss5
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html3
-rw-r--r--client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html2
-rw-r--r--client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss4
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.html2
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.ts15
-rw-r--r--client/src/app/+page-not-found/page-not-found.component.html6
-rw-r--r--client/src/app/+remote-interaction/remote-interaction.component.html4
-rw-r--r--client/src/app/+reset-password/reset-password.component.html33
-rw-r--r--client/src/app/+reset-password/reset-password.component.scss9
-rw-r--r--client/src/app/+search/search-filters.component.html4
-rw-r--r--client/src/app/+search/search-filters.component.scss5
-rw-r--r--client/src/app/+search/search.component.html12
-rw-r--r--client/src/app/+search/search.component.scss1
-rw-r--r--client/src/app/+search/search.component.ts8
-rw-r--r--client/src/app/+signup/+register/custom-stepper.component.html37
-rw-r--r--client/src/app/+signup/+register/custom-stepper.component.scss139
-rw-r--r--client/src/app/+signup/+register/custom-stepper.component.ts9
-rw-r--r--client/src/app/+signup/+register/register-step-channel.component.html54
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.html66
-rw-r--r--client/src/app/+signup/+register/register.component.html133
-rw-r--r--client/src/app/+signup/+register/register.component.scss101
-rw-r--r--client/src/app/+signup/+register/register.component.ts53
-rw-r--r--client/src/app/+signup/+register/register.module.ts9
-rw-r--r--client/src/app/+signup/+register/steps/index.ts4
-rw-r--r--client/src/app/+signup/+register/steps/register-step-about.component.html39
-rw-r--r--client/src/app/+signup/+register/steps/register-step-about.component.scss53
-rw-r--r--client/src/app/+signup/+register/steps/register-step-about.component.ts19
-rw-r--r--client/src/app/+signup/+register/steps/register-step-channel.component.html55
-rw-r--r--client/src/app/+signup/+register/steps/register-step-channel.component.ts (renamed from client/src/app/+signup/+register/register-step-channel.component.ts)5
-rw-r--r--client/src/app/+signup/+register/steps/register-step-terms.component.html (renamed from client/src/app/+signup/+register/register-step-terms.component.html)10
-rw-r--r--client/src/app/+signup/+register/steps/register-step-terms.component.ts (renamed from client/src/app/+signup/+register/register-step-terms.component.ts)2
-rw-r--r--client/src/app/+signup/+register/steps/register-step-user.component.html71
-rw-r--r--client/src/app/+signup/+register/steps/register-step-user.component.ts (renamed from client/src/app/+signup/+register/register-step-user.component.ts)7
-rw-r--r--client/src/app/+signup/+register/steps/step.component.scss27
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html14
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss8
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html14
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-routing.module.ts2
-rw-r--r--client/src/app/+signup/shared/shared-signup.module.ts10
-rw-r--r--client/src/app/+signup/shared/signup-mascot.component.scss11
-rw-r--r--client/src/app/+signup/shared/signup-mascot.component.ts29
-rw-r--r--client/src/app/+signup/shared/signup-step-title.component.html9
-rw-r--r--client/src/app/+signup/shared/signup-step-title.component.scss23
-rw-r--r--client/src/app/+signup/shared/signup-step-title.component.ts12
-rw-r--r--client/src/app/+signup/shared/signup-success.component.html32
-rw-r--r--client/src/app/+signup/shared/signup-success.component.scss54
-rw-r--r--client/src/app/+signup/shared/signup-success.component.ts11
-rw-r--r--client/src/app/+stats/video/video-stats.component.html2
-rw-r--r--client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html4
-rw-r--r--client/src/app/+video-channels/video-channels.component.html12
-rw-r--r--client/src/app/+video-channels/video-channels.component.scss6
-rw-r--r--client/src/app/+video-studio/edit/video-studio-edit.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss5
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html38
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.scss30
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts2
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts2
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.scss1
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.html5
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts5
-rw-r--r--client/src/app/+videos/+video-edit/video-update.resolver.ts7
-rw-r--r--client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html2
-rw-r--r--client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.scss3
-rw-r--r--client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts6
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html8
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.scss3
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts41
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html8
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss1
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html8
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comments.component.scss12
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts4
-rw-r--r--client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html2
-rw-r--r--client/src/app/+videos/+video-watch/shared/information/video-alert.component.html6
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html6
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.scss6
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html4
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss16
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html6
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss13
-rw-r--r--client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html6
-rw-r--r--client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss58
-rw-r--r--client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.html5
-rw-r--r--client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss8
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.html2
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.scss4
-rw-r--r--client/src/app/+videos/video-list/overview/video-overview.component.html10
-rw-r--r--client/src/app/+videos/video-list/overview/video-overview.component.scss61
-rw-r--r--client/src/app/+videos/video-list/videos-list-common-page.component.ts25
-rw-r--r--client/src/app/app.component.html4
-rw-r--r--client/src/app/app.component.scss10
-rw-r--r--client/src/app/app.component.ts12
-rw-r--r--client/src/app/core/core.module.ts4
-rw-r--r--client/src/app/core/menu/menu.service.ts2
-rw-r--r--client/src/app/core/notification/peertube-socket.service.ts4
-rw-r--r--client/src/app/core/rest/rest-extractor.service.ts111
-rw-r--r--client/src/app/core/rest/rest-table.ts8
-rw-r--r--client/src/app/core/routing/scroll.service.ts2
-rw-r--r--client/src/app/core/theme/theme.service.ts23
-rw-r--r--client/src/app/header/header.component.scss8
-rw-r--r--client/src/app/header/search-typeahead.component.html8
-rw-r--r--client/src/app/header/search-typeahead.component.scss4
-rw-r--r--client/src/app/header/suggestion.component.html8
-rw-r--r--client/src/app/helpers/i18n-utils.ts25
-rw-r--r--client/src/app/helpers/utils/upload.ts49
-rw-r--r--client/src/app/menu/language-chooser.component.scss1
-rw-r--r--client/src/app/menu/menu.component.html19
-rw-r--r--client/src/app/menu/menu.component.scss33
-rw-r--r--client/src/app/menu/notification.component.html17
-rw-r--r--client/src/app/menu/notification.component.scss4
-rw-r--r--client/src/app/modal/account-setup-warning-modal.component.scss2
-rw-r--r--client/src/app/modal/admin-welcome-modal.component.html2
-rw-r--r--client/src/app/modal/admin-welcome-modal.component.scss6
-rw-r--r--client/src/app/modal/confirm.component.html2
-rw-r--r--client/src/app/modal/confirm.component.scss8
-rw-r--r--client/src/app/modal/custom-modal.component.scss4
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.scss4
-rw-r--r--client/src/app/modal/quick-settings-modal.component.html10
-rw-r--r--client/src/app/shared/form-validators/user-validators.ts2
-rw-r--r--client/src/app/shared/form-validators/video-channel-validators.ts2
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-details.component.html16
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-details.component.scss4
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.html12
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.scss11
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss5
-rw-r--r--client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html6
-rw-r--r--client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.scss51
-rw-r--r--client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts16
-rw-r--r--client/src/app/shared/shared-actor-image/actor-avatar.component.html14
-rw-r--r--client/src/app/shared/shared-actor-image/actor-avatar.component.ts86
-rw-r--r--client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html2
-rw-r--r--client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts2
-rw-r--r--client/src/app/shared/shared-forms/advanced-input-filter.component.html13
-rw-r--r--client/src/app/shared/shared-forms/advanced-input-filter.component.scss1
-rw-r--r--client/src/app/shared/shared-forms/dynamic-form-field.component.html12
-rw-r--r--client/src/app/shared/shared-forms/dynamic-form-field.component.scss5
-rw-r--r--client/src/app/shared/shared-forms/dynamic-form-field.component.ts11
-rw-r--r--client/src/app/shared/shared-forms/index.ts2
-rw-r--r--client/src/app/shared/shared-forms/input-switch.component.html2
-rw-r--r--client/src/app/shared/shared-forms/input-text.component.html23
-rw-r--r--client/src/app/shared/shared-forms/input-text.component.scss (renamed from client/src/app/shared/shared-forms/input-toggle-hidden.component.scss)11
-rw-r--r--client/src/app/shared/shared-forms/input-text.component.ts (renamed from client/src/app/shared/shared-forms/input-toggle-hidden.component.ts)11
-rw-r--r--client/src/app/shared/shared-forms/input-toggle-hidden.component.html21
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.html22
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.scss318
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.ts7
-rw-r--r--client/src/app/shared/shared-forms/peertube-checkbox.component.html8
-rw-r--r--client/src/app/shared/shared-forms/peertube-checkbox.component.scss16
-rw-r--r--client/src/app/shared/shared-forms/reactive-file.component.ts2
-rw-r--r--client/src/app/shared/shared-forms/select/select-channel.component.html2
-rw-r--r--client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts8
-rw-r--r--client/src/app/shared/shared-forms/select/select-checkbox.component.html4
-rw-r--r--client/src/app/shared/shared-forms/select/select-checkbox.component.scss2
-rw-r--r--client/src/app/shared/shared-forms/shared-form.module.ts6
-rw-r--r--client/src/app/shared/shared-forms/timestamp-input.component.scss1
-rw-r--r--client/src/app/shared/shared-icons/global-icon.component.scss5
-rw-r--r--client/src/app/shared/shared-icons/global-icon.component.ts12
-rw-r--r--client/src/app/shared/shared-instance/feature-boolean.component.html5
-rw-r--r--client/src/app/shared/shared-instance/feature-boolean.component.scss4
-rw-r--r--client/src/app/shared/shared-instance/instance-about-accordion.component.html6
-rw-r--r--client/src/app/shared/shared-instance/instance-about-accordion.component.scss27
-rw-r--r--client/src/app/shared/shared-instance/instance-about-accordion.component.ts7
-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.scss6
-rw-r--r--client/src/app/shared/shared-instance/instance-features-table.component.ts18
-rw-r--r--client/src/app/shared/shared-instance/instance-statistics.component.html18
-rw-r--r--client/src/app/shared/shared-instance/instance-statistics.component.scss29
-rw-r--r--client/src/app/shared/shared-instance/shared-instance.module.ts2
-rw-r--r--client/src/app/shared/shared-main/angular/autofocus.directive.ts4
-rw-r--r--client/src/app/shared/shared-main/angular/from-now.pipe.ts34
-rw-r--r--client/src/app/shared/shared-main/buttons/action-dropdown.component.html2
-rw-r--r--client/src/app/shared/shared-main/buttons/action-dropdown.component.ts2
-rw-r--r--client/src/app/shared/shared-main/buttons/button.component.html14
-rw-r--r--client/src/app/shared/shared-main/buttons/button.component.scss40
-rw-r--r--client/src/app/shared/shared-main/buttons/button.component.ts12
-rw-r--r--client/src/app/shared/shared-main/buttons/delete-button.component.html8
-rw-r--r--client/src/app/shared/shared-main/buttons/delete-button.component.ts7
-rw-r--r--client/src/app/shared/shared-main/buttons/edit-button.component.html8
-rw-r--r--client/src/app/shared/shared-main/buttons/edit-button.component.ts13
-rw-r--r--client/src/app/shared/shared-main/loaders/index.ts1
-rw-r--r--client/src/app/shared/shared-main/loaders/loader.component.html8
-rw-r--r--client/src/app/shared/shared-main/loaders/loader.component.scss45
-rw-r--r--client/src/app/shared/shared-main/loaders/loader.component.ts22
-rw-r--r--client/src/app/shared/shared-main/loaders/small-loader.component.html3
-rw-r--r--client/src/app/shared/shared-main/loaders/small-loader.component.ts11
-rw-r--r--client/src/app/shared/shared-main/misc/channels-setup-message.component.html2
-rw-r--r--client/src/app/shared/shared-main/misc/channels-setup-message.component.scss34
-rw-r--r--client/src/app/shared/shared-main/misc/list-overflow.component.html12
-rw-r--r--client/src/app/shared/shared-main/misc/list-overflow.component.scss3
-rw-r--r--client/src/app/shared/shared-main/misc/list-overflow.component.ts9
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.html15
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.scss6
-rw-r--r--client/src/app/shared/shared-main/misc/top-menu-dropdown.component.html12
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts4
-rw-r--r--client/src/app/shared/shared-main/users/user-quota.component.html2
-rw-r--r--client/src/app/shared/shared-main/users/user-quota.component.scss5
-rw-r--r--client/src/app/shared/shared-main/video-caption/video-caption.service.ts2
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.model.ts5
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts11
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts27
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.html8
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.scss3
-rw-r--r--client/src/app/shared/shared-moderation/account-blocklist.component.html6
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/report.component.html5
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.html44
-rw-r--r--client/src/app/shared/shared-moderation/server-blocklist.component.html6
-rw-r--r--client/src/app/shared/shared-moderation/user-ban-modal.component.scss1
-rw-r--r--client/src/app/shared/shared-moderation/user-ban-modal.component.ts21
-rw-r--r--client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts3
-rw-r--r--client/src/app/shared/shared-moderation/video-block.component.scss1
-rw-r--r--client/src/app/shared/shared-moderation/video-block.component.ts8
-rw-r--r--client/src/app/shared/shared-share-modal/video-share.component.html24
-rw-r--r--client/src/app/shared/shared-share-modal/video-share.component.scss26
-rw-r--r--client/src/app/shared/shared-tables/table-expander-icon.component.ts2
-rw-r--r--client/src/app/shared/shared-tables/video-cell.component.scss7
-rw-r--r--client/src/app/shared/shared-user-settings/user-interface-settings.component.html6
-rw-r--r--client/src/app/shared/shared-user-settings/user-interface-settings.component.scss5
-rw-r--r--client/src/app/shared/shared-user-settings/user-interface-settings.component.ts27
-rw-r--r--client/src/app/shared/shared-user-settings/user-video-settings.component.html8
-rw-r--r--client/src/app/shared/shared-user-settings/user-video-settings.component.scss11
-rw-r--r--client/src/app/shared/shared-user-subscription/remote-subscribe.component.html2
-rw-r--r--client/src/app/shared/shared-user-subscription/subscribe-button.component.html14
-rw-r--r--client/src/app/shared/shared-user-subscription/subscribe-button.component.scss14
-rw-r--r--client/src/app/shared/shared-video-comment/video-comment.service.ts8
-rw-r--r--client/src/app/shared/shared-video-live/live-documentation-link.component.html6
-rw-r--r--client/src/app/shared/shared-video-live/live-stream-information.component.html16
-rw-r--r--client/src/app/shared/shared-video-live/live-stream-information.component.scss9
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.html29
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.scss5
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.ts4
-rw-r--r--client/src/app/shared/shared-video-miniature/video-filters-header.component.html1
-rw-r--r--client/src/app/shared/shared-video-miniature/video-filters-header.component.scss4
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.html4
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.scss4
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.ts2
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-list.component.html6
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-list.component.scss8
-rw-r--r--client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html10
-rw-r--r--client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss45
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss74
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss2
331 files changed, 2788 insertions, 2721 deletions
diff --git a/client/src/app/+about/about-follows/about-follows.component.html b/client/src/app/+about/about-follows/about-follows.component.html
index 6bc1d0448..f16f8bd71 100644
--- a/client/src/app/+about/about-follows/about-follows.component.html
+++ b/client/src/app/+about/about-follows/about-follows.component.html
@@ -1,5 +1,6 @@
1<div class="row"> 1<div class="row">
2 <h1 class="sr-only" i18n>Follows</h1> 2 <h1 class="visually-hidden" i18n>Follows</h1>
3
3 <div class="col-xl-6 col-md-12"> 4 <div class="col-xl-6 col-md-12">
4 <h2 i18n class="subtitle">Follower instances ({{ followersPagination.totalItems }})</h2> 5 <h2 i18n class="subtitle">Follower instances ({{ followersPagination.totalItems }})</h2>
5 6
diff --git a/client/src/app/+about/about-follows/about-follows.component.scss b/client/src/app/+about/about-follows/about-follows.component.scss
index 80eb997e4..057d04ced 100644
--- a/client/src/app/+about/about-follows/about-follows.component.scss
+++ b/client/src/app/+about/about-follows/about-follows.component.scss
@@ -15,6 +15,7 @@ a {
15 15
16.no-results { 16.no-results {
17 justify-content: flex-start; 17 justify-content: flex-start;
18 align-items: flex-start;
18} 19}
19 20
20.show-more { 21.show-more {
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html
index 7f2a6aa77..b113df82f 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -8,9 +8,9 @@
8 </div> 8 </div>
9 9
10 <div class="instance-badges" *ngIf="categories.length !== 0 || languages.length !== 0"> 10 <div class="instance-badges" *ngIf="categories.length !== 0 || languages.length !== 0">
11 <span *ngFor="let category of categories" class="badge badge-primary category">{{ category }}</span> 11 <span *ngFor="let category of categories" class="pt-badge badge-primary">{{ category }}</span>
12 12
13 <span *ngFor="let language of languages" class="badge badge-secondary language">{{ language }}</span> 13 <span *ngFor="let language of languages" class="pt-badge badge-secondary">{{ language }}</span>
14 </div> 14 </div>
15 15
16 <div class="short-description"> 16 <div class="short-description">
@@ -204,7 +204,7 @@
204 </div> 204 </div>
205 205
206 <div class="col-md-12 col-xl-6" myPluginSelector pluginSelectorId="about-instance-features"> 206 <div class="col-md-12 col-xl-6" myPluginSelector pluginSelectorId="about-instance-features">
207 <h2 class="sr-only" i18n>FEATURES</h2> 207 <h2 class="visually-hidden" i18n>FEATURES</h2>
208 <my-instance-features-table></my-instance-features-table> 208 <my-instance-features-table></my-instance-features-table>
209 </div> 209 </div>
210 210
diff --git a/client/src/app/+about/about-instance/about-instance.component.scss b/client/src/app/+about/about-instance/about-instance.component.scss
index 965c04b6c..c01e690d1 100644
--- a/client/src/app/+about/about-instance/about-instance.component.scss
+++ b/client/src/app/+about/about-instance/about-instance.component.scss
@@ -19,27 +19,19 @@
19} 19}
20 20
21.instance-badges { 21.instance-badges {
22 font-size: 16px;
23 margin-bottom: 20px; 22 margin-bottom: 20px;
24 23
25 .badge { 24 .pt-badge {
26 @include margin-right(5px); 25 @include margin-right(5px);
27
28 font-size: 12px;
29 font-weight: $font-semibold;
30
31 &.category {
32 background-color: pvar(--mainColor);
33 }
34 } 26 }
35} 27}
36 28
37.section-title { 29.section-title {
38 font-weight: $font-semibold; 30 font-weight: $font-semibold;
39 font-size: 16px;
40 margin-bottom: 5px; 31 margin-bottom: 5px;
41 display: flex; 32 display: flex;
42 align-items: center; 33 align-items: center;
34 font-size: 1rem;
43} 35}
44 36
45.middle-title { 37.middle-title {
@@ -51,7 +43,6 @@
51 43
52.block { 44.block {
53 margin-bottom: 75px; 45 margin-bottom: 75px;
54 font-size: 15px;
55} 46}
56 47
57.short-description { 48.short-description {
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 ed027af44..a14ba3a4f 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
@@ -53,7 +53,7 @@
53 </div> 53 </div>
54 </form> 54 </form>
55 55
56 <div *ngIf="!isContactFormEnabled()" class="alert alert-error" i18n>The contact form is not enabled on this instance.</div> 56 <div *ngIf="!isContactFormEnabled()" class="alert alert-danger" i18n>The contact form is not enabled on this instance.</div>
57 57
58 </div> 58 </div>
59</ng-template> 59</ng-template>
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 e143a9dc6..6db6f13b3 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
@@ -2,7 +2,6 @@
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.modal-subtitle { 4.modal-subtitle {
5 font-size: 16px;
6 line-height: 1rem; 5 line-height: 1rem;
7 margin-bottom: 0; 6 margin-bottom: 0;
8} 7}
diff --git a/client/src/app/+about/about-peertube/about-peertube.component.html b/client/src/app/+about/about-peertube/about-peertube.component.html
index 62689e3ae..f8700d9b6 100644
--- a/client/src/app/+about/about-peertube/about-peertube.component.html
+++ b/client/src/app/+about/about-peertube/about-peertube.component.html
@@ -11,12 +11,12 @@
11 </p> 11 </p>
12 12
13 <p i18n> 13 <p i18n>
14 It is free and open-source software, under <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE">AGPLv3 14 It is free and open-source software, under <a class="link-orange" href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE">AGPLv3
15 licence</a>. 15 licence</a>.
16 </p> 16 </p>
17 17
18 <p i18n> 18 <p i18n>
19 For more information, please visit <a target="_blank" rel="noopener noreferrer" href="https://joinpeertube.org">joinpeertube.org</a>. 19 For more information, please visit <a class="link-orange" target="_blank" rel="noopener noreferrer" href="https://joinpeertube.org">joinpeertube.org</a>.
20 </p> 20 </p>
21 </div> 21 </div>
22 22
@@ -25,9 +25,8 @@
25 <div class="card"> 25 <div class="card">
26 <div class="card-body"> 26 <div class="card-body">
27 <div class="card-title"> 27 <div class="card-title">
28 <a i18n target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/use-setup-account">Use PeerTube 28 <a i18n class="link-orange" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/use-setup-account">Use PeerTube documentation</a>
29 documentation</a> 29 </div>
30 </div>
31 30
32 <div i18n class="card-text"> 31 <div i18n class="card-text">
33 Discover how to setup your account, what is a channel, how to create a playlist and more! 32 Discover how to setup your account, what is a channel, how to create a playlist and more!
@@ -38,9 +37,8 @@
38 <div class="card"> 37 <div class="card">
39 <div class="card-body"> 38 <div class="card-body">
40 <div class="card-title"> 39 <div class="card-title">
41 <a i18n target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/use-third-party-application">PeerTube 40 <a i18n class="link-orange" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/use-third-party-application">PeerTube Applications</a>
42 Applications</a> 41 </div>
43 </div>
44 42
45 <div i18n class="card-text"> 43 <div i18n class="card-text">
46 Discover unofficial Android applications or browser addons! 44 Discover unofficial Android applications or browser addons!
@@ -51,9 +49,8 @@
51 <div class="card"> 49 <div class="card">
52 <div class="card-body"> 50 <div class="card-body">
53 <div class="card-title"> 51 <div class="card-title">
54 <a i18n target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/contribute-getting-started">Contribute on 52 <a i18n class="link-orange" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/contribute-getting-started">Contribute on PeerTube</a>
55 PeerTube</a> 53 </div>
56 </div>
57 54
58 <div i18n class="card-text"> 55 <div i18n class="card-text">
59 Want to help to improve PeerTube? You can translate the web interface, give your feedback or directly contribute to the code! 56 Want to help to improve PeerTube? You can translate the web interface, give your feedback or directly contribute to the code!
@@ -116,7 +113,7 @@
116 Web peers are not publicly accessible: because we use the websocket transport, the protocol is different from classic BitTorrent tracker. 113 Web peers are not publicly accessible: because we use the websocket transport, the protocol is different from classic BitTorrent tracker.
117 When you are in a web browser, you send a signal containing your IP address to the tracker that will randomly choose other peers 114 When you are in a web browser, you send a signal containing your IP address to the tracker that will randomly choose other peers
118 to forward the information to. 115 to forward the information to.
119 See <a href="https://github.com/yciabaud/webtorrent/blob/beps/bep_webrtc.rst">this document</a> for more information 116 See <a class="link-orange" href="https://github.com/yciabaud/webtorrent/blob/beps/bep_webrtc.rst">this document</a> for more information
120 </li> 117 </li>
121 </ul> 118 </ul>
122 119
diff --git a/client/src/app/+about/about-peertube/about-peertube.component.scss b/client/src/app/+about/about-peertube/about-peertube.component.scss
index 2a5ec08f5..a0d227a28 100644
--- a/client/src/app/+about/about-peertube/about-peertube.component.scss
+++ b/client/src/app/+about/about-peertube/about-peertube.component.scss
@@ -37,18 +37,6 @@
37 .card { 37 .card {
38 margin: 30px; 38 margin: 30px;
39 flex-basis: 300px; 39 flex-basis: 300px;
40 font-size: 15px;
41 }
42}
43
44.description,
45.p2p-privacy,
46my-about-peertube-contributors {
47 ::ng-deep {
48 p,
49 li {
50 font-size: 15px;
51 }
52 } 40 }
53} 41}
54 42
@@ -70,7 +58,9 @@ my-about-peertube-contributors {
70} 58}
71 59
72.card-title { 60.card-title {
73 font-size: 1.25rem; 61 font-size: 1.1rem;
62 text-align: center;
63 margin-bottom: 1rem;
74} 64}
75 65
76.p2p-privacy-title { 66.p2p-privacy-title {
diff --git a/client/src/app/+about/about.component.html b/client/src/app/+about/about.component.html
index 63d429ebf..cf302e061 100644
--- a/client/src/app/+about/about.component.html
+++ b/client/src/app/+about/about.component.html
@@ -1,13 +1,10 @@
1<div class="row"> 1<div>
2 <div class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }"> 2 <div class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }">
3 <a myPluginSelector pluginSelectorId="about-menu-instance" i18n routerLink="instance" routerLinkActive="active" class="sub-menu-entry">Instance</a>
3 4
4 <div class="links"> 5 <a myPluginSelector pluginSelectorId="about-menu-peertube" i18n routerLink="peertube" routerLinkActive="active" class="sub-menu-entry">PeerTube</a>
5 <a myPluginSelector pluginSelectorId="about-menu-instance" i18n routerLink="instance" routerLinkActive="active" class="title-page title-page-about">Instance</a>
6 6
7 <a myPluginSelector pluginSelectorId="about-menu-peertube" i18n routerLink="peertube" routerLinkActive="active" class="title-page title-page-about">PeerTube</a> 7 <a myPluginSelector pluginSelectorId="about-menu-network" i18n routerLink="follows" routerLinkActive="active" class="sub-menu-entry">Network</a>
8
9 <a myPluginSelector pluginSelectorId="about-menu-network" i18n routerLink="follows" routerLinkActive="active" class="title-page title-page-about">Network</a>
10 </div>
11 </div> 8 </div>
12 9
13 <div class="margin-content" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }"> 10 <div class="margin-content" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }">
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
index 379c0443e..200d9415f 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
@@ -1,4 +1,4 @@
1<h1 class="sr-only" i18n>Video channels</h1> 1<h1 class="visually-hidden" i18n>Video channels</h1>
2 2
3<div class="margin-content"> 3<div class="margin-content">
4 4
@@ -9,7 +9,7 @@
9 9
10 <div class="channel-avatar-row"> 10 <div class="channel-avatar-row">
11 <my-actor-avatar 11 <my-actor-avatar
12 [channel]="videoChannel" 12 [actor]="videoChannel" actorType="channel"
13 [internalHref]="getVideoChannelLink(videoChannel)" 13 [internalHref]="getVideoChannelLink(videoChannel)"
14 i18n-title 14 i18n-title
15 title="See this video channel" 15 title="See this video channel"
@@ -23,10 +23,10 @@
23 </h2> 23 </h2>
24 24
25 <div class="actor-counters"> 25 <div class="actor-counters">
26 <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> 26 <div class="followers" i18n>{videoChannel.followersCount, plural, =0 {No subscribers} =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
27 27
28 <span class="videos-count" *ngIf="getTotalVideosOf(videoChannel) !== undefined" i18n> 28 <span class="videos-count" *ngIf="getTotalVideosOf(videoChannel) !== undefined" i18n>
29 {getTotalVideosOf(videoChannel), plural, =1 {1 videos} other {{{ getTotalVideosOf(videoChannel) }} videos}} 29 {getTotalVideosOf(videoChannel), plural, =0 {No videos} =1 {1 video} other {{{ getTotalVideosOf(videoChannel) }} videos}}
30 </span> 30 </span>
31 </div> 31 </div>
32 32
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
index 30b8098be..832ddf973 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
@@ -65,7 +65,6 @@
65 grid-row: 2; 65 grid-row: 2;
66 66
67 max-height: 80px; 67 max-height: 80px;
68 font-size: 16px;
69 } 68 }
70} 69}
71 70
@@ -105,7 +104,6 @@ my-subscribe-button {
105 104
106 a { 105 a {
107 color: pvar(--mainColor); 106 color: pvar(--mainColor);
108 font-size: 16px;
109 font-weight: $font-semibold; 107 font-weight: $font-semibold;
110 } 108 }
111} 109}
@@ -126,10 +124,6 @@ my-subscribe-button {
126 grid-row: 1 / 4; 124 grid-row: 1 / 4;
127 } 125 }
128 126
129 h2 {
130 font-size: 16px;
131 }
132
133 .actor-counters { 127 .actor-counters {
134 margin: 0; 128 margin: 0;
135 font-size: 13px; 129 font-size: 13px;
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 8362e6b7e..226fa8f31 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -2,7 +2,7 @@
2 <div class="account-info"> 2 <div class="account-info">
3 3
4 <div class="account-avatar-row"> 4 <div class="account-avatar-row">
5 <my-actor-avatar class="main-avatar" [account]="account"></my-actor-avatar> 5 <my-actor-avatar class="main-avatar" actorType="account" [actor]="account"></my-actor-avatar>
6 6
7 <div> 7 <div>
8 <div class="section-label" i18n>ACCOUNT</div> 8 <div class="section-label" i18n>ACCOUNT</div>
@@ -18,7 +18,7 @@
18 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()" 18 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
19 ></my-user-moderation-dropdown> 19 ></my-user-moderation-dropdown>
20 20
21 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> 21 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="pt-badge badge-danger" i18n>Banned</span>
22 22
23 <my-account-block-badges [account]="account"></my-account-block-badges> 23 <my-account-block-badges [account]="account"></my-account-block-badges>
24 </div> 24 </div>
@@ -28,15 +28,15 @@
28 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" 28 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
29 class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title 29 class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title
30 > 30 >
31 <span class="glyphicon glyphicon-duplicate"></span> 31 <my-global-icon iconName="copy"></my-global-icon>
32 </button> 32 </button>
33 </div> 33 </div>
34 34
35 <div class="actor-counters"> 35 <div class="actor-counters">
36 <span i18n>{naiveAggregatedSubscribers(), plural, =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span> 36 <span i18n>{naiveAggregatedSubscribers(), plural, =0 {No subscribers} =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span>
37 37
38 <span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n> 38 <span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n>
39 {accountVideosCount, plural, =1 {1 videos} other {{{ accountVideosCount }} videos}} 39 {accountVideosCount, plural, =0 {No videos} =1 {1 video} other {{{ accountVideosCount }} videos}}
40 </span> 40 </span>
41 </div> 41 </div>
42 </div> 42 </div>
@@ -66,7 +66,7 @@
66 66
67 <div class="links" [ngClass]="{ 'on-channel-page': isOnChannelPage() }"> 67 <div class="links" [ngClass]="{ 'on-channel-page': isOnChannelPage() }">
68 <ng-template #linkTemplate let-item="item"> 68 <ng-template #linkTemplate let-item="item">
69 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> 69 <a [routerLink]="item.routerLink" routerLinkActive="active" class="sub-menu-entry">{{ item.label }}</a>
70 </ng-template> 70 </ng-template>
71 71
72 <my-list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></my-list-overflow> 72 <my-list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></my-list-overflow>
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index 5043b98c4..e5f86e61e 100644
--- a/client/src/app/+accounts/accounts.component.scss
+++ b/client/src/app/+accounts/accounts.component.scss
@@ -37,7 +37,13 @@ my-user-moderation-dropdown {
37} 37}
38 38
39.copy-button { 39.copy-button {
40 @include margin-left(3px);
41
40 border: 0; 42 border: 0;
43
44 my-global-icon {
45 width: 15px;
46 }
41} 47}
42 48
43.account-info { 49.account-info {
@@ -93,6 +99,10 @@ my-user-moderation-dropdown {
93 } 99 }
94} 100}
95 101
102.pt-badge {
103 @include margin-right(5px);
104}
105
96@media screen and (max-width: $small-view) { 106@media screen and (max-width: $small-view) {
97 .root { 107 .root {
98 --myGlobalTopPadding: 45px; 108 --myGlobalTopPadding: 45px;
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index 898325492..cf66b817a 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -30,8 +30,6 @@ export class AccountsComponent implements OnInit, OnDestroy {
30 links: ListOverflowItem[] = [] 30 links: ListOverflowItem[] = []
31 hideMenu = false 31 hideMenu = false
32 32
33 accountFollowerTitle = ''
34
35 accountVideosCount: number 33 accountVideosCount: number
36 accountDescriptionHTML = '' 34 accountDescriptionHTML = ''
37 accountDescriptionExpanded = false 35 accountDescriptionExpanded = false
@@ -121,12 +119,6 @@ export class AccountsComponent implements OnInit, OnDestroy {
121 this.notifier.success($localize`Username copied`) 119 this.notifier.success($localize`Username copied`)
122 } 120 }
123 121
124 subscribersDisplayFor (count: number) {
125 if (count === 1) return $localize`1 subscriber`
126
127 return $localize`${count} subscribers`
128 }
129
130 searchChanged (search: string) { 122 searchChanged (search: string) {
131 const queryParams = { search } 123 const queryParams = { search }
132 124
@@ -150,8 +142,6 @@ export class AccountsComponent implements OnInit, OnDestroy {
150 } 142 }
151 143
152 private async onAccount (account: Account) { 144 private async onAccount (account: Account) {
153 this.accountFollowerTitle = $localize`${account.followersCount} direct account followers`
154
155 this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description) 145 this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description)
156 146
157 // After the markdown renderer to avoid layout changes 147 // After the markdown renderer to avoid layout changes
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html
index 0ab80e5a9..a17b13fdf 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html
@@ -1,15 +1,15 @@
1<ng-container [formGroup]="form"> 1<ng-container [formGroup]="form">
2 2
3 <div class="form-row mt-5"> <!-- cache grid --> 3 <div class="row mt-5"> <!-- cache grid -->
4 <div class="form-group col-12 col-lg-4 col-xl-3"> 4
5 <div class="col-12 col-lg-4 col-xl-3">
5 <div i18n class="inner-form-title">CACHE</div> 6 <div i18n class="inner-form-title">CACHE</div>
6 <div i18n class="inner-form-description"> 7 <div i18n class="inner-form-description">
7 Some files are not federated, and fetched when necessary. Define their caching policies. 8 Some files are not federated, and fetched when necessary. Define their caching policies.
8 </div> 9 </div>
9 </div> 10 </div>
10 11
11 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 12 <div class="col-12 col-lg-8 col-xl-9">
12
13 <ng-container formGroupName="cache"> 13 <ng-container formGroupName="cache">
14 <div class="form-group" formGroupName="previews"> 14 <div class="form-group" formGroupName="previews">
15 <label i18n for="cachePreviewsSize">Number of previews to keep in cache</label> 15 <label i18n for="cachePreviewsSize">Number of previews to keep in cache</label>
@@ -57,8 +57,8 @@
57 </div> 57 </div>
58 </div> 58 </div>
59 59
60 <div class="form-row mt-4"> <!-- cache grid --> 60 <div class="row mt-4"> <!-- cache grid -->
61 <div class="form-group col-12 col-lg-4 col-xl-3"> 61 <div class="col-12 col-lg-4 col-xl-3">
62 <div class="anchor" id="customizations"></div> <!-- customizations anchor --> 62 <div class="anchor" id="customizations"></div> <!-- customizations anchor -->
63 <div i18n class="inner-form-title">CUSTOMIZATIONS</div> 63 <div i18n class="inner-form-title">CUSTOMIZATIONS</div>
64 <div i18n class="inner-form-description"> 64 <div i18n class="inner-form-description">
@@ -66,8 +66,7 @@
66 </div> 66 </div>
67 </div> 67 </div>
68 68
69 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 69 <div class="col-12 col-lg-8 col-xl-9">
70
71 <ng-container formGroupName="instance"> 70 <ng-container formGroupName="instance">
72 <ng-container formGroupName="customizations"> 71 <ng-container formGroupName="customizations">
73 <div class="form-group"> 72 <div class="form-group">
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
index bae9d9775..7dfe5f5f9 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
@@ -1,13 +1,13 @@
1<ng-container [formGroup]="form"> 1<ng-container [formGroup]="form">
2 <div class="form-row mt-5"> <!-- appearance grid --> 2 <div class="row mt-5"> <!-- appearance grid -->
3 <div class="form-group col-12 col-lg-4 col-xl-3"> 3 <div class="col-12 col-lg-4 col-xl-3">
4 <div i18n class="inner-form-title">APPEARANCE</div> 4 <div i18n class="inner-form-title">APPEARANCE</div>
5 <div i18n class="inner-form-description"> 5 <div i18n class="inner-form-description">
6 Use <a routerLink="/admin/plugins">plugins & themes</a> for more involved changes, or <a routerLink="/admin/config/edit-custom" fragment="advanced-configuration">add slight customizations</a>. 6 Use <a class="link-orange" routerLink="/admin/plugins">plugins & themes</a> for more involved changes, or add slight <a class="link-orange" routerLink="/admin/config/edit-custom" fragment="advanced-configuration">customizations</a>.
7 </div> 7 </div>
8 </div> 8 </div>
9 9
10 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 10 <div class="col-12 col-lg-8 col-xl-9">
11 11
12 <ng-container formGroupName="theme"> 12 <ng-container formGroupName="theme">
13 <div class="form-group"> 13 <div class="form-group">
@@ -15,9 +15,9 @@
15 15
16 <div class="peertube-select-container"> 16 <div class="peertube-select-container">
17 <select formControlName="default" id="themeDefault" class="form-control"> 17 <select formControlName="default" id="themeDefault" class="form-control">
18 <option i18n value="default">default</option> 18 <option i18n value="default">{{ getDefaultThemeLabel() }}</option>
19 19
20 <option *ngFor="let theme of getAvailableThemes()" [value]="theme">{{ theme }}</option> 20 <option *ngFor="let theme of availableThemes" [value]="theme.id">{{ theme.label }}</option>
21 </select> 21 </select>
22 </div> 22 </div>
23 </div> 23 </div>
@@ -88,15 +88,15 @@
88 </div> 88 </div>
89 </div> 89 </div>
90 90
91 <div class="form-row mt-4"> <!-- broadcast grid --> 91 <div class="row mt-4"> <!-- broadcast grid -->
92 <div class="form-group col-12 col-lg-4 col-xl-3"> 92 <div class="col-12 col-lg-4 col-xl-3">
93 <div i18n class="inner-form-title">BROADCAST MESSAGE</div> 93 <div i18n class="inner-form-title">BROADCAST MESSAGE</div>
94 <div i18n class="inner-for-description"> 94 <div i18n class="inner-form-description">
95 Display a message on your instance 95 Display a message on your instance
96 </div> 96 </div>
97 </div> 97 </div>
98 98
99 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 99 <div class="col-12 col-lg-8 col-xl-9">
100 100
101 <ng-container formGroupName="broadcastMessage"> 101 <ng-container formGroupName="broadcastMessage">
102 102
@@ -132,8 +132,8 @@
132 <label i18n for="broadcastMessageMessage">Message</label><my-help helpType="markdownText"></my-help> 132 <label i18n for="broadcastMessageMessage">Message</label><my-help helpType="markdownText"></my-help>
133 133
134 <my-markdown-textarea 134 <my-markdown-textarea
135 name="broadcastMessageMessage" formControlName="message" textareaMaxWidth="500px" 135 name="broadcastMessageMessage" formControlName="message"
136 [classes]="{ 'input-error': formErrors['broadcastMessage.message'] }" 136 [formError]="formErrors['broadcastMessage.message']"
137 ></my-markdown-textarea> 137 ></my-markdown-textarea>
138 138
139 <div *ngIf="formErrors.broadcastMessage.message" class="form-error">{{ formErrors.broadcastMessage.message }}</div> 139 <div *ngIf="formErrors.broadcastMessage.message" class="form-error">{{ formErrors.broadcastMessage.message }}</div>
@@ -144,15 +144,15 @@
144 </div> 144 </div>
145 </div> 145 </div>
146 146
147 <div class="form-row mt-4"> <!-- new users grid --> 147 <div class="row mt-4"> <!-- new users grid -->
148 <div class="form-group col-12 col-lg-4 col-xl-3"> 148 <div class="col-12 col-lg-4 col-xl-3">
149 <div i18n class="inner-form-title">NEW USERS</div> 149 <div i18n class="inner-form-title">NEW USERS</div>
150 <div i18n class="inner-for-description"> 150 <div i18n class="inner-form-description">
151 Manage <a routerLink="/admin/users">users</a> to set their quota individually. 151 Manage <a class="link-orange" routerLink="/admin/users">users</a> to set their quota individually.
152 </div> 152 </div>
153 </div> 153 </div>
154 154
155 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 155 <div class="col-12 col-lg-8 col-xl-9">
156 156
157 <ng-container formGroupName="signup"> 157 <ng-container formGroupName="signup">
158 <div class="form-group"> 158 <div class="form-group">
@@ -163,7 +163,7 @@
163 <ng-container ngProjectAs="description"> 163 <ng-container ngProjectAs="description">
164 <span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span> 164 <span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
165 165
166 <div class="alert alert-info alert-signup" *ngIf="signupAlertMessage">{{ signupAlertMessage }}</div> 166 <div class="alert pt-alert-primary alert-signup" *ngIf="signupAlertMessage">{{ signupAlertMessage }}</div>
167 </ng-container> 167 </ng-container>
168 168
169 <ng-container ngProjectAs="extra"> 169 <ng-container ngProjectAs="extra">
@@ -185,7 +185,7 @@
185 185
186 <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div> 186 <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
187 187
188 <small i18n *ngIf="hasUnlimitedSignup()" class="muted">Signup won't be limited to a fixed number of users.</small> 188 <small i18n *ngIf="hasUnlimitedSignup()" class="muted small">Signup won't be limited to a fixed number of users.</small>
189 </div> 189 </div>
190 190
191 <div [ngClass]="getDisabledSignupClass()" class="mt-3"> 191 <div [ngClass]="getDisabledSignupClass()" class="mt-3">
@@ -239,20 +239,20 @@
239 </div> 239 </div>
240 </div> 240 </div>
241 241
242 <div class="form-row mt-4"> <!-- videos grid --> 242 <div class="row mt-4"> <!-- videos grid -->
243 <div class="form-group col-12 col-lg-4 col-xl-3"> 243 <div class="col-12 col-lg-4 col-xl-3">
244 <div i18n class="inner-form-title">VIDEOS</div> 244 <div i18n class="inner-form-title">VIDEOS</div>
245 </div> 245 </div>
246 246
247 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 247 <div class="col-12 col-lg-8 col-xl-9">
248 248
249 <ng-container formGroupName="import"> 249 <ng-container formGroupName="import">
250 250
251 <ng-container formGroupName="videos"> 251 <ng-container formGroupName="videos">
252 252
253 <div class="form-group mt-4"> 253 <div class="form-group">
254 <label i18n for="importConcurrency">Import jobs concurrency</label> 254 <label i18n for="importConcurrency">Import jobs concurrency</label>
255 <span i18n class="muted ml-1">allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.</span> 255 <span i18n class="small muted ms-1">allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.</span>
256 256
257 <div class="number-with-unit"> 257 <div class="number-with-unit">
258 <input type="number" name="importConcurrency" formControlName="concurrency" /> 258 <input type="number" name="importConcurrency" formControlName="concurrency" />
@@ -268,7 +268,7 @@
268 i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)" 268 i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
269 > 269 >
270 <ng-container ngProjectAs="description"> 270 <ng-container ngProjectAs="description">
271 <span i18n>⚠️ If enabled, we recommend to use <a href="https://docs.joinpeertube.org/maintain-configuration?id=security">a HTTP proxy</a> to prevent private URL access from your PeerTube server</span> 271 <span i18n>⚠️ If enabled, we recommend to use <a class="link-orange" href="https://docs.joinpeertube.org/maintain-configuration?id=security">a HTTP proxy</a> to prevent private URL access from your PeerTube server</span>
272 </ng-container> 272 </ng-container>
273 </my-peertube-checkbox> 273 </my-peertube-checkbox>
274 </div> 274 </div>
@@ -309,12 +309,12 @@
309 </div> 309 </div>
310 </div> 310 </div>
311 311
312 <div class="form-row mt-4"> <!-- video channels grid --> 312 <div class="row mt-4"> <!-- video channels grid -->
313 <div class="form-group col-12 col-lg-4 col-xl-3"> 313 <div class="col-12 col-lg-4 col-xl-3">
314 <div i18n class="inner-form-title">VIDEO CHANNELS</div> 314 <div i18n class="inner-form-title">VIDEO CHANNELS</div>
315 </div> 315 </div>
316 316
317 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 317 <div class="col-12 col-lg-8 col-xl-9">
318 <div class="form-group" formGroupName="videoChannels"> 318 <div class="form-group" formGroupName="videoChannels">
319 <label i18n for="videoChannelsMaxPerUser">Max video channels per user</label> 319 <label i18n for="videoChannelsMaxPerUser">Max video channels per user</label>
320 320
@@ -331,12 +331,12 @@
331 </div> 331 </div>
332 </div> 332 </div>
333 333
334 <div class="form-row mt-4"> <!-- search grid --> 334 <div class="row mt-4"> <!-- search grid -->
335 <div class="form-group col-12 col-lg-4 col-xl-3"> 335 <div class="col-12 col-lg-4 col-xl-3">
336 <div i18n class="inner-form-title">SEARCH</div> 336 <div i18n class="inner-form-title">SEARCH</div>
337 </div> 337 </div>
338 338
339 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 339 <div class="col-12 col-lg-8 col-xl-9">
340 340
341 <ng-container formGroupName="search"> 341 <ng-container formGroupName="search">
342 <ng-container formGroupName="remoteUri"> 342 <ng-container formGroupName="remoteUri">
@@ -372,11 +372,11 @@
372 i18n-labelText labelText="Enable global search" 372 i18n-labelText labelText="Enable global search"
373 > 373 >
374 <ng-container ngProjectAs="description"> 374 <ng-container ngProjectAs="description">
375 <p i18n>⚠️ This functionality depends heavily on the moderation of instances followed by the search index you select.</p> 375 <div i18n>⚠️ This functionality depends heavily on the moderation of instances followed by the search index you select.</div>
376 376
377 <span i18n> 377 <div i18n>
378 You should only use moderated search indexes in production, or <a href="https://framagit.org/framasoft/peertube/search-index">host your own</a>. 378 You should only use moderated search indexes in production, or <a class="link-orange" href="https://framagit.org/framasoft/peertube/search-index">host your own</a>.
379 </span> 379 </div>
380 </ng-container> 380 </ng-container>
381 381
382 <ng-container ngProjectAs="extra"> 382 <ng-container ngProjectAs="extra">
@@ -420,15 +420,15 @@
420 </div> 420 </div>
421 </div> 421 </div>
422 422
423 <div class="form-row mt-4"> <!-- federation grid --> 423 <div class="row mt-4"> <!-- federation grid -->
424 <div class="form-group col-12 col-lg-4 col-xl-3"> 424 <div class="col-12 col-lg-4 col-xl-3">
425 <div i18n class="inner-form-title">FEDERATION</div> 425 <div i18n class="inner-form-title">FEDERATION</div>
426 <div i18n class="inner-form-description"> 426 <div i18n class="inner-form-description">
427 Manage <a routerLink="/admin/follows">relations</a> with other instances. 427 Manage <a class="link-orange" routerLink="/admin/follows">relations</a> with other instances.
428 </div> 428 </div>
429 </div> 429 </div>
430 430
431 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 431 <div class="col-12 col-lg-8 col-xl-9">
432 432
433 <ng-container formGroupName="followers"> 433 <ng-container formGroupName="followers">
434 <ng-container formGroupName="instance"> 434 <ng-container formGroupName="instance">
@@ -472,10 +472,10 @@
472 i18n-labelText labelText="Automatically follow instances of a public index" 472 i18n-labelText labelText="Automatically follow instances of a public index"
473 > 473 >
474 <ng-container ngProjectAs="description"> 474 <ng-container ngProjectAs="description">
475 <p i18n>⚠️ This functionality requires a lot of attention and extra moderation.</p> 475 <div i18n>⚠️ This functionality requires a lot of attention and extra moderation.</div>
476 476
477 <span i18n> 477 <span i18n>
478 See <a href="https://docs.joinpeertube.org/admin-following-instances?id=automatically-follow-other-instances" rel="noopener noreferrer" target="_blank">the documentation</a> for more information about the expected URL 478 See <a class="link-orange" href="https://docs.joinpeertube.org/admin-following-instances?id=automatically-follow-other-instances" rel="noopener noreferrer" target="_blank">the documentation</a> for more information about the expected URL
479 </span> 479 </span>
480 </ng-container> 480 </ng-container>
481 481
@@ -499,12 +499,12 @@
499 </div> 499 </div>
500 </div> 500 </div>
501 501
502 <div class="form-row mt-4"> <!-- administrators grid --> 502 <div class="row mt-4"> <!-- administrators grid -->
503 <div class="form-group col-12 col-lg-4 col-xl-3"> 503 <div class="col-12 col-lg-4 col-xl-3">
504 <div i18n class="inner-form-title">ADMINISTRATORS</div> 504 <div i18n class="inner-form-title">ADMINISTRATORS</div>
505 </div> 505 </div>
506 506
507 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 507 <div class="col-12 col-lg-8 col-xl-9">
508 508
509 <div class="form-group" formGroupName="admin"> 509 <div class="form-group" formGroupName="admin">
510 <label i18n for="adminEmail">Admin email</label> 510 <label i18n for="adminEmail">Admin email</label>
@@ -527,8 +527,8 @@
527 </div> 527 </div>
528 </div> 528 </div>
529 529
530 <div class="form-row mt-4"> <!-- Twitter grid --> 530 <div class="row mt-4"> <!-- Twitter grid -->
531 <div class="form-group col-12 col-lg-4 col-xl-3"> 531 <div class="col-12 col-lg-4 col-xl-3">
532 <div i18n class="inner-form-title">TWITTER</div> 532 <div i18n class="inner-form-title">TWITTER</div>
533 <div i18n class="inner-form-description"> 533 <div i18n class="inner-form-description">
534 Provide the Twitter account representing your instance to improve link previews. 534 Provide the Twitter account representing your instance to improve link previews.
@@ -536,7 +536,7 @@
536 </div> 536 </div>
537 </div> 537 </div>
538 538
539 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 539 <div class="col-12 col-lg-8 col-xl-9">
540 540
541 <ng-container formGroupName="services"> 541 <ng-container formGroupName="services">
542 <ng-container formGroupName="twitter"> 542 <ng-container formGroupName="twitter">
@@ -563,7 +563,7 @@
563 If your instance is explicitly allowed by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br /> 563 If your instance is explicitly allowed by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
564 If the instance is not, we use an image link card that will redirect to your PeerTube instance.<br /><br /> 564 If the instance is not, we use an image link card that will redirect to your PeerTube instance.<br /><br />
565 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/w/blabla) on 565 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/w/blabla) on
566 <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> 566 <a class="link-orange" target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a>
567 to see if you instance is allowed. 567 to see if you instance is allowed.
568 </ng-container> 568 </ng-container>
569 </ng-template> 569 </ng-template>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
index 81457bd36..56227d11c 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
@@ -2,7 +2,7 @@ import { pairwise } from 'rxjs/operators'
2import { SelectOptionsItem } from 'src/types/select-options-item.model' 2import { SelectOptionsItem } from 'src/types/select-options-item.model'
3import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core' 3import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
4import { FormGroup } from '@angular/forms' 4import { FormGroup } from '@angular/forms'
5import { MenuService } from '@app/core' 5import { MenuService, ThemeService } from '@app/core'
6import { HTMLServerConfig } from '@shared/models' 6import { HTMLServerConfig } from '@shared/models'
7import { ConfigService } from '../shared/config.service' 7import { ConfigService } from '../shared/config.service'
8 8
@@ -19,15 +19,19 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
19 19
20 signupAlertMessage: string 20 signupAlertMessage: string
21 defaultLandingPageOptions: SelectOptionsItem[] = [] 21 defaultLandingPageOptions: SelectOptionsItem[] = []
22 availableThemes: SelectOptionsItem[]
22 23
23 constructor ( 24 constructor (
24 private configService: ConfigService, 25 private configService: ConfigService,
25 private menuService: MenuService 26 private menuService: MenuService,
27 private themeService: ThemeService
26 ) { } 28 ) { }
27 29
28 ngOnInit () { 30 ngOnInit () {
29 this.buildLandingPageOptions() 31 this.buildLandingPageOptions()
30 this.checkSignupField() 32 this.checkSignupField()
33
34 this.availableThemes = this.themeService.buildAvailableThemes()
31 } 35 }
32 36
33 ngOnChanges (changes: SimpleChanges) { 37 ngOnChanges (changes: SimpleChanges) {
@@ -48,11 +52,6 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
48 return this.configService.videoQuotaDailyOptions 52 return this.configService.videoQuotaDailyOptions
49 } 53 }
50 54
51 getAvailableThemes () {
52 return this.serverConfig.theme.registered
53 .map(t => t.name)
54 }
55
56 doesTrendingVideosAlgorithmsEnabledInclude (algorithm: string) { 55 doesTrendingVideosAlgorithmsEnabledInclude (algorithm: string) {
57 const enabled = this.form.value['trending']['videos']['algorithms']['enabled'] 56 const enabled = this.form.value['trending']['videos']['algorithms']['enabled']
58 if (!Array.isArray(enabled)) return false 57 if (!Array.isArray(enabled)) return false
@@ -94,6 +93,10 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
94 })) 93 }))
95 } 94 }
96 95
96 getDefaultThemeLabel () {
97 return this.themeService.getDefaultThemeLabel()
98 }
99
97 private checkSignupField () { 100 private checkSignupField () {
98 const signupControl = this.form.get('signup.enabled') 101 const signupControl = this.form.get('signup.enabled')
99 102
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts b/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts
index 9b55cb43c..96f5b830e 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts
@@ -1,5 +1,6 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { FormGroup } from '@angular/forms' 2import { FormGroup } from '@angular/forms'
3import { prepareIcu } from '@app/helpers'
3 4
4export type ResolutionOption = { 5export type ResolutionOption = {
5 id: string 6 id: string
@@ -86,9 +87,10 @@ export class EditConfigurationService {
86 return { 87 return {
87 value, 88 value,
88 atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible 89 atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
89 unit: value > 1 90 unit: prepareIcu($localize`{value, plural, =1 {thread} other {threads}}`)(
90 ? $localize`threads` 91 { value },
91 : $localize`thread` 92 $localize`threads`
93 )
92 } 94 }
93 } 95 }
94} 96}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 8fef39b79..cc8e699f3 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -1,4 +1,4 @@
1<h1 class="sr-only" i18n>Configuration</h1> 1<h1 class="visually-hidden" i18n>Configuration</h1>
2 2
3<div class="alert alert-warning" *ngIf="!isUpdateAllowed()" i18n> 3<div class="alert alert-warning" *ngIf="!isUpdateAllowed()" i18n>
4 Updating instance configuration from the web interface is disabled by the system administrator. 4 Updating instance configuration from the web interface is disabled by the system administrator.
@@ -64,7 +64,7 @@
64 64
65 <div [ngbNavOutlet]="nav"></div> 65 <div [ngbNavOutlet]="nav"></div>
66 66
67 <div class="form-row mt-4"> <!-- submit placement block --> 67 <div class="row mt-4"> <!-- submit placement block -->
68 <div class="col-md-7 col-xl-5"></div> 68 <div class="col-md-7 col-xl-5"></div>
69 <div class="col-md-5 col-xl-5"> 69 <div class="col-md-5 col-xl-5">
70 70
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
index 0458d257f..df523cedc 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
@@ -3,21 +3,25 @@
3 3
4$form-base-input-width: 340px; 4$form-base-input-width: 340px;
5 5
6label {
7 font-weight: $font-regular;
8 font-size: 100%;
9}
10
11form { 6form {
12 padding-bottom: 1.5rem; 7 padding-bottom: 1.5rem;
13} 8}
14 9
15input[type=text] { 10my-markdown-textarea {
16 @include peertube-input-text($form-base-input-width); 11 display: block;
12 max-width: 500px;
13}
17 14
15.homepage my-markdown-textarea {
18 display: block; 16 display: block;
17 max-width: 90%;
18
19 ::ng-deep textarea {
20 height: 300px !important;
21 }
19} 22}
20 23
24input[type=text],
21input[type=number] { 25input[type=number] {
22 @include peertube-input-text($form-base-input-width); 26 @include peertube-input-text($form-base-input-width);
23 27
@@ -30,7 +34,7 @@ input[type=number] {
30 34
31 input[type=number] + span { 35 input[type=number] + span {
32 position: absolute; 36 position: absolute;
33 top: 5px; 37 top: 0.2em;
34 right: 2.5rem; 38 right: 2.5rem;
35 } 39 }
36 40
@@ -74,6 +78,10 @@ input[type=submit] {
74 @include settings-big-title; 78 @include settings-big-title;
75} 79}
76 80
81.inner-form-description {
82 font-size: 15px;
83}
84
77textarea { 85textarea {
78 @include peertube-textarea(500px, 150px); 86 @include peertube-textarea(500px, 150px);
79 87
@@ -88,6 +96,7 @@ textarea {
88.label-small-info { 96.label-small-info {
89 font-style: italic; 97 font-style: italic;
90 margin-bottom: 10px; 98 margin-bottom: 10px;
99 font-size: 14px;
91} 100}
92 101
93.disabled-checkbox-extra { 102.disabled-checkbox-extra {
@@ -102,11 +111,6 @@ input[disabled] {
102 opacity: 0.5; 111 opacity: 0.5;
103} 112}
104 113
105
106.form-group-right {
107 padding-top: 2px;
108}
109
110ngb-tabset:not(.previews) ::ng-deep { 114ngb-tabset:not(.previews) ::ng-deep {
111 .nav-link { 115 .nav-link {
112 font-size: 105%; 116 font-size: 105%;
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
index 2286a5a1a..5339240bb 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
@@ -2,12 +2,12 @@
2 2
3 <ng-container formGroupName="instanceCustomHomepage"> 3 <ng-container formGroupName="instanceCustomHomepage">
4 4
5 <div class="form-row mt-5"> <!-- homepage grid --> 5 <div class="homepage row mt-5"> <!-- homepage grid -->
6 <div class="form-group col-12 col-lg-4 col-xl-3"> 6 <div class="col-12 col-lg-4 col-xl-3">
7 <div i18n class="inner-form-title">INSTANCE HOMEPAGE</div> 7 <div i18n class="inner-form-title">INSTANCE HOMEPAGE</div>
8 </div> 8 </div>
9 9
10 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 10 <div class="col-12 col-lg-8 col-xl-9">
11 11
12 <div class="form-group"> 12 <div class="form-group">
13 <label i18n for="instanceCustomHomepageContent">Homepage</label> 13 <label i18n for="instanceCustomHomepageContent">Homepage</label>
@@ -16,9 +16,9 @@
16 </div> 16 </div>
17 17
18 <my-markdown-textarea 18 <my-markdown-textarea
19 name="instanceCustomHomepageContent" formControlName="content" textareaMaxWidth="90%" textareaHeight="300px" 19 name="instanceCustomHomepageContent" formControlName="content"
20 [customMarkdownRenderer]="getCustomMarkdownRenderer()" 20 [customMarkdownRenderer]="getCustomMarkdownRenderer()"
21 [classes]="{ 'input-error': formErrors['instanceCustomHomepage.content'] }" 21 [formError]="formErrors['instanceCustomHomepage.content']"
22 ></my-markdown-textarea> 22 ></my-markdown-textarea>
23 23
24 <div *ngIf="formErrors.instanceCustomHomepage.content" class="form-error">{{ formErrors.instanceCustomHomepage.content }}</div> 24 <div *ngIf="formErrors.instanceCustomHomepage.content" class="form-error">{{ formErrors.instanceCustomHomepage.content }}</div>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
index d806616bd..b54733327 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
@@ -2,13 +2,12 @@
2 2
3 <ng-container formGroupName="instance"> 3 <ng-container formGroupName="instance">
4 4
5 <div class="form-row mt-5"> <!-- instance grid --> 5 <div class="row mt-5"> <!-- instance grid -->
6 <div class="form-group col-12 col-lg-4 col-xl-3"> 6 <div class="col-12 col-lg-4 col-xl-3">
7 <div i18n class="inner-form-title">INSTANCE</div> 7 <div i18n class="inner-form-title">INSTANCE</div>
8 </div> 8 </div>
9 9
10 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 10 <div class="col-12 col-lg-8 col-xl-9">
11
12 <div class="form-group"> 11 <div class="form-group">
13 <label i18n for="instanceName">Name</label> 12 <label i18n for="instanceName">Name</label>
14 13
@@ -38,12 +37,10 @@
38 </div> 37 </div>
39 38
40 <my-markdown-textarea 39 <my-markdown-textarea
41 name="instanceDescription" formControlName="description" textareaMaxWidth="500px" 40 name="instanceDescription" formControlName="description"
42 [customMarkdownRenderer]="getCustomMarkdownRenderer()" 41 [customMarkdownRenderer]="getCustomMarkdownRenderer()"
43 [classes]="{ 'input-error': formErrors['instance.description'] }" 42 [formError]="formErrors['instance.description']"
44 ></my-markdown-textarea> 43 ></my-markdown-textarea>
45
46 <div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</div>
47 </div> 44 </div>
48 45
49 <div class="form-group"> 46 <div class="form-group">
@@ -77,16 +74,15 @@
77 </div> 74 </div>
78 </div> 75 </div>
79 76
80 <div class="form-row mt-4"> <!-- moderation & nsfw grid --> 77 <div class="row mt-4"> <!-- moderation & nsfw grid -->
81 <div class="form-group col-12 col-lg-4 col-xl-3"> 78 <div class="col-12 col-lg-4 col-xl-3">
82 <div i18n class="inner-form-title">MODERATION & NSFW</div> 79 <div i18n class="inner-form-title">MODERATION & NSFW</div>
83 <div i18n class="inner-for-description"> 80 <div i18row="inner-form-description">
84 Manage <a routerLink="/admin/users">users</a> to build a moderation team. 81 Manage <a class="link-orange" routerLink="/admin/users">users</a> to build a moderation team.
85 </div> 82 </div>
86 </div> 83 </div>
87 84
88 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 85 <div class="col-12 col-lg-8 col-xl-9">
89
90 <div class="form-group"> 86 <div class="form-group">
91 <my-peertube-checkbox inputName="instanceIsNSFW" formControlName="isNSFW"> 87 <my-peertube-checkbox inputName="instanceIsNSFW" formControlName="isNSFW">
92 <ng-template ptTemplate="label"> 88 <ng-template ptTemplate="label">
@@ -129,22 +125,18 @@
129 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help> 125 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
130 126
131 <my-markdown-textarea 127 <my-markdown-textarea
132 name="instanceTerms" formControlName="terms" textareaMaxWidth="500px" 128 name="instanceTerms" formControlName="terms"
133 [ngClass]="{ 'input-error': formErrors['instance.terms'] }" 129 [formError]="formErrors['instance.terms']"
134 ></my-markdown-textarea> 130 ></my-markdown-textarea>
135
136 <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
137 </div> 131 </div>
138 132
139 <div class="form-group"> 133 <div class="form-group">
140 <label i18n for="instanceCodeOfConduct">Code of conduct</label><my-help helpType="markdownText"></my-help> 134 <label i18n for="instanceCodeOfConduct">Code of conduct</label><my-help helpType="markdownText"></my-help>
141 135
142 <my-markdown-textarea 136 <my-markdown-textarea
143 name="instanceCodeOfConduct" formControlName="codeOfConduct" textareaMaxWidth="500px" 137 name="instanceCodeOfConduct" formControlName="codeOfConduct"
144 [ngClass]="{ 'input-error': formErrors['instance.codeOfConduct'] }" 138 [formError]="formErrors['instance.codeOfConduct']"
145 ></my-markdown-textarea> 139 ></my-markdown-textarea>
146
147 <div *ngIf="formErrors.instance.codeOfConduct" class="form-error">{{ formErrors.instance.codeOfConduct }}</div>
148 </div> 140 </div>
149 141
150 <div class="form-group"> 142 <div class="form-group">
@@ -152,33 +144,29 @@
152 <div i18n class="label-small-info">Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc</div> 144 <div i18n class="label-small-info">Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc</div>
153 145
154 <my-markdown-textarea 146 <my-markdown-textarea
155 name="instanceModerationInformation" formControlName="moderationInformation" textareaMaxWidth="500px" 147 name="instanceModerationInformation" formControlName="moderationInformation"
156 [ngClass]="{ 'input-error': formErrors['instance.moderationInformation'] }" 148 [formError]="formErrors['instance.moderationInformation']"
157 ></my-markdown-textarea> 149 ></my-markdown-textarea>
158
159 <div *ngIf="formErrors.instance.moderationInformation" class="form-error">{{ formErrors.instance.moderationInformation }}</div>
160 </div> 150 </div>
161 151
162 </div> 152 </div>
163 </div> 153 </div>
164 154
165 <div class="form-row mt-4"> <!-- you and your instance grid --> 155 <div class="row mt-4"> <!-- you and your instance grid -->
166 <div class="form-group col-12 col-lg-4 col-xl-3"> 156 <div class="col-12 col-lg-4 col-xl-3">
167 <div i18n class="inner-form-title">YOU AND YOUR INSTANCE</div> 157 <div i18n class="inner-form-title">YOU AND YOUR INSTANCE</div>
168 </div> 158 </div>
169 159
170 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 160 <div class="col-12 col-lg-8 col-xl-9">
171 161
172 <div class="form-group"> 162 <div class="form-group">
173 <label i18n for="instanceAdministrator">Who is behind the instance?</label><my-help helpType="markdownText"></my-help> 163 <label i18n for="instanceAdministrator">Who is behind the instance?</label><my-help helpType="markdownText"></my-help>
174 <div i18n class="label-small-info">A single person? A non-profit? A company?</div> 164 <div i18n class="label-small-info">A single person? A non-profit? A company?</div>
175 165
176 <my-markdown-textarea 166 <my-markdown-textarea
177 name="instanceAdministrator" formControlName="administrator" textareaMaxWidth="500px" 167 name="instanceAdministrator" formControlName="administrator"
178 [classes]="{ 'input-error': formErrors['instance.administrator'] }" 168 [formError]="formErrors['instance.administrator']"
179 ></my-markdown-textarea> 169 ></my-markdown-textarea>
180
181 <div *ngIf="formErrors.instance.administrator" class="form-error">{{ formErrors.instance.administrator }}</div>
182 </div> 170 </div>
183 171
184 <div class="form-group"> 172 <div class="form-group">
@@ -186,11 +174,9 @@
186 <div i18n class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div> 174 <div i18n class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div>
187 175
188 <my-markdown-textarea 176 <my-markdown-textarea
189 name="instanceCreationReason" formControlName="creationReason" textareaMaxWidth="500px" 177 name="instanceCreationReason" formControlName="creationReason"
190 [ngClass]="{ 'input-error': formErrors['instance.creationReason'] }" 178 [formError]="formErrors['instance.creationReason']"
191 ></my-markdown-textarea> 179 ></my-markdown-textarea>
192
193 <div *ngIf="formErrors.instance.creationReason" class="form-error">{{ formErrors.instance.creationReason }}</div>
194 </div> 180 </div>
195 181
196 <div class="form-group"> 182 <div class="form-group">
@@ -198,11 +184,9 @@
198 <div i18n class="label-small-info">It's important to know for users who want to register on your instance</div> 184 <div i18n class="label-small-info">It's important to know for users who want to register on your instance</div>
199 185
200 <my-markdown-textarea 186 <my-markdown-textarea
201 name="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" textareaMaxWidth="500px" 187 name="instanceMaintenanceLifetime" formControlName="maintenanceLifetime"
202 [ngClass]="{ 'input-error': formErrors['instance.maintenanceLifetime'] }" 188 [formError]="formErrors['instance.maintenanceLifetime']"
203 ></my-markdown-textarea> 189 ></my-markdown-textarea>
204
205 <div *ngIf="formErrors.instance.maintenanceLifetime" class="form-error">{{ formErrors.instance.maintenanceLifetime }}</div>
206 </div> 190 </div>
207 191
208 <div class="form-group"> 192 <div class="form-group">
@@ -210,33 +194,29 @@
210 <div i18n class="label-small-info">With your own funds? With user donations? Advertising?</div> 194 <div i18n class="label-small-info">With your own funds? With user donations? Advertising?</div>
211 195
212 <my-markdown-textarea 196 <my-markdown-textarea
213 name="instanceBusinessModel" formControlName="businessModel" textareaMaxWidth="500px" 197 name="instanceBusinessModel" formControlName="businessModel"
214 [ngClass]="{ 'input-error': formErrors['instance.businessModel'] }" 198 [formError]="formErrors['instance.businessModel']"
215 ></my-markdown-textarea> 199 ></my-markdown-textarea>
216
217 <div *ngIf="formErrors.instance.businessModel" class="form-error">{{ formErrors.instance.businessModel }}</div>
218 </div> 200 </div>
219 201
220 </div> 202 </div>
221 </div> 203 </div>
222 204
223 <div class="form-row mt-4"> <!-- other information grid --> 205 <div class="row mt-4"> <!-- other information grid -->
224 <div class="form-group col-12 col-lg-4 col-xl-3"> 206 <div class="col-12 col-lg-4 col-xl-3">
225 <div i18n class="inner-form-title">OTHER INFORMATION</div> 207 <div i18n class="inner-form-title">OTHER INFORMATION</div>
226 </div> 208 </div>
227 209
228 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 210 <div class="col-12 col-lg-8 col-xl-9">
229 211
230 <div class="form-group"> 212 <div class="form-group">
231 <label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label> 213 <label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label>
232 <div i18n class="label-small-info">i.e. 2vCore 2GB RAM, a direct the link to the server you rent, etc.</div> 214 <div i18n class="label-small-info">i.e. 2vCore 2GB RAM, a direct the link to the server you rent, etc.</div>
233 215
234 <my-markdown-textarea 216 <my-markdown-textarea
235 name="instanceHardwareInformation" formControlName="hardwareInformation" textareaMaxWidth="500px" 217 name="instanceHardwareInformation" formControlName="hardwareInformation"
236 [classes]="{ 'input-error': formErrors['instance.hardwareInformation'] }" 218 [formError]="formErrors['instance.hardwareInformation']"
237 ></my-markdown-textarea> 219 ></my-markdown-textarea>
238
239 <div *ngIf="formErrors.instance.hardwareInformation" class="form-error">{{ formErrors.instance.hardwareInformation }}</div>
240 </div> 220 </div>
241 221
242 </div> 222 </div>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
index 71d5d91f0..ae79e54fc 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
@@ -1,14 +1,14 @@
1<ng-container [formGroup]="form"> 1<ng-container [formGroup]="form">
2 2
3 <div class="form-row mt-5"> 3 <div class="row mt-5">
4 <div class="form-group col-12 col-lg-4 col-xl-3"> 4 <div class="col-12 col-lg-4 col-xl-3">
5 <div i18n class="inner-form-title">LIVE</div> 5 <div i18n class="inner-form-title">LIVE</div>
6 <div i18n class="inner-form-description"> 6 <div i18n class="inner-form-description">
7 Enable users of your instance to stream live. 7 Enable users of your instance to stream live.
8 </div> 8 </div>
9 </div> 9 </div>
10 10
11 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 11 <div class="col-12 col-lg-8 col-xl-9">
12 12
13 <ng-container formGroupName="live"> 13 <ng-container formGroupName="live">
14 14
@@ -46,9 +46,9 @@
46 </div> 46 </div>
47 47
48 <div class="form-group" [ngClass]="getDisabledLiveClass()"> 48 <div class="form-group" [ngClass]="getDisabledLiveClass()">
49 <label i18n for="liveMaxInstanceLives"> 49 <label i18n for="liveMaxInstanceLives">Max simultaneous lives created on your instance</label>
50 Max simultaneous lives created on your instance <span class="muted">(-1 for "unlimited")</span> 50
51 </label> 51 <span class="ms-2 small muted">(-1 for "unlimited")</span>
52 52
53 <div class="number-with-unit"> 53 <div class="number-with-unit">
54 <input type="number" name="liveMaxInstanceLives" formControlName="maxInstanceLives" /> 54 <input type="number" name="liveMaxInstanceLives" formControlName="maxInstanceLives" />
@@ -59,9 +59,8 @@
59 </div> 59 </div>
60 60
61 <div class="form-group" [ngClass]="getDisabledLiveClass()"> 61 <div class="form-group" [ngClass]="getDisabledLiveClass()">
62 <label i18n for="liveMaxUserLives"> 62 <label i18n for="liveMaxUserLives">Max simultaneous lives created per user</label>
63 Max simultaneous lives created per user <span class="muted">(-1 for "unlimited")</span> 63 <span class="ms-2 small muted">(-1 for "unlimited")</span>
64 </label>
65 64
66 <div class="number-with-unit"> 65 <div class="number-with-unit">
67 <input type="number" name="liveMaxUserLives" formControlName="maxUserLives" /> 66 <input type="number" name="liveMaxUserLives" formControlName="maxUserLives" />
@@ -89,15 +88,15 @@
89 </div> 88 </div>
90 </div> 89 </div>
91 90
92 <div class="form-row"> <!-- transcoding live streams grid --> 91 <div class="row"> <!-- transcoding live streams grid -->
93 <div class="form-group col-12 col-lg-4 col-xl-3"> 92 <div class="col-12 col-lg-4 col-xl-3">
94 <div i18n class="inner-form-title">TRANSCODING</div> 93 <div i18n class="inner-form-title">TRANSCODING</div>
95 <div i18n class="inner-form-description"> 94 <div i18n class="inner-form-description">
96 Same as VOD transcoding, transcoding live streams so that they are in a streamable form that any device can play. Requires a beefy CPU, and then some. 95 Same as VOD transcoding, transcoding live streams so that they are in a streamable form that any device can play. Requires a beefy CPU, and then some.
97 </div> 96 </div>
98 </div> 97 </div>
99 98
100 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 99 <div class="col-12 col-lg-8 col-xl-9">
101 100
102 <ng-container formGroupName="live"> 101 <ng-container formGroupName="live">
103 <ng-container formGroupName="transcoding"> 102 <ng-container formGroupName="transcoding">
@@ -115,7 +114,7 @@
115 <div class="form-group" [ngClass]="getDisabledLiveTranscodingClass()"> 114 <div class="form-group" [ngClass]="getDisabledLiveTranscodingClass()">
116 <label i18n for="liveTranscodingThreads">Live resolutions to generate</label> 115 <label i18n for="liveTranscodingThreads">Live resolutions to generate</label>
117 116
118 <div class="ml-2 mt-2 d-flex flex-column"> 117 <div class="ms-2 mt-2 d-flex flex-column">
119 <ng-container formGroupName="resolutions"> 118 <ng-container formGroupName="resolutions">
120 119
121 <div class="form-group" *ngFor="let resolution of liveResolutions"> 120 <div class="form-group" *ngFor="let resolution of liveResolutions">
@@ -136,7 +135,7 @@
136 <div class="form-group" [ngClass]="getDisabledLiveTranscodingClass()"> 135 <div class="form-group" [ngClass]="getDisabledLiveTranscodingClass()">
137 <label i18n for="liveTranscodingThreads">Live transcoding threads</label> 136 <label i18n for="liveTranscodingThreads">Live transcoding threads</label>
138 137
139 <span class="muted ml-1"> 138 <span class="small muted ms-1">
140 <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n> 139 <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n>
141 will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with VOD transcoding 140 will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with VOD transcoding
142 </ng-container> 141 </ng-container>
@@ -157,7 +156,7 @@
157 156
158 <div class="form-group mt-4" [ngClass]="getDisabledLiveTranscodingClass()"> 157 <div class="form-group mt-4" [ngClass]="getDisabledLiveTranscodingClass()">
159 <label i18n for="liveTranscodingProfile">Live transcoding profile</label> 158 <label i18n for="liveTranscodingProfile">Live transcoding profile</label>
160 <span class="muted ml-1" i18n>new live transcoding profiles can be added by PeerTube plugins</span> 159 <span class="small muted ms-1" i18n>new live transcoding profiles can be added by PeerTube plugins</span>
161 160
162 <my-select-options 161 <my-select-options
163 id="liveTranscodingProfile" 162 id="liveTranscodingProfile"
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html
index 5c0bea4a5..66e421b16 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html
@@ -1,28 +1,23 @@
1<ng-container [formGroup]="form"> 1<ng-container [formGroup]="form">
2 2
3 <div class="form-row mt-4"> <!-- transcoding grid --> 3 <div class="row mt-4"> <!-- transcoding grid -->
4 <div class="form-group col-12 col-lg-4 col-xl-3"></div> 4 <div class="col-12 col-lg-4 col-xl-3"></div>
5 <div class="form-group form-group-right col-12 col-lg-8"> 5 <div class="col-12 col-lg-8">
6 6
7 <div class="callout callout-info"> 7 <div class="callout callout-orange">
8 <span i18n> 8 <span i18n>
9 Estimating a server's capacity to transcode and stream videos isn't easy and we can't tune PeerTube automatically. 9 Estimating a server's capacity to transcode and stream videos isn't easy and we can't tune PeerTube automatically.
10 </span> 10 </span>
11
11 <span i18n> 12 <span i18n>
12 However, you may want to read our guidelines before tweaking the following values. 13 However, you may want to read <a class="link-orange" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/admin-configuration?id=transcoding">our guidelines</a> before tweaking the following values.
13 </span> 14 </span>
14
15 <div class="callout-container">
16 <a class="callout-link" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/admin-configuration?id=transcoding" i18n>
17 Read guidelines
18 </a>
19 </div>
20 </div> 15 </div>
21 </div> 16 </div>
22 </div> 17 </div>
23 18
24 <div class="form-row mt-2"> <!-- transcoding grid --> 19 <div class="row mt-4"> <!-- transcoding grid -->
25 <div class="form-group col-12 col-lg-4 col-xl-3"> 20 <div class="col-12 col-lg-4 col-xl-3">
26 <div i18n class="inner-form-title">TRANSCODING</div> 21 <div i18n class="inner-form-title">TRANSCODING</div>
27 <div i18n class="inner-form-description"> 22 <div i18n class="inner-form-description">
28 Process uploaded videos so that they are in a streamable form that any device can play. Though costly in 23 Process uploaded videos so that they are in a streamable form that any device can play. Though costly in
@@ -30,11 +25,11 @@
30 </div> 25 </div>
31 </div> 26 </div>
32 27
33 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 28 <div class="col-12 col-lg-8 col-xl-9">
34 29
35 <ng-container formGroupName="transcoding"> 30 <ng-container formGroupName="transcoding">
36 31
37 <div class="form-group mb-0 col-12 col-xl-11"> 32 <div class="col-12 col-xl-11">
38 <my-peertube-checkbox inputName="transcodingEnabled" formControlName="enabled" [recommended]="true"> 33 <my-peertube-checkbox inputName="transcodingEnabled" formControlName="enabled" [recommended]="true">
39 <ng-template ptTemplate="label"> 34 <ng-template ptTemplate="label">
40 <ng-container i18n>Transcoding enabled</ng-container> 35 <ng-container i18n>Transcoding enabled</ng-container>
@@ -115,7 +110,11 @@
115 <div class="form-group" [ngClass]="getTranscodingDisabledClass()"> 110 <div class="form-group" [ngClass]="getTranscodingDisabledClass()">
116 <label i18n>Resolutions to generate per enabled format</label> 111 <label i18n>Resolutions to generate per enabled format</label>
117 112
118 <div class="ml-2 mt-2 d-flex flex-column"> 113 <div class="ms-2 d-flex flex-column">
114 <span class="mb-3 small muted" i18n>
115 The original file resolution will be the default target if no option is selected.
116 </span>
117
119 <ng-container formGroupName="resolutions"> 118 <ng-container formGroupName="resolutions">
120 <div class="form-group" *ngFor="let resolution of resolutions"> 119 <div class="form-group" *ngFor="let resolution of resolutions">
121 <my-peertube-checkbox 120 <my-peertube-checkbox
@@ -127,10 +126,6 @@
127 </ng-template> 126 </ng-template>
128 </my-peertube-checkbox> 127 </my-peertube-checkbox>
129 </div> 128 </div>
130
131 <span class="mb-2 muted" i18n>
132 The original file resolution will be the default target if no option is selected.
133 </span>
134 </ng-container> 129 </ng-container>
135 </div> 130 </div>
136 </div> 131 </div>
@@ -142,7 +137,8 @@
142 137
143 <div class="form-group mt-4" [ngClass]="getTranscodingDisabledClass()"> 138 <div class="form-group mt-4" [ngClass]="getTranscodingDisabledClass()">
144 <label i18n for="transcodingThreads">Transcoding threads</label> 139 <label i18n for="transcodingThreads">Transcoding threads</label>
145 <span class="muted ml-1"> 140
141 <span class="small muted ms-1">
146 <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n> 142 <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n>
147 will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with live transcoding 143 will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with live transcoding
148 </ng-container> 144 </ng-container>
@@ -162,9 +158,9 @@
162 <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div> 158 <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
163 </div> 159 </div>
164 160
165 <div class="form-group mt-4" [ngClass]="getTranscodingDisabledClass()"> 161 <div class="form-group" [ngClass]="getTranscodingDisabledClass()">
166 <label i18n for="transcodingConcurrency">Transcoding jobs concurrency</label> 162 <label i18n for="transcodingConcurrency">Transcoding jobs concurrency</label>
167 <span class="muted ml-1" i18n>allows to transcode multiple files in parallel. ⚠️ Requires a PeerTube restart</span> 163 <span class="small muted ms-1" i18n>allows to transcode multiple files in parallel. ⚠️ Requires a PeerTube restart</span>
168 164
169 <div class="number-with-unit"> 165 <div class="number-with-unit">
170 <input type="number" name="transcodingConcurrency" formControlName="concurrency" /> 166 <input type="number" name="transcodingConcurrency" formControlName="concurrency" />
@@ -174,9 +170,9 @@
174 <div *ngIf="formErrors.transcoding.concurrency" class="form-error">{{ formErrors.transcoding.concurrency }}</div> 170 <div *ngIf="formErrors.transcoding.concurrency" class="form-error">{{ formErrors.transcoding.concurrency }}</div>
175 </div> 171 </div>
176 172
177 <div class="form-group mt-4" [ngClass]="getTranscodingDisabledClass()"> 173 <div class="form-group" [ngClass]="getTranscodingDisabledClass()">
178 <label i18n for="transcodingProfile">Transcoding profile</label> 174 <label i18n for="transcodingProfile">Transcoding profile</label>
179 <span class="muted ml-1" i18n>new transcoding profiles can be added by PeerTube plugins</span> 175 <span class="small muted ms-1" i18n>new transcoding profiles can be added by PeerTube plugins</span>
180 176
181 <my-select-options 177 <my-select-options
182 id="transcodingProfile" 178 id="transcodingProfile"
@@ -193,15 +189,15 @@
193 </div> 189 </div>
194 </div> 190 </div>
195 191
196 <div class="form-row mt-2"> <!-- video studio grid --> 192 <div class="row mt-2"> <!-- video studio grid -->
197 <div class="form-group col-12 col-lg-4 col-xl-3"> 193 <div class="col-12 col-lg-4 col-xl-3">
198 <div i18n class="inner-form-title">VIDEO STUDIO</div> 194 <div i18n class="inner-form-title">VIDEO STUDIO</div>
199 <div i18n class="inner-form-description"> 195 <div i18n class="inner-form-description">
200 Allows your users to edit their video (cut, add intro/outro, add a watermark etc) 196 Allows your users to edit their video (cut, add intro/outro, add a watermark etc)
201 </div> 197 </div>
202 </div> 198 </div>
203 199
204 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 200 <div class="col-12 col-lg-8 col-xl-9">
205 201
206 <ng-container formGroupName="videoStudio"> 202 <ng-container formGroupName="videoStudio">
207 <div class="form-group" [ngClass]="getTranscodingDisabledClass()"> 203 <div class="form-group" [ngClass]="getTranscodingDisabledClass()">
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html
index 1df7bb164..5367bf517 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.html
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html
@@ -12,7 +12,7 @@
12> 12>
13 <ng-template pTemplate="caption"> 13 <ng-template pTemplate="caption">
14 <div class="caption"> 14 <div class="caption">
15 <div class="ml-auto"> 15 <div class="ms-auto">
16 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 16 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
17 </div> 17 </div>
18 </div> 18 </div>
@@ -41,15 +41,15 @@
41 <td> 41 <td>
42 <a [href]="follow.follower.url" i18n-title title="Open actor page in a new tab" target="_blank" rel="noopener noreferrer"> 42 <a [href]="follow.follower.url" i18n-title title="Open actor page in a new tab" target="_blank" rel="noopener noreferrer">
43 {{ follow.follower.name + '@' + follow.follower.host }} 43 {{ follow.follower.name + '@' + follow.follower.host }}
44 <span class="glyphicon glyphicon-new-window"></span> 44 <my-global-icon iconName="external-link"></my-global-icon>
45 </a> 45 </a>
46 </td> 46 </td>
47 47
48 <td *ngIf="follow.state === 'accepted'"> 48 <td *ngIf="follow.state === 'accepted'">
49 <span class="badge badge-green" i18n>Accepted</span> 49 <span class="pt-badge badge-green" i18n>Accepted</span>
50 </td> 50 </td>
51 <td *ngIf="follow.state === 'pending'"> 51 <td *ngIf="follow.state === 'pending'">
52 <span class="badge badge-yellow" i18n>Pending</span> 52 <span class="pt-badge badge-yellow" i18n>Pending</span>
53 </td> 53 </td>
54 54
55 <td>{{ follow.score }}</td> 55 <td>{{ follow.score }}</td>
@@ -59,7 +59,7 @@
59 59
60 <ng-template pTemplate="emptymessage"> 60 <ng-template pTemplate="emptymessage">
61 <tr> 61 <tr>
62 <td colspan="6"> 62 <td colspan="5">
63 <div class="no-results"> 63 <div class="no-results">
64 <ng-container *ngIf="search" i18n>No follower found matching current filters.</ng-container> 64 <ng-container *ngIf="search" i18n>No follower found matching current filters.</ng-container>
65 <ng-container *ngIf="!search" i18n>Your instance doesn't have any follower.</ng-container> 65 <ng-container *ngIf="!search" i18n>Your instance doesn't have any follower.</ng-container>
diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.ts b/client/src/app/+admin/follows/following-list/follow-modal.component.ts
index c40b36e10..07cc75d77 100644
--- a/client/src/app/+admin/follows/following-list/follow-modal.component.ts
+++ b/client/src/app/+admin/follows/following-list/follow-modal.component.ts
@@ -1,5 +1,6 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { prepareIcu } from '@app/helpers'
3import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' 4import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { InstanceFollowService } from '@app/shared/shared-instance' 6import { InstanceFollowService } from '@app/shared/shared-instance'
@@ -60,7 +61,13 @@ export class FollowModalComponent extends FormReactive implements OnInit {
60 this.followService.follow(hostsOrHandles) 61 this.followService.follow(hostsOrHandles)
61 .subscribe({ 62 .subscribe({
62 next: () => { 63 next: () => {
63 this.notifier.success($localize`Follow request(s) sent!`) 64 this.notifier.success(
65 prepareIcu($localize`{count, plural, =1 {Follow request sent!} other {Follow requests sent!}}`)(
66 { count: hostsOrHandles.length },
67 $localize`Follow request(s) sent!`
68 )
69 )
70
64 this.newFollow.emit() 71 this.newFollow.emit()
65 }, 72 },
66 73
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html
index 767e92d18..106e1805e 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.html
+++ b/client/src/app/+admin/follows/following-list/following-list.component.html
@@ -19,7 +19,7 @@
19 </a> 19 </a>
20 </div> 20 </div>
21 21
22 <div class="ml-auto"> 22 <div class="ms-auto">
23 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 23 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
24 </div> 24 </div>
25 </div> 25 </div>
@@ -43,15 +43,15 @@
43 <td> 43 <td>
44 <a [href]="follow.following.url" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer"> 44 <a [href]="follow.following.url" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
45 {{ follow.following.name + '@' + follow.following.host }} 45 {{ follow.following.name + '@' + follow.following.host }}
46 <span class="glyphicon glyphicon-new-window"></span> 46 <my-global-icon iconName="external-link"></my-global-icon>
47 </a> 47 </a>
48 </td> 48 </td>
49 49
50 <td *ngIf="follow.state === 'accepted'"> 50 <td *ngIf="follow.state === 'accepted'">
51 <span class="badge badge-green" i18n>Accepted</span> 51 <span class="pt-badge badge-green" i18n>Accepted</span>
52 </td> 52 </td>
53 <td *ngIf="follow.state === 'pending'"> 53 <td *ngIf="follow.state === 'pending'">
54 <span class="badge badge-yellow" i18n>Pending</span> 54 <span class="pt-badge badge-yellow" i18n>Pending</span>
55 </td> 55 </td>
56 56
57 <td>{{ follow.createdAt | date: 'short' }}</td> 57 <td>{{ follow.createdAt | date: 'short' }}</td>
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
index f13a0c378..12b07da11 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
@@ -46,7 +46,7 @@
46 <td> 46 <td>
47 <a [href]="redundancy.url" i18n-title title="Open video in a new tab" target="_blank" rel="noopener noreferrer"> 47 <a [href]="redundancy.url" i18n-title title="Open video in a new tab" target="_blank" rel="noopener noreferrer">
48 {{ redundancy.name }} 48 {{ redundancy.name }}
49 <span class="glyphicon glyphicon-new-window"></span> 49 <my-global-icon iconName="external-link"></my-global-icon>
50 </a> 50 </a>
51 </td> 51 </td>
52 52
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
index 3634951c9..b302014b6 100644
--- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
@@ -13,7 +13,7 @@
13> 13>
14 <ng-template pTemplate="caption"> 14 <ng-template pTemplate="caption">
15 <div class="caption"> 15 <div class="caption">
16 <div class="ml-auto"> 16 <div class="ms-auto">
17 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> 17 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
18 </div> 18 </div>
19 </div> 19 </div>
@@ -53,11 +53,11 @@
53 </td> 53 </td>
54 54
55 <td> 55 <td>
56 <span *ngIf="videoBlock.video.nsfw" class="badge badge-red" i18n>NSFW</span> 56 <span *ngIf="videoBlock.video.nsfw" class="pt-badge badge-red" i18n>NSFW</span>
57 </td> 57 </td>
58 58
59 <td> 59 <td>
60 <span *ngIf="videoBlock.unfederated" class="badge badge-blue" i18n>Unfederated</span> 60 <span *ngIf="videoBlock.unfederated" class="pt-badge badge-blue" i18n>Unfederated</span>
61 </td> 61 </td>
62 62
63 <td> 63 <td>
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.html b/client/src/app/+admin/overview/comments/video-comment-list.component.html
index 27a5d82ff..b7fc0a1eb 100644
--- a/client/src/app/+admin/overview/comments/video-comment-list.component.html
+++ b/client/src/app/+admin/overview/comments/video-comment-list.component.html
@@ -25,7 +25,7 @@
25 </my-action-dropdown> 25 </my-action-dropdown>
26 </div> 26 </div>
27 27
28 <div class="ml-auto right-form"> 28 <div class="ms-auto right-form">
29 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> 29 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
30 30
31 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button> 31 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>
@@ -68,7 +68,7 @@
68 <td> 68 <td>
69 <a [href]="videoComment.account.localUrl" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> 69 <a [href]="videoComment.account.localUrl" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
70 <div class="chip two-lines"> 70 <div class="chip two-lines">
71 <my-actor-avatar [account]="videoComment.account" size="32"></my-actor-avatar> 71 <my-actor-avatar [actor]="videoComment.account" actorType="account" size="32"></my-actor-avatar>
72 <div> 72 <div>
73 {{ videoComment.account.displayName }} 73 {{ videoComment.account.displayName }}
74 <span>{{ videoComment.by }}</span> 74 <span>{{ videoComment.by }}</span>
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.ts b/client/src/app/+admin/overview/comments/video-comment-list.component.ts
index f3f43a900..f01a1629b 100644
--- a/client/src/app/+admin/overview/comments/video-comment-list.component.ts
+++ b/client/src/app/+admin/overview/comments/video-comment-list.component.ts
@@ -7,6 +7,7 @@ import { DropdownAction } from '@app/shared/shared-main'
7import { BulkService } from '@app/shared/shared-moderation' 7import { BulkService } from '@app/shared/shared-moderation'
8import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' 8import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment'
9import { FeedFormat, UserRight } from '@shared/models' 9import { FeedFormat, UserRight } from '@shared/models'
10import { prepareIcu } from '@app/helpers'
10 11
11@Component({ 12@Component({
12 selector: 'my-video-comment-list', 13 selector: 'my-video-comment-list',
@@ -145,7 +146,13 @@ export class VideoCommentListComponent extends RestTable implements OnInit {
145 this.videoCommentService.deleteVideoComments(commentArgs) 146 this.videoCommentService.deleteVideoComments(commentArgs)
146 .subscribe({ 147 .subscribe({
147 next: () => { 148 next: () => {
148 this.notifier.success($localize`${commentArgs.length} comments deleted.`) 149 this.notifier.success(
150 prepareIcu($localize`{count, plural, =1 {1 comment deleted.} other {{count} comments deleted.}}`)(
151 { count: commentArgs.length },
152 $localize`${commentArgs.length} comment(s) deleted.`
153 )
154 )
155
149 this.reloadData() 156 this.reloadData()
150 }, 157 },
151 158
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html
index 772ebf272..e484ab8b0 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html
@@ -57,7 +57,7 @@
57 </div> 57 </div>
58</ng-template> 58</ng-template>
59 59
60<div class="form-row" *ngIf="!isInBigView()"> <!-- hidden on large screens, as it is then displayed on the right side of the form --> 60<div class="row d-xxl-none"> <!-- hidden on large screens, as it is then displayed on the right side of the form -->
61 <div class="col-12 col-xl-3"></div> 61 <div class="col-12 col-xl-3"></div>
62 62
63 <div class="col-12 col-xl-9"> 63 <div class="col-12 col-xl-9">
@@ -67,8 +67,8 @@
67 67
68<div *ngIf="error" class="alert alert-danger">{{ error }}</div> 68<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
69 69
70<div class="form-row mt-4"> <!-- user grid --> 70<div class="row mt-4"> <!-- user grid -->
71 <div class="form-group col-12 col-lg-4 col-xl-3"> 71 <div class="col-12 col-lg-4 col-xl-3">
72 <div class="anchor" id="user"></div> <!-- user anchor --> 72 <div class="anchor" id="user"></div> <!-- user anchor -->
73 <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div> 73 <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div>
74 <div *ngIf="!isCreation() && user" class="account-title"> 74 <div *ngIf="!isCreation() && user" class="account-title">
@@ -76,150 +76,144 @@
76 </div> 76 </div>
77 </div> 77 </div>
78 78
79 <div class="form-group col-12 col-lg-8 col-xl-9" [ngClass]="{ 'form-row': isInBigView() }"> 79 <div class="col-12 col-lg-8 col-xl-9">
80 80 <div class="row">
81 <form role="form" (ngSubmit)="formValidated()" [formGroup]="form" [ngClass]="{ 'col-5': isInBigView() }"> 81 <form class="col" role="form" (ngSubmit)="formValidated()" [formGroup]="form">
82 <div class="form-group" *ngIf="isCreation()"> 82 <div class="form-group" *ngIf="isCreation()">
83 <label i18n for="username">Username</label> 83 <label i18n for="username">Username</label>
84 <input 84 <input
85 type="text" id="username" i18n-placeholder placeholder="john" class="form-control" 85 type="text" id="username" i18n-placeholder placeholder="john" class="form-control"
86 formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" 86 formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
87 > 87 >
88 <div *ngIf="formErrors.username" class="form-error"> 88 <div *ngIf="formErrors.username" class="form-error">
89 {{ formErrors.username }} 89 {{ formErrors.username }}
90 </div>
90 </div> 91 </div>
91 </div>
92 92
93 <div class="form-group" *ngIf="isCreation()"> 93 <div class="form-group" *ngIf="isCreation()">
94 <label i18n for="channelName">Channel name</label> 94 <label i18n for="channelName">Channel name</label>
95 <input 95 <input
96 type="text" id="channelName" i18n-placeholder placeholder="john_channel" class="form-control" 96 type="text" id="channelName" i18n-placeholder placeholder="john_channel" class="form-control"
97 formControlName="channelName" [ngClass]="{ 'input-error': formErrors['channelName'] }" 97 formControlName="channelName" [ngClass]="{ 'input-error': formErrors['channelName'] }"
98 > 98 >
99 <div *ngIf="formErrors.channelName" class="form-error"> 99 <div *ngIf="formErrors.channelName" class="form-error">
100 {{ formErrors.channelName }} 100 {{ formErrors.channelName }}
101 </div>
101 </div> 102 </div>
102 </div>
103 103
104 <div class="form-group"> 104 <div class="form-group">
105 <label i18n for="email">Email</label> 105 <label i18n for="email">Email</label>
106 <input 106 <input
107 type="text" id="email" i18n-placeholder placeholder="mail@example.com" class="form-control" 107 type="text" id="email" i18n-placeholder placeholder="mail@example.com" class="form-control"
108 formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" 108 formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }"
109 autocomplete="off" [readonly]="user && user.pluginAuth !== null" 109 autocomplete="off" [readonly]="user && user.pluginAuth !== null"
110 > 110 >
111 <div *ngIf="formErrors.email" class="form-error"> 111 <div *ngIf="formErrors.email" class="form-error">
112 {{ formErrors.email }} 112 {{ formErrors.email }}
113 </div>
113 </div> 114 </div>
114 </div>
115 115
116 <div class="form-group" *ngIf="isCreation()"> 116 <div class="form-group" *ngIf="isCreation()">
117 <label i18n for="password">Password</label> 117 <label i18n for="password">Password</label>
118 <my-help *ngIf="isPasswordOptional()"> 118 <my-help *ngIf="isPasswordOptional()">
119 <ng-template ptTemplate="customHtml"> 119 <ng-template ptTemplate="customHtml">
120 <ng-container i18n> 120 <ng-container i18n>
121 If you leave the password empty, an email will be sent to the user. 121 If you leave the password empty, an email will be sent to the user.
122 </ng-container> 122 </ng-container>
123 </ng-template> 123 </ng-template>
124 </my-help> 124 </my-help>
125 125
126 <my-input-toggle-hidden 126 <my-input-text formControlName="password" inputId="password" [formError]="formErrors['password']" autocomplete="new-password"></my-input-text>
127 formControlName="password" inputId="password" [ngClass]="{ 'input-error': formErrors['password'] }" autocomplete="new-password"
128 ></my-input-toggle-hidden>
129
130 <div *ngIf="formErrors.password" class="form-error">
131 {{ formErrors.password }}
132 </div> 127 </div>
133 </div>
134 128
135 <div class="form-group"> 129 <div class="form-group">
136 <label i18n for="role">Role</label> 130 <label i18n for="role">Role</label>
137 <div class="peertube-select-container"> 131 <div class="peertube-select-container">
138 <select id="role" formControlName="role" class="form-control"> 132 <select id="role" formControlName="role" class="form-control">
139 <option *ngFor="let role of roles" [value]="role.value"> 133 <option *ngFor="let role of roles" [value]="role.value">
140 {{ role.label }} 134 {{ role.label }}
141 </option> 135 </option>
142 </select> 136 </select>
137 </div>
138
139 <div *ngIf="formErrors.role" class="form-error">
140 {{ formErrors.role }}
141 </div>
143 </div> 142 </div>
144 143
145 <div *ngIf="formErrors.role" class="form-error"> 144 <div class="form-group">
146 {{ formErrors.role }} 145 <label i18n for="videoQuota">Video quota</label>
146
147 <my-select-custom-value
148 id="videoQuota"
149 [items]="videoQuotaOptions"
150 formControlName="videoQuota"
151 i18n-inputSuffix inputSuffix="bytes" inputType="number"
152 [clearable]="false"
153 ></my-select-custom-value>
154
155 <div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
156 Transcoding is enabled. The video quota only takes into account <strong>original</strong> video size. <br />
157 At most, this user could upload ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
158 </div>
159
160 <div *ngIf="formErrors.videoQuota" class="form-error">
161 {{ formErrors.videoQuota }}
162 </div>
147 </div> 163 </div>
148 </div>
149 164
150 <div class="form-group"> 165 <div class="form-group">
151 <label i18n for="videoQuota">Video quota</label> 166 <label i18n for="videoQuotaDaily">Daily video quota</label>
152
153 <my-select-custom-value
154 id="videoQuota"
155 [items]="videoQuotaOptions"
156 formControlName="videoQuota"
157 i18n-inputSuffix inputSuffix="bytes" inputType="number"
158 [clearable]="false"
159 ></my-select-custom-value>
160
161 <div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
162 Transcoding is enabled. The video quota only takes into account <strong>original</strong> video size. <br />
163 At most, this user could upload ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
164 </div>
165 167
166 <div *ngIf="formErrors.videoQuota" class="form-error"> 168 <my-select-custom-value
167 {{ formErrors.videoQuota }} 169 id="videoQuotaDaily"
168 </div> 170 [items]="videoQuotaDailyOptions"
169 </div> 171 formControlName="videoQuotaDaily"
172 i18n-inputSuffix inputSuffix="bytes" inputType="number"
173 [clearable]="false"
174 ></my-select-custom-value>
170 175
171 <div class="form-group"> 176 <div *ngIf="formErrors.videoQuotaDaily" class="form-error">
172 <label i18n for="videoQuotaDaily">Daily video quota</label> 177 {{ formErrors.videoQuotaDaily }}
173 178 </div>
174 <my-select-custom-value
175 id="videoQuotaDaily"
176 [items]="videoQuotaDailyOptions"
177 formControlName="videoQuotaDaily"
178 i18n-inputSuffix inputSuffix="bytes" inputType="number"
179 [clearable]="false"
180 ></my-select-custom-value>
181
182 <div *ngIf="formErrors.videoQuotaDaily" class="form-error">
183 {{ formErrors.videoQuotaDaily }}
184 </div> 179 </div>
185 </div>
186 180
187 <div class="form-group" *ngIf="!isCreation() && getAuthPlugins().length !== 0"> 181 <div class="form-group" *ngIf="!isCreation() && getAuthPlugins().length !== 0">
188 <label i18n for="pluginAuth">Auth plugin</label> 182 <label i18n for="pluginAuth">Auth plugin</label>
189 183
190 <div class="peertube-select-container"> 184 <div class="peertube-select-container">
191 <select id="pluginAuth" formControlName="pluginAuth" class="form-control"> 185 <select id="pluginAuth" formControlName="pluginAuth" class="form-control">
192 <option [value]="null" i18n>None (local authentication)</option> 186 <option [value]="null" i18n>None (local authentication)</option>
193 <option *ngFor="let authPlugin of getAuthPlugins()" [value]="authPlugin">{{ authPlugin }}</option> 187 <option *ngFor="let authPlugin of getAuthPlugins()" [value]="authPlugin">{{ authPlugin }}</option>
194 </select> 188 </select>
189 </div>
195 </div> 190 </div>
196 </div>
197 191
198 <div class="form-group"> 192 <div class="form-group">
199 <my-peertube-checkbox 193 <my-peertube-checkbox
200 inputName="byPassAutoBlock" formControlName="byPassAutoBlock" 194 inputName="byPassAutoBlock" formControlName="byPassAutoBlock"
201 i18n-labelText labelText="Doesn't need review before a video goes public" 195 i18n-labelText labelText="Doesn't need review before a video goes public"
202 ></my-peertube-checkbox> 196 ></my-peertube-checkbox>
203 </div> 197 </div>
204 198
205 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 199 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
206 </form> 200 </form>
207 201
208 <div *ngIf="isInBigView()" class="col-7"> 202 <div class="d-none d-xxl-block col-7">
209 <ng-template *ngTemplateOutlet="dashboard"></ng-template> 203 <ng-template *ngTemplateOutlet="dashboard"></ng-template>
204 </div>
210 </div> 205 </div>
211
212 </div> 206 </div>
213</div> 207</div>
214 208
215 209
216<div *ngIf="!isCreation() && user && user.pluginAuth === null" class="form-row mt-4"> <!-- danger zone grid --> 210<div *ngIf="!isCreation() && user && user.pluginAuth === null" class="row mt-4"> <!-- danger zone grid -->
217 <div class="form-group col-12 col-lg-4 col-xl-3"> 211 <div class="col-12 col-lg-4 col-xl-3">
218 <div class="anchor" id="danger"></div> <!-- danger zone anchor --> 212 <div class="anchor" id="danger"></div> <!-- danger zone anchor -->
219 <div i18n class="account-title account-title-danger">DANGER ZONE</div> 213 <div i18n class="account-title account-title-danger">DANGER ZONE</div>
220 </div> 214 </div>
221 215
222 <div class="form-group col-12 col-lg-8 col-xl-9" [ngClass]="{ 'form-row': isInBigView() }"> 216 <div class="col-12 col-lg-8 col-xl-9">
223 217
224 <div class="danger-zone"> 218 <div class="danger-zone">
225 <div class="form-group reset-password-email"> 219 <div class="form-group reset-password-email">
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss b/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
index d7932154b..254286ae3 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
@@ -4,11 +4,6 @@
4 4
5$form-base-input-width: 340px; 5$form-base-input-width: 340px;
6 6
7label {
8 font-weight: $font-regular;
9 font-size: 100%;
10}
11
12.account-title { 7.account-title {
13 @include settings-big-title; 8 @include settings-big-title;
14 9
@@ -22,7 +17,7 @@ input:not([type=submit]) {
22 display: block; 17 display: block;
23} 18}
24 19
25my-input-toggle-hidden { 20my-input-text {
26 @include responsive-width($form-base-input-width); 21 @include responsive-width($form-base-input-width);
27 22
28 display: block; 23 display: block;
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.ts b/client/src/app/+admin/overview/users/user-edit/user-edit.ts
index 069b62a53..395d07423 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.ts
@@ -46,10 +46,6 @@ export abstract class UserEdit extends FormReactive implements OnInit {
46 .concat(this.serverConfig.plugin.registeredExternalAuths.map(p => p.npmName)) 46 .concat(this.serverConfig.plugin.registeredExternalAuths.map(p => p.npmName))
47 } 47 }
48 48
49 isInBigView () {
50 return this.screenService.getWindowInnerWidth() > 1600
51 }
52
53 buildRoles () { 49 buildRoles () {
54 const authUser = this.auth.getUser() 50 const authUser = this.auth.getUser()
55 51
diff --git a/client/src/app/+admin/overview/users/user-edit/user-password.component.html b/client/src/app/+admin/overview/users/user-edit/user-password.component.html
index 1238d1839..13f57024b 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-password.component.html
+++ b/client/src/app/+admin/overview/users/user-edit/user-password.component.html
@@ -1,20 +1,17 @@
1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
2 <div class="form-group">
3 2
4 <div class="input-group"> 3 <div class="input-group">
5 <input id="password" [attr.type]="showPassword ? 'text' : 'password'" class="form-control" 4 <input id="password" [attr.type]="showPassword ? 'text' : 'password'" class="form-control"
6 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" 5 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
7 > 6 >
8 <div class="input-group-append"> 7 <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
9 <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button"> 8 <ng-container *ngIf="!showPassword" i18n>Show</ng-container>
10 <ng-container *ngIf="!showPassword" i18n>Show</ng-container> 9 <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
11 <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container> 10 </button>
12 </button> 11 </div>
13 </div> 12
14 </div> 13 <div *ngIf="formErrors.password" class="form-error">
15 <div *ngIf="formErrors.password" class="form-error"> 14 {{ formErrors.password }}
16 {{ formErrors.password }}
17 </div>
18 </div> 15 </div>
19 16
20 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 17 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
diff --git a/client/src/app/+admin/overview/users/user-edit/user-password.component.scss b/client/src/app/+admin/overview/users/user-edit/user-password.component.scss
index acb680682..54f782086 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-password.component.scss
+++ b/client/src/app/+admin/overview/users/user-edit/user-password.component.scss
@@ -1,13 +1,9 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4input:not([type=submit]):not([type=checkbox]) { 4input[type=text],
5input[type=password] {
5 @include peertube-input-text(340px); 6 @include peertube-input-text(340px);
6
7 display: block;
8 border-top-right-radius: 0;
9 border-bottom-right-radius: 0;
10 border-right: 0;
11} 7}
12 8
13input[type=submit] { 9input[type=submit] {
@@ -17,7 +13,3 @@ input[type=submit] {
17 13
18 margin-top: 10px; 14 margin-top: 10px;
19} 15}
20
21.input-group-append {
22 height: 30px;
23}
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.html b/client/src/app/+admin/overview/users/user-list/user-list.component.html
index 30d10e3cf..aa3def63a 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.html
@@ -5,7 +5,7 @@
5 5
6<p-table 6<p-table
7 [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" 7 [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
8 [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" [(selection)]="selectedUsers" 8 [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" [(selection)]="selectedUsers"
9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" 9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true"
10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users" 11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users"
@@ -26,7 +26,7 @@
26 </a> 26 </a>
27 </div> 27 </div>
28 28
29 <div class="ml-auto"> 29 <div class="ms-auto">
30 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> 30 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
31 </div> 31 </div>
32 32
@@ -90,24 +90,24 @@
90 </my-user-moderation-dropdown> 90 </my-user-moderation-dropdown>
91 </td> 91 </td>
92 92
93 <td *ngIf="isSelected('username')"> 93 <td *ngIf="isSelected('username')" class="cell-username">
94 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/a/' + user.username ]"> 94 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/a/' + user.username ]">
95 <div class="chip two-lines"> 95 <div class="chip two-lines">
96 <my-actor-avatar [account]="user?.account" size="32"></my-actor-avatar> 96 <my-actor-avatar [actor]="user?.account" actorType="account" size="32"></my-actor-avatar>
97 <div> 97 <div>
98 <span class="user-table-primary-text">{{ user.account.displayName }}</span> 98 <span class="user-table-primary-text">{{ user.account.displayName }}</span>
99 <span class="muted">{{ user.username }}</span> 99 <span class="muted">{{ user.username }}</span>
100 </div> 100 </div>
101 </div> 101 </div>
102 </a> 102 </a>
103 103
104 <div *ngIf="user.accountMutedStatus.mutedByInstance" class="badges-username badge badge-red" i18n>Muted</div> 104 <div *ngIf="user.accountMutedStatus.mutedByInstance" class="pt-badge badge-red" i18n>Muted</div>
105 <div *ngIf="user.blocked" class="badges-username badge badge-red" i18n>Banned</div> 105 <div *ngIf="user.blocked" class="pt-badge badge-red" i18n>Banned</div>
106 </td> 106 </td>
107 107
108 <td *ngIf="isSelected('role')"> 108 <td *ngIf="isSelected('role')">
109 <span *ngIf="user.blocked" class="badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span> 109 <span *ngIf="user.blocked" class="pt-badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span>
110 <span *ngIf="!user.blocked" class="badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span> 110 <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span>
111 </td> 111 </td>
112 112
113 <td *ngIf="isSelected('email')" [title]="user.email"> 113 <td *ngIf="isSelected('email')" [title]="user.email">
@@ -139,7 +139,7 @@
139 139
140 <td *ngIf="isSelected('quotaDaily')"> 140 <td *ngIf="isSelected('quotaDaily')">
141 <div class="progress" i18n-title title="Total daily video quota"> 141 <div class="progress" i18n-title title="Total daily video quota">
142 <div class="progress-bar secondary" role="progressbar" [style]="{ width: getUserVideoQuotaDailyPercentage(user) + '%' }" 142 <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaDailyPercentage(user) + '%' }"
143 [attr.aria-valuenow]="user.rawVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuotaDaily"> 143 [attr.aria-valuenow]="user.rawVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuotaDaily">
144 </div> 144 </div>
145 <span>{{ user.videoQuotaUsedDaily }}</span> 145 <span>{{ user.videoQuotaUsedDaily }}</span>
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.scss b/client/src/app/+admin/overview/users/user-list/user-list.component.scss
index 8160703f0..3c775cac5 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.scss
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.scss
@@ -1,5 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3@use '~bootstrap/scss/functions' as *;
3 4
4.add-button { 5.add-button {
5 @include create-button; 6 @include create-button;
@@ -23,15 +24,8 @@ tr.banned > td {
23 font-weight: $font-semibold; 24 font-weight: $font-semibold;
24} 25}
25 26
26.badges-username { 27.cell-username .pt-badge {
27 margin-left: 15px; 28 @include margin-left(15px);
28}
29
30.user-table-primary-text .glyphicon {
31 @include margin-left(0.1rem);
32
33 font-size: 80%;
34 color: #808080;
35} 29}
36 30
37p-tableCheckbox { 31p-tableCheckbox {
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.ts b/client/src/app/+admin/overview/users/user-list/user-list.component.ts
index d22e1355e..3e1a5f6b8 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.ts
@@ -1,8 +1,8 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { Component, OnInit, ViewChild } from '@angular/core' 2import { Component, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' 4import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
5import { getAPIHost } from '@app/helpers' 5import { prepareIcu, getAPIHost } from '@app/helpers'
6import { AdvancedInputFilter } from '@app/shared/shared-forms' 6import { AdvancedInputFilter } from '@app/shared/shared-forms'
7import { Actor, DropdownAction } from '@app/shared/shared-main' 7import { Actor, DropdownAction } from '@app/shared/shared-main'
8import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation' 8import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation'
@@ -22,6 +22,8 @@ type UserForList = User & {
22 styleUrls: [ './user-list.component.scss' ] 22 styleUrls: [ './user-list.component.scss' ]
23}) 23})
24export class UserListComponent extends RestTable implements OnInit { 24export class UserListComponent extends RestTable implements OnInit {
25 private static readonly LOCAL_STORAGE_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns'
26
25 @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent 27 @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent
26 28
27 users: (User & { accountMutedStatus: AccountMutedStatus })[] = [] 29 users: (User & { accountMutedStatus: AccountMutedStatus })[] = []
@@ -56,7 +58,7 @@ export class UserListComponent extends RestTable implements OnInit {
56 58
57 requiresEmailVerification = false 59 requiresEmailVerification = false
58 60
59 private _selectedColumns: string[] 61 private _selectedColumns: string[] = []
60 62
61 constructor ( 63 constructor (
62 protected route: ActivatedRoute, 64 protected route: ActivatedRoute,
@@ -66,7 +68,8 @@ export class UserListComponent extends RestTable implements OnInit {
66 private serverService: ServerService, 68 private serverService: ServerService,
67 private auth: AuthService, 69 private auth: AuthService,
68 private blocklist: BlocklistService, 70 private blocklist: BlocklistService,
69 private userAdminService: UserAdminService 71 private userAdminService: UserAdminService,
72 private peertubeLocalStorage: LocalStorageService
70 ) { 73 ) {
71 super() 74 super()
72 } 75 }
@@ -76,11 +79,13 @@ export class UserListComponent extends RestTable implements OnInit {
76 } 79 }
77 80
78 get selectedColumns () { 81 get selectedColumns () {
79 return this._selectedColumns 82 return this._selectedColumns || []
80 } 83 }
81 84
82 set selectedColumns (val: string[]) { 85 set selectedColumns (val: string[]) {
83 this._selectedColumns = val 86 this._selectedColumns = val
87
88 this.saveSelectedColumns()
84 } 89 }
85 90
86 ngOnInit () { 91 ngOnInit () {
@@ -126,14 +131,35 @@ export class UserListComponent extends RestTable implements OnInit {
126 { id: 'role', label: $localize`Role` }, 131 { id: 'role', label: $localize`Role` },
127 { id: 'email', label: $localize`Email` }, 132 { id: 'email', label: $localize`Email` },
128 { id: 'quota', label: $localize`Video quota` }, 133 { id: 'quota', label: $localize`Video quota` },
129 { id: 'createdAt', label: $localize`Created` } 134 { id: 'createdAt', label: $localize`Created` },
135 { id: 'lastLoginDate', label: $localize`Last login` },
136
137 { id: 'quotaDaily', label: $localize`Daily quota` },
138 { id: 'pluginAuth', label: $localize`Auth plugin` }
130 ] 139 ]
131 140
132 this.selectedColumns = this.columns.map(c => c.id) 141 this.loadSelectedColumns()
142 }
143
144 loadSelectedColumns () {
145 const result = this.peertubeLocalStorage.getItem(UserListComponent.LOCAL_STORAGE_SELECTED_COLUMNS_KEY)
133 146
134 this.columns.push({ id: 'quotaDaily', label: $localize`Daily quota` }) 147 if (result) {
135 this.columns.push({ id: 'pluginAuth', label: $localize`Auth plugin` }) 148 try {
136 this.columns.push({ id: 'lastLoginDate', label: $localize`Last login` }) 149 this.selectedColumns = JSON.parse(result)
150 return
151 } catch (err) {
152 console.error('Cannot load selected columns.', err)
153 }
154 }
155
156 // Default behaviour
157 this.selectedColumns = [ 'username', 'role', 'email', 'quota', 'createdAt', 'lastLoginDate' ]
158 return
159 }
160
161 saveSelectedColumns () {
162 this.peertubeLocalStorage.setItem(UserListComponent.LOCAL_STORAGE_SELECTED_COLUMNS_KEY, JSON.stringify(this.selectedColumns))
137 } 163 }
138 164
139 getIdentifier () { 165 getIdentifier () {
@@ -183,13 +209,25 @@ export class UserListComponent extends RestTable implements OnInit {
183 } 209 }
184 210
185 async unbanUsers (users: User[]) { 211 async unbanUsers (users: User[]) {
186 const res = await this.confirmService.confirm($localize`Do you really want to unban ${users.length} users?`, $localize`Unban`) 212 const res = await this.confirmService.confirm(
213 prepareIcu($localize`Do you really want to unban {count, plural, =1 {1 user} other {{count} users}}?`)(
214 { count: users.length },
215 $localize`Do you really want to unban ${users.length} users?`
216 ),
217 $localize`Unban`
218 )
219
187 if (res === false) return 220 if (res === false) return
188 221
189 this.userAdminService.unbanUsers(users) 222 this.userAdminService.unbanUsers(users)
190 .subscribe({ 223 .subscribe({
191 next: () => { 224 next: () => {
192 this.notifier.success($localize`${users.length} users unbanned.`) 225 this.notifier.success(
226 prepareIcu($localize`{count, plural, =1 {1 user unbanned.} other {{count} users unbanned.}}`)(
227 { count: users.length },
228 $localize`${users.length} users unbanned.`
229 )
230 )
193 this.reloadData() 231 this.reloadData()
194 }, 232 },
195 233
@@ -198,21 +236,28 @@ export class UserListComponent extends RestTable implements OnInit {
198 } 236 }
199 237
200 async removeUsers (users: User[]) { 238 async removeUsers (users: User[]) {
201 for (const user of users) { 239 if (users.some(u => u.username === 'root')) {
202 if (user.username === 'root') { 240 this.notifier.error($localize`You cannot delete root.`)
203 this.notifier.error($localize`You cannot delete root.`) 241 return
204 return
205 }
206 } 242 }
207 243
208 const message = $localize`If you remove these users, you will not be able to create others with the same username!` 244 const message = $localize`<p>You can't create users or channels with a username that already used by a deleted user/channel.</p>` +
245 $localize`It means the following usernames will be permanently deleted and cannot be recovered:` +
246 '<ul>' + users.map(u => '<li>' + u.username + '</li>').join('') + '</ul>'
247
209 const res = await this.confirmService.confirm(message, $localize`Delete`) 248 const res = await this.confirmService.confirm(message, $localize`Delete`)
210 if (res === false) return 249 if (res === false) return
211 250
212 this.userAdminService.removeUser(users) 251 this.userAdminService.removeUser(users)
213 .subscribe({ 252 .subscribe({
214 next: () => { 253 next: () => {
215 this.notifier.success($localize`${users.length} users deleted.`) 254 this.notifier.success(
255 prepareIcu($localize`{count, plural, =1 {1 user deleted.} other {{count} users deleted.}}`)(
256 { count: users.length },
257 $localize`${users.length} users deleted.`
258 )
259 )
260
216 this.reloadData() 261 this.reloadData()
217 }, 262 },
218 263
@@ -224,7 +269,13 @@ export class UserListComponent extends RestTable implements OnInit {
224 this.userAdminService.updateUsers(users, { emailVerified: true }) 269 this.userAdminService.updateUsers(users, { emailVerified: true })
225 .subscribe({ 270 .subscribe({
226 next: () => { 271 next: () => {
227 this.notifier.success($localize`${users.length} users email set as verified.`) 272 this.notifier.success(
273 prepareIcu($localize`{count, plural, =1 {1 user email set as verified.} other {{count} user emails set as verified.}}`)(
274 { count: users.length },
275 $localize`${users.length} users email set as verified.`
276 )
277 )
278
228 this.reloadData() 279 this.reloadData()
229 }, 280 },
230 281
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html
index 75d9be5f1..2f36c27b7 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.html
+++ b/client/src/app/+admin/overview/videos/video-list.component.html
@@ -21,7 +21,7 @@
21 </my-action-dropdown> 21 </my-action-dropdown>
22 </div> 22 </div>
23 23
24 <div class="ml-auto right-form"> 24 <div class="ms-auto right-form">
25 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> 25 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
26 26
27 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button> 27 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>
@@ -67,25 +67,25 @@
67 </td> 67 </td>
68 68
69 <td> 69 <td>
70 <span class="badge badge-blue" *ngIf="video.isLocal">Local</span> 70 <span class="pt-badge badge-blue" *ngIf="video.isLocal">Local</span>
71 <span class="badge badge-purple" *ngIf="!video.isLocal">Remote</span> 71 <span class="pt-badge badge-purple" *ngIf="!video.isLocal">Remote</span>
72 72
73 <span [ngClass]="getPrivacyBadgeClass(video)" class="badge">{{ video.privacy.label }}</span> 73 <span [ngClass]="getPrivacyBadgeClass(video)" class="pt-badge">{{ video.privacy.label }}</span>
74 74
75 <span *ngIf="video.nsfw" class="badge badge-red" i18n>NSFW</span> 75 <span *ngIf="video.nsfw" class="pt-badge badge-red" i18n>NSFW</span>
76 76
77 <span *ngIf="isUnpublished(video)" class="badge badge-yellow" i18n>{{ video.state.label }}</span> 77 <span *ngIf="isUnpublished(video)" class="pt-badge badge-yellow" i18n>{{ video.state.label }}</span>
78 78
79 <span *ngIf="isAccountBlocked(video)" class="badge badge-red" i18n>Account muted</span> 79 <span *ngIf="isAccountBlocked(video)" class="pt-badge badge-red" i18n>Account muted</span>
80 <span *ngIf="isServerBlocked(video)" class="badge badge-red" i18n>Server muted</span> 80 <span *ngIf="isServerBlocked(video)" class="pt-badge badge-red" i18n>Server muted</span>
81 81
82 <span *ngIf="isVideoBlocked(video)" class="badge badge-red" i18n>Blocked</span> 82 <span *ngIf="isVideoBlocked(video)" class="pt-badge badge-red" i18n>Blocked</span>
83 </td> 83 </td>
84 84
85 <td> 85 <td>
86 <span *ngIf="isHLS(video)" class="badge badge-blue">HLS</span> 86 <span *ngIf="isHLS(video)" class="pt-badge badge-blue">HLS</span>
87 <span *ngIf="isWebTorrent(video)" class="badge badge-blue">WebTorrent ({{ video.files.length }})</span> 87 <span *ngIf="isWebTorrent(video)" class="pt-badge badge-blue">WebTorrent ({{ video.files.length }})</span>
88 <span *ngIf="video.isLive" class="badge badge-blue">Live</span> 88 <span *ngIf="video.isLive" class="pt-badge badge-blue">Live</span>
89 89
90 <span *ngIf="!isImport(video) && !video.isLive && video.isLocal">{{ getFilesSize(video) | bytes: 1 }}</span> 90 <span *ngIf="!isImport(video) && !video.isLive && video.isLocal">{{ getFilesSize(video) | bytes: 1 }}</span>
91 </td> 91 </td>
@@ -121,7 +121,7 @@
121 </ul> 121 </ul>
122 </div> 122 </div>
123 123
124 <my-embed class="ml-auto" [video]="video"></my-embed> 124 <my-embed class="ms-auto" [video]="video"></my-embed>
125 </div> 125 </div>
126 </td> 126 </td>
127 </tr> 127 </tr>
diff --git a/client/src/app/+admin/overview/videos/video-list.component.scss b/client/src/app/+admin/overview/videos/video-list.component.scss
index cb47b6548..dcd41a1b4 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.scss
+++ b/client/src/app/+admin/overview/videos/video-list.component.scss
@@ -7,10 +7,8 @@ my-embed {
7 width: 50%; 7 width: 50%;
8} 8}
9 9
10.badge { 10.pt-badge {
11 @include peertube-badge; 11 @include margin-right(5px);
12
13 margin-right: 5px;
14} 12}
15 13
16.video-info > div { 14.video-info > div {
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts
index 82ff372aa..67e52d100 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.ts
+++ b/client/src/app/+admin/overview/videos/video-list.component.ts
@@ -3,6 +3,7 @@ import { finalize } from 'rxjs/operators'
3import { Component, OnInit, ViewChild } from '@angular/core' 3import { Component, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' 5import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
6import { prepareIcu } from '@app/helpers'
6import { AdvancedInputFilter } from '@app/shared/shared-forms' 7import { AdvancedInputFilter } from '@app/shared/shared-forms'
7import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
8import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' 9import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation'
@@ -196,14 +197,24 @@ export class VideoListComponent extends RestTable implements OnInit {
196 } 197 }
197 198
198 private async removeVideos (videos: Video[]) { 199 private async removeVideos (videos: Video[]) {
199 const message = $localize`Are you sure you want to delete these ${videos.length} videos?` 200 const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)(
201 { count: videos.length },
202 $localize`Are you sure you want to delete these ${videos.length} videos?`
203 )
204
200 const res = await this.confirmService.confirm(message, $localize`Delete`) 205 const res = await this.confirmService.confirm(message, $localize`Delete`)
201 if (res === false) return 206 if (res === false) return
202 207
203 this.videoService.removeVideo(videos.map(v => v.id)) 208 this.videoService.removeVideo(videos.map(v => v.id))
204 .subscribe({ 209 .subscribe({
205 next: () => { 210 next: () => {
206 this.notifier.success($localize`Deleted ${videos.length} videos.`) 211 this.notifier.success(
212 prepareIcu($localize`Deleted {count, plural, =1 {1 video} other {{count} videos}}.`)(
213 { count: videos.length },
214 $localize`Deleted ${videos.length} videos.`
215 )
216 )
217
207 this.reloadData() 218 this.reloadData()
208 }, 219 },
209 220
@@ -215,7 +226,13 @@ export class VideoListComponent extends RestTable implements OnInit {
215 this.videoBlockService.unblockVideo(videos.map(v => v.id)) 226 this.videoBlockService.unblockVideo(videos.map(v => v.id))
216 .subscribe({ 227 .subscribe({
217 next: () => { 228 next: () => {
218 this.notifier.success($localize`Unblocked ${videos.length} videos.`) 229 this.notifier.success(
230 prepareIcu($localize`Unblocked {count, plural, =1 {1 video} other {{count} videos}}.`)(
231 { count: videos.length },
232 $localize`Unblocked ${videos.length} videos.`
233 )
234 )
235
219 this.reloadData() 236 this.reloadData()
220 }, 237 },
221 238
@@ -224,9 +241,21 @@ export class VideoListComponent extends RestTable implements OnInit {
224 } 241 }
225 242
226 private async removeVideoFiles (videos: Video[], type: 'hls' | 'webtorrent') { 243 private async removeVideoFiles (videos: Video[], type: 'hls' | 'webtorrent') {
227 const message = type === 'hls' 244 let message: string
228 ? $localize`Are you sure you want to delete ${videos.length} HLS streaming playlists?` 245
229 : $localize`Are you sure you want to delete WebTorrent files of ${videos.length} videos?` 246 if (type === 'hls') {
247 // eslint-disable-next-line max-len
248 message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {1 HLS streaming playlist} other {{count} HLS streaming playlists}}?`)(
249 { count: videos.length },
250 $localize`Are you sure you want to delete ${videos.length} HLS streaming playlists?`
251 )
252 } else {
253 // eslint-disable-next-line max-len
254 message = prepareIcu($localize`Are you sure you want to delete WebTorrent files of {count, plural, =1 {1 video} other {{count} videos}}?`)(
255 { count: videos.length },
256 $localize`Are you sure you want to delete WebTorrent files of ${videos.length} videos?`
257 )
258 }
230 259
231 const res = await this.confirmService.confirm(message, $localize`Delete`) 260 const res = await this.confirmService.confirm(message, $localize`Delete`)
232 if (res === false) return 261 if (res === false) return
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
index 33575ef52..c989d2e38 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
@@ -1,6 +1,6 @@
1<my-plugin-navigation [pluginType]="pluginType"></my-plugin-navigation> 1<my-plugin-navigation [pluginType]="pluginType"></my-plugin-navigation>
2 2
3<div class="alert alert-info" i18n *ngIf="pluginInstalled"> 3<div class="alert pt-alert-primary" i18n *ngIf="pluginInstalled">
4 To load your new installed plugins or themes, refresh the page. 4 To load your new installed plugins or themes, refresh the page.
5</div> 5</div>
6 6
@@ -32,9 +32,9 @@
32 <ng-container *ngFor="let plugin of plugins" > 32 <ng-container *ngFor="let plugin of plugins" >
33 <my-plugin-card [plugin]="plugin" [version]="plugin.latestVersion" [pluginType]="pluginType"> 33 <my-plugin-card [plugin]="plugin" [version]="plugin.latestVersion" [pluginType]="pluginType">
34 <div ngProjectAs="badges"> 34 <div ngProjectAs="badges">
35 <span i18n *ngIf="plugin.installed" class="badge badge-success">Installed</span> 35 <span i18n *ngIf="plugin.installed" class="pt-badge badge-success">Installed</span>
36 36
37 <span *ngIf="plugin.official" class="badge badge-primary" i18n i18n-title title="This plugin is developed by Framasoft"> 37 <span *ngIf="plugin.official" class="pt-badge badge-primary" i18n i18n-title title="This plugin is developed by Framasoft">
38 Official 38 Official
39 </span> 39 </span>
40 </div> 40 </div>
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss
index 10401e9df..d7b41f4d8 100644
--- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss
+++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss
@@ -17,16 +17,13 @@
17 17
18 input { 18 input {
19 @include peertube-input-text(500px); 19 @include peertube-input-text(500px);
20
21 height: 35px;
22 } 20 }
23} 21}
24 22
25.badge { 23.pt-badge {
26 @include margin-left(15px); 24 @include margin-left(15px);
27 25
28 font-size: 13px; 26 font-size: 13px;
29 font-weight: $font-semibold;
30} 27}
31 28
32.alert { 29.alert {
diff --git a/client/src/app/+admin/plugins/shared/plugin-card.component.scss b/client/src/app/+admin/plugins/shared/plugin-card.component.scss
index 608064722..7ee3376a7 100644
--- a/client/src/app/+admin/plugins/shared/plugin-card.component.scss
+++ b/client/src/app/+admin/plugins/shared/plugin-card.component.scss
@@ -14,12 +14,11 @@
14 .plugin-name { 14 .plugin-name {
15 @include margin-right(10px); 15 @include margin-right(10px);
16 16
17 font-size: 16px;
18 font-weight: $font-semibold; 17 font-weight: $font-semibold;
19 } 18 }
20 19
21 .plugin-version { 20 .plugin-version {
22 opacity: 0.6; 21 opacity: 0.7;
23 } 22 }
24 23
25 .plugin-icon { 24 .plugin-icon {
diff --git a/client/src/app/+admin/system/debug/debug.component.scss b/client/src/app/+admin/system/debug/debug.component.scss
index 7f5e861e2..bcbc3fc7c 100644
--- a/client/src/app/+admin/system/debug/debug.component.scss
+++ b/client/src/app/+admin/system/debug/debug.component.scss
@@ -1,11 +1,7 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.root { 4code {
5 font-size: 15px; 5 font-size: 14px;
6 6 font-weight: $font-semibold;
7 code {
8 font-size: 14px;
9 font-weight: $font-semibold;
10 }
11} 7}
diff --git a/client/src/app/+admin/system/jobs/jobs.component.html b/client/src/app/+admin/system/jobs/jobs.component.html
index 301591786..8068fe626 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.html
+++ b/client/src/app/+admin/system/jobs/jobs.component.html
@@ -21,7 +21,7 @@
21 <span i18n="Selector for the list displaying jobs, filtering by their state">any</span> 21 <span i18n="Selector for the list displaying jobs, filtering by their state">any</span>
22 </ng-option> 22 </ng-option>
23 <ng-option *ngFor="let state of jobStates" [value]="state"> 23 <ng-option *ngFor="let state of jobStates" [value]="state">
24 <span class="badge" [ngClass]="getJobStateClass(state)">{{ state }}</span> 24 <span class="pt-badge" [ngClass]="getJobStateClass(state)">{{ state }}</span>
25 </ng-option> 25 </ng-option>
26 </ng-select> 26 </ng-select>
27 </div> 27 </div>
@@ -62,7 +62,7 @@
62 <td class="job-priority c-hand" [pRowToggler]="job">{{ job.priority }}</td> 62 <td class="job-priority c-hand" [pRowToggler]="job">{{ job.priority }}</td>
63 63
64 <td class="job-state c-hand" [pRowToggler]="job" *ngIf="jobState === 'all'"> 64 <td class="job-state c-hand" [pRowToggler]="job" *ngIf="jobState === 'all'">
65 <span class="badge" [ngClass]="getJobStateClass(job.state)">{{ job.state }}</span> 65 <span class="pt-badge" [ngClass]="getJobStateClass(job.state)">{{ job.state }}</span>
66 </td> 66 </td>
67 67
68 <td *ngIf="hasGlobalProgress()" class="job-progress c-hand" [pRowToggler]="job"> 68 <td *ngIf="hasGlobalProgress()" class="job-progress c-hand" [pRowToggler]="job">
@@ -107,8 +107,8 @@
107 </ng-container> 107 </ng-container>
108 108
109 <ng-container *ngIf="jobState !== 'all'"> 109 <ng-container *ngIf="jobState !== 'all'">
110 <ng-container *ngIf="jobType === 'all'" i18n>No <span class="badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span> jobs found.</ng-container> 110 <ng-container *ngIf="jobType === 'all'" i18n>No <span class="pt-badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span> jobs found.</ng-container>
111 <ng-container *ngIf="jobType !== 'all'" i18n>No <code>{{ jobType }}</code> jobs found that are <span class="badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span>.</ng-container> 111 <ng-container *ngIf="jobType !== 'all'" i18n>No <code>{{ jobType }}</code> jobs found that are <span class="pt-badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span>.</ng-container>
112 </ng-container> 112 </ng-container>
113 </div> 113 </div>
114 </div> 114 </div>
diff --git a/client/src/app/+admin/system/jobs/jobs.component.scss b/client/src/app/+admin/system/jobs/jobs.component.scss
index a9e5e8d4b..4a76f1783 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.scss
+++ b/client/src/app/+admin/system/jobs/jobs.component.scss
@@ -44,10 +44,6 @@
44 } 44 }
45} 45}
46 46
47td .glyphicon {
48 @include margin-right(10px);
49}
50
51pre { 47pre {
52 font-size: 11px; 48 font-size: 11px;
53} 49}
@@ -55,7 +51,3 @@ pre {
55.job-error { 51.job-error {
56 color: #ff0000; 52 color: #ff0000;
57} 53}
58
59.select-filter-block .badge {
60 @include peertube-badge;
61}
diff --git a/client/src/app/+login/login.component.html b/client/src/app/+login/login.component.html
index dc74354d8..f3a2476f9 100644
--- a/client/src/app/+login/login.component.html
+++ b/client/src/app/+login/login.component.html
@@ -1,15 +1,38 @@
1<h1 i18n class="title-page-v2">
2 <strong class="underline-orange">{{ instanceName }}</strong>
3 >
4 Login
5</h1>
6
1<div class="margin-content"> 7<div class="margin-content">
2 <div i18n class="title-page title-page-single"> 8 <ng-container *ngIf="!externalAuthError && !isAuthenticatedWithExternalAuth">
3 Login
4 </div>
5 9
6 <div class="alert alert-danger" i18n *ngIf="externalAuthError"> 10 <div class="alert pt-alert-primary" role="alert">
7 Sorry but there was an issue with the external login process. Please <a routerLink="/about">contact an administrator</a>. 11 <h5 class="alert-heading" i18n>
8 </div> 12 Logging into an account lets you publish content
13 </h5>
9 14
10 <ng-container *ngIf="!externalAuthError && !isAuthenticatedWithExternalAuth"> 15 <p *ngIf="signupAllowed" i18n>
11 <div *ngIf="error" class="alert alert-danger">{{ error }} 16 This instance allows registration. However, be careful to check the <a class="link-orange terms-anchor" (click)="onTermsClick($event, instanceInformation)" href='#'>Terms</a><a class="terms-link" target="_blank" routerLink="/about/instance" fragment="terms">Terms</a> before creating an account.
12 <span *ngIf="error === 'User email is not verified.'"> <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a></span> 17 You may also search for another instance to match your exact needs at: <a class="link-orange" href="https://joinpeertube.org/instances" target="_blank" rel="noopener noreferrer">https://joinpeertube.org/instances</a>.
18 </p>
19
20 <p *ngIf="!signupAllowed" i18n>
21 Currently this instance doesn't allow for user registration, you may check the <a class="link-orange" (click)="onTermsClick($event, instanceInformation)" href='#'>Terms</a> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there.
22 Find yours among multiple instances at: <a class="link-orange" href="https://joinpeertube.org/instances" target="_blank" rel="noopener noreferrer">https://joinpeertube.org/instances</a>.
23 </p>
24 </div>
25
26 <div class="alert alert-danger" i18n *ngIf="externalAuthError">
27 Sorry but there was an issue with the external login process. Please <a class="link-orange" routerLink="/about">contact an administrator</a>.
28 </div>
29
30 <div *ngIf="error" class="alert alert-danger">
31 {{ error }}
32
33 <a *ngIf="error === 'User email is not verified.'" class="ms-1 link-orange" i18n routerLink="/verify-account/ask-send-email">
34 Request new verification email
35 </a>
13 </div> 36 </div>
14 37
15 <div class="wrapper"> 38 <div class="wrapper">
@@ -18,16 +41,14 @@
18 <form myPluginSelector pluginSelectorId="login-form" role="form" (ngSubmit)="login()" [formGroup]="form"> 41 <form myPluginSelector pluginSelectorId="login-form" role="form" (ngSubmit)="login()" [formGroup]="form">
19 <div class="form-group"> 42 <div class="form-group">
20 <div> 43 <div>
21 <label i18n for="username">User</label> 44 <label i18n for="username">Username or email address</label>
22 <input 45 <input
23 type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1" 46 type="text" id="username" i18n-placeholder placeholder="Example: john@example.com" required tabindex="1"
24 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" myAutofocus 47 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" myAutofocus
25 > 48 >
26 </div> 49 </div>
27 50
28 <div *ngIf="formErrors.username" class="form-error"> 51 <div *ngIf="formErrors.username" class="form-error">{{ formErrors.username }}</div>
29 {{ formErrors.username }}
30 </div>
31 52
32 <div *ngIf="hasUsernameUppercase()" i18n class="form-warning"> 53 <div *ngIf="hasUsernameUppercase()" i18n class="form-warning">
33 ⚠️ Most email addresses do not include capital letters. 54 ⚠️ Most email addresses do not include capital letters.
@@ -36,40 +57,22 @@
36 57
37 <div class="form-group"> 58 <div class="form-group">
38 <label i18n for="password">Password</label> 59 <label i18n for="password">Password</label>
39 <my-input-toggle-hidden formControlName="password" inputId="password" 60
40 i18n-placeholder placeholder="Password" 61 <my-input-text
41 [ngClass]="{ 'input-error': formErrors['password'] }" 62 formControlName="password" inputId="password" i18n-placeholder placeholder="Password"
42 autocomplete="current-password" [tabindex]="2"></my-input-toggle-hidden> 63 [formError]="formErrors['password']" autocomplete="current-password" [tabindex]="2"
43 <div *ngIf="formErrors.password" class="form-error"> 64 ></my-input-text>
44 {{ formErrors.password }}
45 </div>
46 </div> 65 </div>
47 66
48 <input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid"> 67 <input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid">
49 68
50 <div class="additionnal-links"> 69 <div class="additional-links">
51 <a i18n role="button" class="forgot-password-button" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a> 70 <a i18n role="button" class="link-orange" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a>
52 71
53 <div *ngIf="signupAllowed" class="signup-link"> 72 <ng-container *ngIf="signupAllowed">
54 <span>·</span> 73 <span>·</span>
55 <a i18n routerLink="/signup" class="create-an-account">Create an account</a> 74 <a i18n routerLink="/signup" class="link-orange">Create an account</a>
56 </div> 75 </ng-container>
57 </div>
58
59 <div class="looking-for-account alert alert-info" role="alert">
60 <h6 class="alert-heading" i18n>
61 Logging into an account lets you publish content
62 </h6>
63
64 <div *ngIf="signupAllowed" i18n>
65 This instance allows registration. However, be careful to check the <a class="terms-anchor" (click)="onTermsClick($event, instanceInformation)" href='#'>Terms</a><a class="terms-link" target="_blank" routerLink="/about/instance" fragment="terms">Terms</a> before creating an account.
66 You may also search for another instance to match your exact needs at: <br /><a class="alert-link" href="https://joinpeertube.org/instances" target="_blank" rel="noopener noreferrer">https://joinpeertube.org/instances</a>.
67 </div>
68
69 <div *ngIf="!signupAllowed" i18n>
70 Currently this instance doesn't allow for user registration, you may check the <a (click)="onTermsClick($event, instanceInformation)" href='#'>Terms</a> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there.
71 Find yours among multiple instances at: <br /> <a class="alert-link" href="https://joinpeertube.org/instances" target="_blank" rel="noopener noreferrer">https://joinpeertube.org/instances</a>.
72 </div>
73 </div> 76 </div>
74 </form> 77 </form>
75 78
@@ -86,6 +89,7 @@
86 89
87 <div #instanceInformation class="instance-information"> 90 <div #instanceInformation class="instance-information">
88 <my-instance-about-accordion 91 <my-instance-about-accordion
92 [displayInstanceName]="false"
89 (init)="onInstanceAboutAccordionInit($event)" [panels]="instanceInformationPanels" 93 (init)="onInstanceAboutAccordionInit($event)" [panels]="instanceInformationPanels"
90 pluginScope="login" pluginHook="filter:login.instance-about-plugin-panels.create.result" 94 pluginScope="login" pluginHook="filter:login.instance-about-plugin-panels.create.result"
91 ></my-instance-about-accordion> 95 ></my-instance-about-accordion>
diff --git a/client/src/app/+login/login.component.scss b/client/src/app/+login/login.component.scss
index 49c873cd4..d31d428f7 100644
--- a/client/src/app/+login/login.component.scss
+++ b/client/src/app/+login/login.component.scss
@@ -6,11 +6,13 @@
6 6
7label { 7label {
8 display: block; 8 display: block;
9 font-size: 18px;
10 margin-bottom: 5px;
9} 11}
10 12
11input[type=text], 13input[type=text],
12input[type=email] { 14input[type=email] {
13 @include peertube-input-text(340px); 15 @include peertube-input-text(100%);
14} 16}
15 17
16.modal-body { 18.modal-body {
@@ -21,7 +23,7 @@ input[type=email] {
21 } 23 }
22} 24}
23 25
24@media screen and (max-width: #{map-get($container-max-widths, sm)}) { 26@media screen and (max-width: $small-view) {
25 .modal-body { 27 .modal-body {
26 #forgot-password-email { 28 #forgot-password-email {
27 width: 100%; 29 width: 100%;
@@ -33,114 +35,102 @@ input[type=email] {
33 } 35 }
34} 36}
35 37
36.create-an-account,
37.forgot-password-button {
38 color: pvar(--mainForegroundColor);
39 cursor: pointer;
40 transition: opacity cubic-bezier(0.39, 0.575, 0.565, 1);
41
42 &:hover {
43 text-decoration: none !important;
44 opacity: .7 !important;
45 }
46}
47
48.wrapper { 38.wrapper {
49 display: flex; 39 display: flex;
50 justify-content: space-around; 40 justify-content: space-between;
51 flex-wrap: wrap; 41 flex-wrap: wrap;
42 margin: auto;
52 43
53 > div { 44 > div {
54 flex: 1 1; 45 flex: 1 1;
55 } 46 }
56 47
57 .login-form-and-externals { 48 form {
58 @include margin-left(10px); 49 width: 100%;
59 @include margin-right(10px); 50 }
51}
60 52
61 display: flex; 53.wrapper,
62 flex-wrap: wrap; 54.alert {
63 font-size: 15px; 55 max-width: 1200px;
64 max-width: 450px; 56}
65 margin-bottom: 40px;
66 57
67 form { 58.alert {
68 margin: 0; 59 margin: 0 auto 30px;
60}
69 61
70 &, 62.login-form-and-externals {
71 input { 63 @include margin-left(10px);
72 width: 100%; 64 @include margin-right(10px);
73 }
74 65
75 .additionnal-links { 66 display: flex;
76 display: block; 67 flex-wrap: wrap;
77 text-align: center; 68 justify-content: center;
78 margin-top: 20px; 69 max-width: 450px;
79 margin-bottom: 20px; 70 margin-bottom: 40px;
80 71
81 .forgot-password-button, 72 form {
82 .create-an-account { 73 margin: 0;
83 padding: 4px;
84 display: inline-block;
85 74
86 color: var(--mainColor); 75 input[type=submit] {
76 width: 100%;
77 }
87 78
88 &:hover, 79 .additional-links {
89 &:active { 80 display: flex;
90 color: var(--mainHoverColor); 81 justify-content: center;
91 } 82 margin: 20px 0 30px;
92 } 83
84 .link-orange {
85 margin: 0 15px;
93 } 86 }
94 } 87 }
88 }
89}
95 90
96 .external-login-blocks { 91.external-login-blocks {
97 min-width: 200px; 92 min-width: 200px;
93 text-align: center;
98 94
99 .block-title { 95 .block-title {
100 font-weight: $font-semibold; 96 font-weight: $font-semibold;
101 } 97 }
102 98
103 .external-login-block { 99 .external-login-block {
104 @include disable-default-a-behaviour; 100 @include disable-default-a-behaviour;
105
106 cursor: pointer;
107 border: 1px solid #d1d7e0;
108 border-radius: 5px;
109 color: pvar(--mainForegroundColor);
110 margin: 10px 10px 0 0;
111 display: flex;
112 justify-content: center;
113 align-items: center;
114 min-height: 35px;
115 min-width: 100px;
116
117 &:hover {
118 background-color: rgba(209, 215, 224, 0.5);
119 }
120 }
121 }
122 101
123 .signup-link { 102 cursor: pointer;
124 display: inline-block; 103 border: 1px solid #d1d7e0;
104 border-radius: 5px;
105 color: pvar(--mainForegroundColor);
106 margin: 10px 10px 0 0;
107 display: flex;
108 justify-content: center;
109 align-items: center;
110 min-height: 35px;
111 min-width: 100px;
112
113 &:hover {
114 background-color: rgba(209, 215, 224, 0.5);
125 } 115 }
126 } 116 }
117}
127 118
128 .instance-information { 119.instance-information {
129 @include margin-left(10px); 120 @include margin-left(10px);
130 @include margin-right(10px); 121 @include margin-right(10px);
131 122
132 max-width: 600px; 123 max-width: 600px;
133 min-width: 350px; 124 min-width: 350px;
134 margin-bottom: 40px; 125 margin-bottom: 40px;
135 } 126}
136 127
137 .terms-anchor { 128.terms-anchor {
138 display: inline; 129 display: inline;
139 } 130}
140 131
141 .terms-link { 132.terms-link {
142 display: none; 133 display: none;
143 }
144} 134}
145 135
146@mixin column-reverse-display { 136@mixin column-reverse-display {
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts
index 96754b782..2ed9be16c 100644
--- a/client/src/app/+login/login.component.ts
+++ b/client/src/app/+login/login.component.ts
@@ -59,6 +59,10 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
59 return this.serverConfig.signup.allowed === true 59 return this.serverConfig.signup.allowed === true
60 } 60 }
61 61
62 get instanceName () {
63 return this.serverConfig.instance.name
64 }
65
62 onTermsClick (event: Event, instanceInformation: HTMLElement) { 66 onTermsClick (event: Event, instanceInformation: HTMLElement) {
63 event.preventDefault() 67 event.preventDefault()
64 68
diff --git a/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html
index 3751747a9..b557fb011 100644
--- a/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html
+++ b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html
@@ -3,42 +3,41 @@
3<div class="margin-content"> 3<div class="margin-content">
4 <form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 4 <form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
5 5
6 <div class="form-row"> <!-- channel grid --> 6 <div class="row"> <!-- channel grid -->
7 <div class="form-group col-12 col-lg-4 col-xl-3"> 7 <div class="col-12 col-lg-4 col-xl-3">
8 <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div> 8 <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
9 <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div> 9 <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div>
10 </div> 10 </div>
11 11
12 <div class="form-group col-12 col-lg-8 col-xl-9"> 12 <div class="col-12 col-lg-8 col-xl-9">
13 <h6 i18n>Banner image of the channel</h6> 13 <label i18n>Banner image of the channel</label>
14 14
15 <my-actor-banner-edit 15 <my-actor-banner-edit
16 *ngIf="videoChannel" [previewImage]="isCreation()" 16 *ngIf="videoChannel" [previewImage]="isCreation()"
17 [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()" 17 [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
18 ></my-actor-banner-edit> 18 ></my-actor-banner-edit>
19 19
20 <my-actor-avatar-edit 20 <my-actor-avatar-edit
21 *ngIf="videoChannel" [previewImage]="isCreation()" 21 *ngIf="videoChannel" [previewImage]="isCreation()"
22 [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()" 22 [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
23 [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()" 23 [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
24 ></my-actor-avatar-edit> 24 ></my-actor-avatar-edit>
25 25
26 <div class="form-group" *ngIf="isCreation()"> 26 <div class="form-group" *ngIf="isCreation()">
27 <label i18n for="name">Name</label> 27 <label i18n for="name">Name</label>
28
28 <div class="input-group"> 29 <div class="input-group">
29 <input 30 <input
30 type="text" id="name" i18n-placeholder placeholder="Example: my_channel" 31 type="text" id="name" i18n-placeholder placeholder="Example: my_channel"
31 formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control" 32 formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control"
32 > 33 >
33 <div class="input-group-append"> 34 <div class="input-group-text">@{{ instanceHost }}</div>
34 <span class="input-group-text">@{{ instanceHost }}</span>
35 </div>
36 </div> 35 </div>
37 <div *ngIf="formErrors['name']" class="form-error"> 36 <div *ngIf="formErrors['name']" class="form-error">
38 {{ formErrors['name'] }} 37 {{ formErrors['name'] }}
39 </div> 38 </div>
40 </div> 39 </div>
41 40
42 <div class="form-group"> 41 <div class="form-group">
43 <label i18n for="display-name">Display name</label> 42 <label i18n for="display-name">Display name</label>
44 <input 43 <input
@@ -49,7 +48,7 @@
49 {{ formErrors['display-name'] }} 48 {{ formErrors['display-name'] }}
50 </div> 49 </div>
51 </div> 50 </div>
52 51
53 <div class="form-group"> 52 <div class="form-group">
54 <label i18n for="description">Description</label> 53 <label i18n for="description">Description</label>
55 <textarea 54 <textarea
@@ -60,37 +59,35 @@
60 {{ formErrors.description }} 59 {{ formErrors.description }}
61 </div> 60 </div>
62 </div> 61 </div>
63 62
64 <div class="form-group"> 63 <div class="form-group">
65 <label for="support">Support</label> 64 <label for="support">Support</label>
66 <my-help 65 <my-help
67 helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support the channel (membership platform...).<br /><br /> 66 helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support the channel (membership platform...).<br /><br />
68 When a video is uploaded in this channel, the video support field will be automatically filled by this text." 67 When a video is uploaded in this channel, the video support field will be automatically filled by this text."
69 ></my-help> 68 ></my-help>
69
70 <my-markdown-textarea 70 <my-markdown-textarea
71 id="support" formControlName="support" textareaMaxWidth="500px" markdownType="enhanced" 71 id="support" formControlName="support" markdownType="enhanced"
72 [classes]="{ 'input-error': formErrors['support'] }" 72 [formError]="formErrors['support']"
73 ></my-markdown-textarea> 73 ></my-markdown-textarea>
74 <div *ngIf="formErrors.support" class="form-error">
75 {{ formErrors.support }}
76 </div>
77 </div> 74 </div>
78 75
79 <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()"> 76 <div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()">
80 <my-peertube-checkbox 77 <my-peertube-checkbox
81 inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate" 78 inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate"
82 i18n-labelText labelText="Overwrite support field of all videos of this channel" 79 i18n-labelText labelText="Overwrite support field of all videos of this channel"
83 ></my-peertube-checkbox> 80 ></my-peertube-checkbox>
84 </div> 81 </div>
85 82
86 </div> 83 </div>
87 </div> 84 </div>
88 85
89 <div class="form-row"> <!-- submit placement block --> 86 <div class="row"> <!-- submit placement block -->
90 <div class="col-md-7 col-xl-5"></div> 87 <div class="col-md-7 col-xl-5"></div>
91 <div class="col-md-5 col-xl-5 d-inline-flex"> 88 <div class="col-md-5 col-xl-5 d-inline-flex">
92 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 89 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
93 </div> 90 </div>
94 </div> 91 </div>
95 </form> 92 </form>
96</div> \ No newline at end of file 93</div>
diff --git a/client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss
index d010d6277..cde848da6 100644
--- a/client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss
+++ b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.scss
@@ -5,11 +5,6 @@
5 padding-top: 20px; 5 padding-top: 20px;
6} 6}
7 7
8label {
9 font-weight: $font-regular;
10 font-size: 100%;
11}
12
13.video-channel-title { 8.video-channel-title {
14 @include settings-big-title; 9 @include settings-big-title;
15} 10}
@@ -24,31 +19,21 @@ my-actor-banner-edit {
24 max-width: 500px; 19 max-width: 500px;
25} 20}
26 21
27.input-group { 22input[type=text] {
28 @include peertube-input-group(fit-content); 23 @include peertube-input-text(340px);
29}
30 24
31.input-group-append { 25 display: block;
32 height: 30px;
33}
34
35input {
36 &[type=text] {
37 @include peertube-input-text(340px);
38
39 display: block;
40 26
41 &#name { 27 &#name {
42 width: auto; 28 width: auto;
43 flex-grow: 1; 29 flex-grow: 1;
44 }
45 } 30 }
31}
46 32
47 &[type=submit] { 33input[type=submit] {
48 @include peertube-button; 34 @include peertube-button;
49 @include orange-button; 35 @include orange-button;
50 @include margin-left(auto); 36 @include margin-left(auto);
51 }
52} 37}
53 38
54textarea { 39textarea {
@@ -57,6 +42,11 @@ textarea {
57 display: block; 42 display: block;
58} 43}
59 44
45my-markdown-textarea {
46 display: block;
47 max-width: 500px;
48}
49
60.peertube-select-container { 50.peertube-select-container {
61 @include peertube-select-container(340px); 51 @include peertube-select-container(340px);
62} 52}
diff --git a/client/src/app/+my-account/my-account-applications/my-account-applications.component.html b/client/src/app/+my-account/my-account-applications/my-account-applications.component.html
index 68d094a4f..2fc691707 100644
--- a/client/src/app/+my-account/my-account-applications/my-account-applications.component.html
+++ b/client/src/app/+my-account/my-account-applications/my-account-applications.component.html
@@ -3,8 +3,9 @@
3 <ng-container i18n>Applications</ng-container> 3 <ng-container i18n>Applications</ng-container>
4</h1> 4</h1>
5 5
6<div class="form-row"> <!-- built-in token grid --> 6<div class="row"> <!-- built-in token grid -->
7 <div class="form-group col-12 col-lg-4 col-xl-3"> 7
8 <div class="group col-12 col-lg-4 col-xl-3">
8 <h2 i18n class="applications-title">SUBSCRIPTION FEED</h2> 9 <h2 i18n class="applications-title">SUBSCRIPTION FEED</h2>
9 <div i18n class="applications-description"> 10 <div i18n class="applications-description">
10 Use third-party feed aggregators to retrieve the list of videos from 11 Use third-party feed aggregators to retrieve the list of videos from
@@ -12,16 +13,16 @@
12 </div> 13 </div>
13 </div> 14 </div>
14 15
15 <div class="form-group col-12 col-lg-8 col-xl-9"> 16 <div class="col-12 col-lg-8 col-xl-9">
16 17
17 <div class="form-group"> 18 <div class="form-group">
18 <label i18n for="feed-url">Feed URL</label> 19 <label i18n for="feed-url">Feed URL</label>
19 <my-input-toggle-hidden [value]="feedUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-toggle-hidden> 20 <my-input-text [value]="feedUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-text>
20 </div> 21 </div>
21 22
22 <div class="form-group"> 23 <div class="form-group">
23 <label i18n for="feed-token">Feed Token</label> 24 <label i18n for="feed-token">Feed Token</label>
24 <my-input-toggle-hidden [value]="feedToken" [withCopy]="true" [readonly]="true"></my-input-toggle-hidden> 25 <my-input-text [value]="feedToken" [withCopy]="true" [readonly]="true"></my-input-text>
25 26
26 <div class="form-group-description" i18n>⚠️ Never share your feed token with anyone.</div> 27 <div class="form-group-description" i18n>⚠️ Never share your feed token with anyone.</div>
27 </div> 28 </div>
@@ -29,7 +30,7 @@
29 </div> 30 </div>
30</div> 31</div>
31 32
32<div class="form-row mt-4"> <!-- submit placement block --> 33<div class="row mt-4"> <!-- submit placement block -->
33 <div class="col-md-7 col-xl-5"></div> 34 <div class="col-md-7 col-xl-5"></div>
34 <div class="col-md-5 col-xl-5"> 35 <div class="col-md-5 col-xl-5">
35 <input (click)="renewToken()" type="submit" i18n-value value="Renew token"> 36 <input (click)="renewToken()" type="submit" i18n-value value="Renew token">
diff --git a/client/src/app/+my-account/my-account-applications/my-account-applications.component.scss b/client/src/app/+my-account/my-account-applications/my-account-applications.component.scss
index 2a3b85c22..b3fedd2f6 100644
--- a/client/src/app/+my-account/my-account-applications/my-account-applications.component.scss
+++ b/client/src/app/+my-account/my-account-applications/my-account-applications.component.scss
@@ -1,11 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4label {
5 font-weight: $font-regular;
6 font-size: 100%;
7}
8
9.applications-title { 4.applications-title {
10 @include settings-big-title; 5 @include settings-big-title;
11} 6}
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html
index f0e9f4010..b98cd1156 100644
--- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html
+++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html
@@ -1,11 +1,11 @@
1<h1 class="sr-only" i18n>Notifications</h1> 1<h1 class="visually-hidden" i18n>Notifications</h1>
2<div class="header"> 2<div class="header">
3 <a routerLink="/my-account/settings" fragment="notifications" i18n> 3 <a routerLink="/my-account/settings" fragment="notifications" i18n>
4 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> 4 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
5 Notification preferences 5 Notification preferences
6 </a> 6 </a>
7 7
8 <div class="peertube-select-container peertube-select-button ml-2 mr-2"> 8 <div class="peertube-select-container peertube-select-button ms-2 me-2">
9 <select [(ngModel)]="notificationSortType" (ngModelChange)="onChangeSortColumn()" class="form-control"> 9 <select [(ngModel)]="notificationSortType" (ngModelChange)="onChangeSortColumn()" class="form-control">
10 <option value="undefined" disabled>Sort by</option> 10 <option value="undefined" disabled>Sort by</option>
11 <option value="createdAt" i18n>Newest first</option> 11 <option value="createdAt" i18n>Newest first</option>
@@ -13,7 +13,7 @@
13 </select> 13 </select>
14 </div> 14 </div>
15 15
16 <button class="btn ml-auto" [disabled]="!hasUnreadNotifications()" (click)="markAllAsRead()"> 16 <button class="btn ms-auto" [disabled]="!hasUnreadNotifications()" (click)="markAllAsRead()">
17 <ng-container *ngIf="hasUnreadNotifications()"> 17 <ng-container *ngIf="hasUnreadNotifications()">
18 <my-global-icon iconName="tick" aria-hidden="true"></my-global-icon> 18 <my-global-icon iconName="tick" aria-hidden="true"></my-global-icon>
19 19
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss
index b0ac45e30..d412e568f 100644
--- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss
+++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss
@@ -3,7 +3,6 @@
3 3
4.header { 4.header {
5 display: flex; 5 display: flex;
6 font-size: 15px;
7 margin-bottom: 20px; 6 margin-bottom: 20px;
8 7
9 a { 8 a {
@@ -23,11 +22,6 @@
23 } 22 }
24} 23}
25 24
26
27my-user-notifications {
28 font-size: 15px;
29}
30
31@media screen and (max-width: $mobile-view) { 25@media screen and (max-width: $mobile-view) {
32 .header { 26 .header {
33 flex-direction: column; 27 flex-direction: column;
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html
index 6d2d7d46e..d85be846b 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html
@@ -1,18 +1,20 @@
1<div *ngIf="error" class="alert alert-danger">{{ error }}</div> 1<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
2<div *ngIf="success" class="alert alert-success">{{ success }}</div> 2<div *ngIf="success" class="alert alert-success">{{ success }}</div>
3 3
4<div i18n class="current-email">
5 Your current email is <span class="email">{{ user.email }}</span>.
6 It is never shown to the public.
7</div>
8
9<div i18n class="pending-email" *ngIf="user.pendingEmail"> 4<div i18n class="pending-email" *ngIf="user.pendingEmail">
10 <span class="email">{{ user.pendingEmail }}</span> is awaiting email verification 5 <strong>{{ user.pendingEmail }}</strong> is awaiting email verification
11</div> 6</div>
12 7
13<form role="form" class="change-email" (ngSubmit)="changeEmail()" [formGroup]="form" *ngIf="user.pluginAuth === null"> 8<form role="form" class="change-email" (ngSubmit)="changeEmail()" [formGroup]="form" *ngIf="user.pluginAuth === null">
14 9
15 <div class="form-group"> 10 <div class="form-group">
11 <label i18n for="new-email">Change your email</label>
12
13 <div i18n class="form-group-description">
14 Your current email is <strong>{{ user.email }}</strong>.
15 It is never shown to the public.
16 </div>
17
16 <input 18 <input
17 type="email" id="new-email" i18n-placeholder placeholder="New email" class="form-control" 19 type="email" id="new-email" i18n-placeholder placeholder="New email" class="form-control"
18 formControlName="new-email" [ngClass]="{ 'input-error': formErrors['new-email'] }" 20 formControlName="new-email" [ngClass]="{ 'input-error': formErrors['new-email'] }"
@@ -23,14 +25,10 @@
23 </div> 25 </div>
24 26
25 <div class="form-group"> 27 <div class="form-group">
26 <my-input-toggle-hidden formControlName="password" 28 <my-input-text
27 id="password" 29 formControlName="password" id="password" i18n-placeholder placeholder="Current password"
28 i18n-placeholder placeholder="Current password" 30 [formError]="formErrors['password']" autocomplete="current-password"
29 [ngClass]="{ 'input-error': formErrors['password'] }" 31 ></my-input-text>
30 autocomplete="current-password"></my-input-toggle-hidden>
31 <div *ngIf="formErrors['password']" class="form-error">
32 {{ formErrors['password'] }}
33 </div>
34 </div> 32 </div>
35 33
36 <input type="submit" i18n-value value="Change email" [disabled]="!form.valid"> 34 <input type="submit" i18n-value value="Change email" [disabled]="!form.valid">
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.scss b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.scss
index 788db02ad..8d1804a93 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.scss
+++ b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.scss
@@ -1,18 +1,17 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4label { 4form {
5 font-weight: $font-regular; 5 max-width: 340px;
6 font-size: 100%;
7} 6}
8 7
9my-input-toggle-hidden { 8my-input-text {
10 width: 340px;
11 display: block; 9 display: block;
10 width: 100%;
12} 11}
13 12
14input[type=email] { 13input[type=email] {
15 @include peertube-input-text(340px); 14 @include peertube-input-text(100%);
16 15
17 display: block; 16 display: block;
18} 17}
@@ -22,15 +21,6 @@ input[type=submit] {
22 @include orange-button; 21 @include orange-button;
23} 22}
24 23
25.current-email,
26.pending-email { 24.pending-email {
27 margin-bottom: 15px; 25 margin-bottom: 15px;
28
29 .email {
30 font-weight: $font-semibold;
31 }
32}
33
34.form-group {
35 width: max-content;
36} 26}
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.html b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.html
index 43facb7ed..f961d3294 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.html
@@ -3,32 +3,20 @@
3<form role="form" (ngSubmit)="changePassword()" [formGroup]="form"> 3<form role="form" (ngSubmit)="changePassword()" [formGroup]="form">
4 4
5 <label i18n for="current-password">Change password</label> 5 <label i18n for="current-password">Change password</label>
6 <my-input-toggle-hidden formControlName="current-password" 6 <my-input-text
7 inputId="current-password" 7 formControlName="current-password" inputId="current-password" i18n-placeholder placeholder="Current password"
8 i18n-placeholder placeholder="Current password" 8 [formError]="formErrors['current-password']" autocomplete="current-password"
9 [ngClass]="{ 'input-error': formErrors['current-password'] }" 9 ></my-input-text>
10 autocomplete="current-password"></my-input-toggle-hidden>
11 <div *ngIf="formErrors['current-password']" class="form-error">
12 {{ formErrors['current-password'] }}
13 </div>
14 10
15 <my-input-toggle-hidden formControlName="new-password" 11 <my-input-text
16 inputId="new-password" 12 formControlName="new-password" inputId="new-password" i18n-placeholder placeholder="New password"
17 i18n-placeholder placeholder="New password" 13 [formError]="formErrors['new-password']" autocomplete="new-password"
18 [ngClass]="{ 'input-error': formErrors['new-password'] }" 14 ></my-input-text>
19 autocomplete="new-password"></my-input-toggle-hidden>
20 <div *ngIf="formErrors['new-password']" class="form-error">
21 {{ formErrors['new-password'] }}
22 </div>
23 15
24 <my-input-toggle-hidden formControlName="new-confirmed-password" 16 <my-input-text
25 inputId="new-confirmed-password" 17 formControlName="new-confirmed-password" inputId="new-confirmed-password" i18n-placeholder placeholder="Confirm new password"
26 i18n-placeholder placeholder="Confirm new password" 18 [formError]="formErrors['new-confirmed-password']" autocomplete="new-password"
27 [ngClass]="{ 'input-error': formErrors['new-confirmed-password'] }" 19 ></my-input-text>
28 autocomplete="new-password"></my-input-toggle-hidden>
29 <div *ngIf="formErrors['new-confirmed-password']" class="form-error">
30 {{ formErrors['new-confirmed-password'] }}
31 </div>
32 20
33 <input type="submit" i18n-value value="Change password" [disabled]="!form.valid"> 21 <input type="submit" i18n-value value="Change password" [disabled]="!form.valid">
34</form> 22</form>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.scss b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.scss
index c46aae266..a29f04c36 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.scss
+++ b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.scss
@@ -1,17 +1,16 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4label { 4form {
5 font-weight: $font-regular; 5 max-width: 340px;
6 font-size: 100%;
7} 6}
8 7
9my-input-toggle-hidden { 8my-input-text {
10 width: 340px;
11 display: block; 9 display: block;
10 width: 100%;
12} 11}
13 12
14my-input-toggle-hidden + my-input-toggle-hidden { 13my-input-text + my-input-text {
15 margin-top: 15px; 14 margin-top: 15px;
16} 15}
17 16
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html
index c3cfe0314..46f5e5d6b 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html
@@ -1,13 +1,13 @@
1<div *ngIf="webNotifications"> 1<div *ngIf="webNotifications">
2 <ng-container *ngFor="let group of notificationSettingGroups"> 2 <ng-container *ngFor="let group of notificationSettingGroups">
3 <div class="header custom-row"> 3 <div class="header notification-row">
4 <div i18n>{{ group.label }}</div> 4 <div i18n>{{ group.label }}</div>
5 <div i18n>Web</div> 5 <div i18n>Web</div>
6 <div i18n *ngIf="emailEnabled">Email</div> 6 <div i18n *ngIf="emailEnabled">Email</div>
7 </div> 7 </div>
8 8
9 <ng-container *ngFor="let notificationType of group.keys"> 9 <ng-container *ngFor="let notificationType of group.keys">
10 <div class="custom-row" *ngIf="hasUserRight(notificationType)"> 10 <div class="small notification-row" *ngIf="hasUserRight(notificationType)">
11 <div>{{ labelNotifications[notificationType] }}</div> 11 <div>{{ labelNotifications[notificationType] }}</div>
12 12
13 <div> 13 <div>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss
index 2fe1f9536..8181c3175 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss
@@ -1,13 +1,12 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.custom-row { 4.notification-row {
5 display: flex; 5 display: flex;
6 align-items: center; 6 align-items: center;
7 border-bottom: 1px solid $separator-border-color; 7 border-bottom: 1px solid $separator-border-color;
8 8
9 &.header { 9 &.header {
10 font-size: 16px;
11 font-weight: $font-semibold; 10 font-weight: $font-semibold;
12 margin-top: 10px; 11 margin-top: 10px;
13 } 12 }
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
index 7c13282fa..769ab647a 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
@@ -37,7 +37,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
37 myVideoPublished: $localize`Video published (after transcoding/scheduled update)`, 37 myVideoPublished: $localize`Video published (after transcoding/scheduled update)`,
38 myVideoImportFinished: $localize`Video import finished`, 38 myVideoImportFinished: $localize`Video import finished`,
39 newUserRegistration: $localize`A new user registered on your instance`, 39 newUserRegistration: $localize`A new user registered on your instance`,
40 newFollow: $localize`You or your channel(s) has a new follower`, 40 newFollow: $localize`You or one of your channels has a new follower`,
41 commentMention: $localize`Someone mentioned you in video comments`, 41 commentMention: $localize`Someone mentioned you in video comments`,
42 newInstanceFollower: $localize`Your instance has a new follower`, 42 newInstanceFollower: $localize`Your instance has a new follower`,
43 autoInstanceFollowing: $localize`Your instance automatically followed another instance`, 43 autoInstanceFollowing: $localize`Your instance automatically followed another instance`,
diff --git a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html
index ae5f25cff..2b192ab6d 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.html
@@ -8,7 +8,7 @@
8 type="text" id="username" class="form-control" 8 type="text" id="username" class="form-control"
9 formControlName="username" readonly 9 formControlName="username" readonly
10 > 10 >
11 <div class="muted" i18n> 11 <div class="form-group-description" i18n>
12 People can find you using @{{ user.username }}@{{ instanceHost }} 12 People can find you using @{{ user.username }}@{{ instanceHost }}
13 </div> 13 </div>
14 </div> 14 </div>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.scss b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.scss
index cbac81d01..23e701332 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.scss
+++ b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.scss
@@ -1,15 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4label {
5 font-weight: $font-regular;
6 font-size: 100%;
7}
8
9.form-group:first-child {
10 margin-bottom: 15px;
11}
12
13input#username + .muted { 4input#username + .muted {
14 margin-top: 5px; 5 margin-top: 5px;
15} 6}
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
index 8ca197fd4..d9e833019 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
@@ -1,83 +1,83 @@
1<h1 class="sr-only" i18n>Settings</h1> 1<h1 class="visually-hidden" i18n>Settings</h1>
2<div class="form-row"> <!-- preview -->
3 <div class="form-group col-12 col-lg-4 col-xl-3"></div>
4 2
5 <div class="form-group col-12 col-lg-8 col-xl-9"> 3<div class="row"> <!-- preview -->
4 <div class="col-12 col-lg-4 col-xl-3"></div>
5
6 <div class="col-12 col-lg-8 col-xl-9">
6 <my-actor-avatar-edit [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-edit> 7 <my-actor-avatar-edit [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-edit>
7 </div> 8 </div>
8</div> 9</div>
9 10
10<div class="form-row"> <!-- profile settings grid --> 11<div class="row mt-3"> <!-- profile settings grid -->
11 <div class="form-group col-12 col-lg-4 col-xl-3"> 12 <div class="col-12 col-lg-4 col-xl-3">
12 <h2 i18n class="account-title">PROFILE SETTINGS</h2> 13 <h2 i18n class="account-title">PROFILE SETTINGS</h2>
13 </div> 14 </div>
14 15
15 <div class="form-group col-12 col-lg-8 col-xl-9"> 16 <div class="col-12 col-lg-8 col-xl-9">
16
17 <my-user-quota [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-quota> 17 <my-user-quota [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-quota>
18 18
19 <my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile> 19 <my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile>
20 </div> 20 </div>
21</div> 21</div>
22 22
23<div class="form-row mt-5"> <!-- interface grid --> 23<div class="row mt-5"> <!-- interface grid -->
24 <div class="form-group col-12 col-lg-4 col-xl-3"> 24 <div class="col-12 col-lg-4 col-xl-3">
25 <h2 i18n class="account-title">INTERFACE</h2> 25 <h2 i18n class="account-title">INTERFACE</h2>
26 </div> 26 </div>
27 27
28 <div class="form-group col-12 col-lg-8 col-xl-9"> 28 <div class="col-12 col-lg-8 col-xl-9">
29 <my-user-interface-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-interface-settings> 29 <my-user-interface-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-interface-settings>
30 </div> 30 </div>
31</div> 31</div>
32 32
33<div class="form-row mt-5"> <!-- video settings grid --> 33<div class="row mt-5"> <!-- video settings grid -->
34 <div class="form-group col-12 col-lg-4 col-xl-3"> 34 <div class="col-12 col-lg-4 col-xl-3">
35 <div class="anchor" id="video-settings"></div> <!-- video settings anchor --> 35 <div class="anchor" id="video-settings"></div> <!-- video settings anchor -->
36 <h2 i18n class="account-title">VIDEO SETTINGS</h2> 36 <h2 i18n class="account-title">VIDEO SETTINGS</h2>
37 </div> 37 </div>
38 38
39 <div class="form-group col-12 col-lg-8 col-xl-9"> 39 <div class="col-12 col-lg-8 col-xl-9">
40 <my-user-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-video-settings> 40 <my-user-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-video-settings>
41 </div> 41 </div>
42</div> 42</div>
43 43
44<div class="form-row mt-5"> <!-- notifications grid --> 44<div class="row mt-5"> <!-- notifications grid -->
45 <div class="form-group col-12 col-lg-4 col-xl-3"> 45 <div class="col-12 col-lg-4 col-xl-3">
46 <div class="anchor" id="notifications"></div> <!-- notifications anchor --> 46 <div class="anchor" id="notifications"></div> <!-- notifications anchor -->
47 <h2 i18n class="account-title">NOTIFICATIONS</h2> 47 <h2 i18n class="account-title">NOTIFICATIONS</h2>
48 </div> 48 </div>
49 49
50 <div class="form-group col-12 col-lg-8 col-xl-9"> 50 <div class="col-12 col-lg-8 col-xl-9">
51 <my-account-notification-preferences [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-notification-preferences> 51 <my-account-notification-preferences [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-notification-preferences>
52 </div> 52 </div>
53</div> 53</div>
54 54
55<div class="form-row mt-5" *ngIf="user.pluginAuth === null"> <!-- password grid --> 55<div class="row mt-5" *ngIf="user.pluginAuth === null"> <!-- password grid -->
56 <div class="form-group col-12 col-lg-4 col-xl-3"> 56 <div class="col-12 col-lg-4 col-xl-3">
57 <h2 i18n class="account-title">PASSWORD</h2> 57 <h2 i18n class="account-title">PASSWORD</h2>
58 </div> 58 </div>
59 59
60 <div class="form-group col-12 col-lg-8 col-xl-9"> 60 <div class="col-12 col-lg-8 col-xl-9">
61 <my-account-change-password></my-account-change-password> 61 <my-account-change-password></my-account-change-password>
62 </div> 62 </div>
63</div> 63</div>
64 64
65<div class="form-row mt-5"> <!-- email grid --> 65<div class="row mt-5"> <!-- email grid -->
66 <div class="form-group col-12 col-lg-4 col-xl-3"> 66 <div class="col-12 col-lg-4 col-xl-3">
67 <h2 i18n class="account-title">EMAIL</h2> 67 <h2 i18n class="account-title">EMAIL</h2>
68 </div> 68 </div>
69 69
70 <div class="form-group col-12 col-lg-8 col-xl-9"> 70 <div class="col-12 col-lg-8 col-xl-9">
71 <my-account-change-email></my-account-change-email> 71 <my-account-change-email></my-account-change-email>
72 </div> 72 </div>
73</div> 73</div>
74 74
75<div class="form-row mt-5"> <!-- danger zone grid --> 75<div class="row mt-5"> <!-- danger zone grid -->
76 <div class="form-group col-12 col-lg-4 col-xl-3"> 76 <div class="col-12 col-lg-4 col-xl-3">
77 <h2 i18n class="account-title account-title-danger">DANGER ZONE</h2> 77 <h2 i18n class="account-title account-title-danger">DANGER ZONE</h2>
78 </div> 78 </div>
79 79
80 <div class="form-group col-12 col-lg-8 col-xl-9"> 80 <div class="col-12 col-lg-8 col-xl-9">
81 <my-account-danger-zone [user]="user"></my-account-danger-zone> 81 <my-account-danger-zone [user]="user"></my-account-danger-zone>
82 </div> 82 </div>
83</div> 83</div>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss b/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss
index 1c216d79d..8206f4dd8 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss
+++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss
@@ -1,5 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3@use '~bootstrap/scss/functions' as *;
3 4
4.account-title { 5.account-title {
5 @include settings-big-title; 6 @include settings-big-title;
@@ -9,6 +10,6 @@
9 } 10 }
10} 11}
11 12
12.form-group { 13.row > div {
13 max-width: 500px; 14 max-width: 500px;
14} 15}
diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html
index b465d0156..1c44c8472 100644
--- a/client/src/app/+my-account/my-account.component.html
+++ b/client/src/app/+my-account/my-account.component.html
@@ -1,4 +1,4 @@
1<div class="row"> 1<div class="root">
2 <my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown> 2 <my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
3 3
4 <div class="margin-content pb-5" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }"> 4 <div class="margin-content pb-5" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }">
diff --git a/client/src/app/+my-account/my-account.component.scss b/client/src/app/+my-account/my-account.component.scss
index 1ec25315a..6275b7ac2 100644
--- a/client/src/app/+my-account/my-account.component.scss
+++ b/client/src/app/+my-account/my-account.component.scss
@@ -1,7 +1,7 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.row { 4.root {
5 @include sub-menu-h1; 5 @include sub-menu-h1;
6 6
7 flex-direction: column; 7 flex-direction: column;
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
index 89327b065..e942e002b 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
@@ -1,7 +1,7 @@
1<h1> 1<h1>
2 <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon> 2 <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>My channels</ng-container> 3 <ng-container i18n>My channels</ng-container>
4 <span class="badge badge-secondary">{{ totalItems }}</span> 4 <span *ngIf="totalItems" class="pt-badge badge-secondary">{{ totalItems }}</span>
5</h1> 5</h1>
6 6
7<my-channels-setup-message [hideLink]="true"></my-channels-setup-message> 7<my-channels-setup-message [hideLink]="true"></my-channels-setup-message>
@@ -19,7 +19,7 @@
19 19
20<div class="video-channels"> 20<div class="video-channels">
21 <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel"> 21 <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel">
22 <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]" size="80"></my-actor-avatar> 22 <my-actor-avatar [actor]="videoChannel" actorType="channel" [internalHref]="[ '/c', videoChannel.nameWithHost ]" size="80"></my-actor-avatar>
23 23
24 <div class="video-channel-info"> 24 <div class="video-channel-info">
25 <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page"> 25 <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
@@ -31,10 +31,14 @@
31 i18n class="video-channel-followers" 31 i18n class="video-channel-followers"
32 [routerLink]="[ '/my-library', 'followers' ]" [queryParams]="{ search: 'channel:' + videoChannel.name }" 32 [routerLink]="[ '/my-library', 'followers' ]" [queryParams]="{ search: 'channel:' + videoChannel.name }"
33 > 33 >
34 {videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}} 34 {videoChannel.followersCount, plural, =0 {No subscribers} =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
35 </a> 35 </a>
36 36
37 <div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div> 37 <div class="d-flex">
38 <span i18n>{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</span>
39 <span class="mx-1">·</span>
40 <span i18n>{videoChannel.totalViews, plural, =0 {No views} =1 {1 view} other {{{ videoChannel.totalViews | myNumberFormatter }} views}}</span>
41 </div>
38 42
39 <div class="video-channel-buttons"> 43 <div class="video-channel-buttons">
40 <my-edit-button label [routerLink]="[ '/manage/update', videoChannel.nameWithHost ]"></my-edit-button> 44 <my-edit-button label [routerLink]="[ '/manage/update', videoChannel.nameWithHost ]"></my-edit-button>
diff --git a/client/src/app/+my-library/my-follows/my-followers.component.html b/client/src/app/+my-library/my-follows/my-followers.component.html
index a8a3da863..e9b0f6355 100644
--- a/client/src/app/+my-library/my-follows/my-followers.component.html
+++ b/client/src/app/+my-library/my-follows/my-followers.component.html
@@ -2,7 +2,7 @@
2 <span> 2 <span>
3 <my-global-icon iconName="follower" aria-hidden="true"></my-global-icon> 3 <my-global-icon iconName="follower" aria-hidden="true"></my-global-icon>
4 <ng-container i18n>My followers</ng-container> 4 <ng-container i18n>My followers</ng-container>
5 <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> 5 <span *ngIf="pagination.totalItems" class="pt-badge badge-secondary"> {{ pagination.totalItems }}</span>
6 </span> 6 </span>
7</h1> 7</h1>
8 8
@@ -14,15 +14,15 @@
14 14
15<div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> 15<div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
16 <div *ngFor="let follow of follows" class="actor"> 16 <div *ngFor="let follow of follows" class="actor">
17 <my-actor-avatar [account]="follow.follower" [href]="follow.follower.url" size="40"></my-actor-avatar> 17 <my-actor-avatar [actor]="follow.follower" actorType="account" [href]="follow.follower.url" size="40"></my-actor-avatar>
18 18
19 <div class="actor-info"> 19 <div class="actor-info">
20 <a [href]="follow.follower.url" class="actor-names" rel="noopener noreferrer" target="_blank" i18n-title title="Follower page"> 20 <a [href]="follow.follower.url" class="actor-names" rel="noopener noreferrer" target="_blank" i18n-title title="Follower page">
21 <div class="actor-display-name">{{ follow.follower.name + '@' + follow.follower.host }}</div> 21 <div class="actor-display-name">{{ follow.follower.name + '@' + follow.follower.host }}</div>
22 <span class="glyphicon glyphicon-new-window"></span> 22 <my-global-icon iconName="external-link"></my-global-icon>
23 </a> 23 </a>
24 24
25 <div class="muted"> 25 <div class="small muted">
26 <ng-container *ngIf="isFollowingAccount(follow)" i18n>Is following all your channels</ng-container> 26 <ng-container *ngIf="isFollowingAccount(follow)" i18n>Is following all your channels</ng-container>
27 <ng-container *ngIf="!isFollowingAccount(follow)" i18n>Is following your channel {{ follow.following.name }}</ng-container> 27 <ng-container *ngIf="!isFollowingAccount(follow)" i18n>Is following your channel {{ follow.following.name }}</ng-container>
28 </div> 28 </div>
diff --git a/client/src/app/+my-library/my-follows/my-followers.component.scss b/client/src/app/+my-library/my-follows/my-followers.component.scss
index fae4cd972..2caa4cc48 100644
--- a/client/src/app/+my-library/my-follows/my-followers.component.scss
+++ b/client/src/app/+my-library/my-follows/my-followers.component.scss
@@ -13,14 +13,4 @@ input[type=text] {
13 13
14.actor { 14.actor {
15 @include actor-row($min-height: auto, $separator: true); 15 @include actor-row($min-height: auto, $separator: true);
16
17 .actor-display-name {
18 font-size: 16px;
19
20 + .glyphicon {
21 @include margin-left(5px);
22
23 font-size: 12px;
24 }
25 }
26} 16}
diff --git a/client/src/app/+my-library/my-follows/my-subscriptions.component.html b/client/src/app/+my-library/my-follows/my-subscriptions.component.html
index 391c4d3be..d5f164bf2 100644
--- a/client/src/app/+my-library/my-follows/my-subscriptions.component.html
+++ b/client/src/app/+my-library/my-follows/my-subscriptions.component.html
@@ -2,7 +2,7 @@
2 <span> 2 <span>
3 <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon> 3 <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon>
4 <ng-container i18n>My subscriptions</ng-container> 4 <ng-container i18n>My subscriptions</ng-container>
5 <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> 5 <span *ngIf="pagination.totalItems" class="pt-badge badge-secondary"> {{ pagination.totalItems }}</span>
6 </span> 6 </span>
7</h1> 7</h1>
8 8
@@ -14,7 +14,7 @@
14 14
15<div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> 15<div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
16 <div *ngFor="let videoChannel of videoChannels" class="actor"> 16 <div *ngFor="let videoChannel of videoChannels" class="actor">
17 <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]" size="80"></my-actor-avatar> 17 <my-actor-avatar [actor]="videoChannel" actorType="channel" [internalHref]="[ '/c', videoChannel.nameWithHost ]" size="80"></my-actor-avatar>
18 18
19 <div class="actor-info"> 19 <div class="actor-info">
20 <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="actor-names" i18n-title title="Channel page"> 20 <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="actor-names" i18n-title title="Channel page">
@@ -27,7 +27,7 @@
27 <a [routerLink]="[ '/a', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner"> 27 <a [routerLink]="[ '/a', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner">
28 <span i18n>Created by {{ videoChannel.ownerBy }}</span> 28 <span i18n>Created by {{ videoChannel.ownerBy }}</span>
29 29
30 <my-actor-avatar [account]="videoChannel.ownerAccount" size="18"></my-actor-avatar> 30 <my-actor-avatar [actor]="videoChannel.ownerAccount" actorType="account" size="18"></my-actor-avatar>
31 </a> 31 </a>
32 </div> 32 </div>
33 33
diff --git a/client/src/app/+my-library/my-history/my-history.component.html b/client/src/app/+my-library/my-history/my-history.component.html
index 14bf01804..6791dab52 100644
--- a/client/src/app/+my-library/my-history/my-history.component.html
+++ b/client/src/app/+my-library/my-history/my-history.component.html
@@ -1,6 +1,7 @@
1<h1> 1<h1>
2 <my-global-icon iconName="history" aria-hidden="true"></my-global-icon> 2 <my-global-icon iconName="history" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>My watch history</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> 3 <ng-container i18n>My watch history</ng-container>
4 <span *ngIf="pagination.totalItems" class="pt-badge badge-secondary">{{ pagination.totalItems }}</span>
4</h1> 5</h1>
5 6
6<div class="top-buttons"> 7<div class="top-buttons">
@@ -26,7 +27,7 @@
26 [titlePage]="titlePage" 27 [titlePage]="titlePage"
27 [getVideosObservableFunction]="getVideosObservableFunction" 28 [getVideosObservableFunction]="getVideosObservableFunction"
28 [user]="user" 29 [user]="user"
29 i18n-noResultMessage noResultMessage="You don't have any video in your watch history yet." 30 [noResultMessage]="getNoResultMessage()"
30 [enableSelection]="false" 31 [enableSelection]="false"
31 [disabled]="disabled" 32 [disabled]="disabled"
32 #videosSelection 33 #videosSelection
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 3257b2215..21011a089 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
@@ -6,7 +6,6 @@
6 justify-content: center; 6 justify-content: center;
7 margin-top: 50px; 7 margin-top: 50px;
8 font-weight: $font-semibold; 8 font-weight: $font-semibold;
9 font-size: 16px;
10} 9}
11 10
12.top-buttons { 11.top-buttons {
@@ -29,11 +28,11 @@
29 28
30 grid-column: 3; 29 grid-column: 3;
31 display: flex; 30 display: flex;
31 align-items: center;
32 32
33 label { 33 label {
34 margin: 0 0 0 5px; 34 margin: 0 0 0 5px;
35 color: var(--greyForegroundColor); 35 color: var(--greyForegroundColor);
36 font-size: 15px;
37 font-weight: $font-semibold; 36 font-weight: $font-semibold;
38 } 37 }
39 } 38 }
@@ -44,8 +43,6 @@
44 @include button-with-icon; 43 @include button-with-icon;
45 44
46 grid-column: 4; 45 grid-column: 4;
47
48 font-size: 15px;
49 } 46 }
50} 47}
51 48
diff --git a/client/src/app/+my-library/my-history/my-history.component.ts b/client/src/app/+my-library/my-history/my-history.component.ts
index f6b712908..c4878c957 100644
--- a/client/src/app/+my-library/my-history/my-history.component.ts
+++ b/client/src/app/+my-library/my-history/my-history.component.ts
@@ -93,8 +93,8 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook {
93 .subscribe({ 93 .subscribe({
94 next: () => { 94 next: () => {
95 const message = this.videosHistoryEnabled === true 95 const message = this.videosHistoryEnabled === true
96 ? $localize`Videos history is enabled` 96 ? $localize`Video history is enabled`
97 : $localize`Videos history is disabled` 97 : $localize`Video history is disabled`
98 98
99 this.notifier.success(message) 99 this.notifier.success(message)
100 100
@@ -117,8 +117,8 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook {
117 } 117 }
118 118
119 async clearAllHistory () { 119 async clearAllHistory () {
120 const title = $localize`Delete videos history` 120 const title = $localize`Delete video history`
121 const message = $localize`Are you sure you want to delete all your videos history?` 121 const message = $localize`Are you sure you want to delete all your video history?`
122 122
123 const res = await this.confirmService.confirm(message, title) 123 const res = await this.confirmService.confirm(message, title)
124 if (res !== true) return 124 if (res !== true) return
@@ -126,7 +126,7 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook {
126 this.userHistoryService.clearAll() 126 this.userHistoryService.clearAll()
127 .subscribe({ 127 .subscribe({
128 next: () => { 128 next: () => {
129 this.notifier.success($localize`Videos history deleted`) 129 this.notifier.success($localize`Video history deleted`)
130 130
131 this.reloadData() 131 this.reloadData()
132 }, 132 },
@@ -134,4 +134,12 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook {
134 error: err => this.notifier.error(err.message) 134 error: err => this.notifier.error(err.message)
135 }) 135 })
136 } 136 }
137
138 getNoResultMessage () {
139 if (this.search) {
140 return $localize`No videos found for "${this.search}".`
141 }
142
143 return $localize`You don't have any video in your watch history yet.`
144 }
137} 145}
diff --git a/client/src/app/+my-library/my-library.component.html b/client/src/app/+my-library/my-library.component.html
index b465d0156..1c44c8472 100644
--- a/client/src/app/+my-library/my-library.component.html
+++ b/client/src/app/+my-library/my-library.component.html
@@ -1,4 +1,4 @@
1<div class="row"> 1<div class="root">
2 <my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown> 2 <my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
3 3
4 <div class="margin-content pb-5" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }"> 4 <div class="margin-content pb-5" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }">
diff --git a/client/src/app/+my-library/my-library.component.scss b/client/src/app/+my-library/my-library.component.scss
index 1ec25315a..6275b7ac2 100644
--- a/client/src/app/+my-library/my-library.component.scss
+++ b/client/src/app/+my-library/my-library.component.scss
@@ -1,7 +1,7 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.row { 4.root {
5 @include sub-menu-h1; 5 @include sub-menu-h1;
6 6
7 flex-direction: column; 7 flex-direction: column;
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.html b/client/src/app/+my-library/my-ownership/my-ownership.component.html
index c29c71c0a..649b3fef1 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,7 +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 <my-actor-avatar [account]="videoChangeOwnership.initiatorAccount" size="32"></my-actor-avatar> 40 <my-actor-avatar [actor]="videoChangeOwnership.initiatorAccount" actorType="account" size="32"></my-actor-avatar>
41 <div> 41 <div>
42 {{ videoChangeOwnership.initiatorAccount.displayName }} 42 {{ videoChangeOwnership.initiatorAccount.displayName }}
43 <span class="muted">{{ videoChangeOwnership.initiatorAccount.nameWithHost }}</span> 43 <span class="muted">{{ videoChangeOwnership.initiatorAccount.nameWithHost }}</span>
@@ -65,7 +65,7 @@
65 <td>{{ videoChangeOwnership.createdAt | date: 'short' }}</td> 65 <td>{{ videoChangeOwnership.createdAt | date: 'short' }}</td>
66 66
67 <td> 67 <td>
68 <span class="badge" 68 <span class="pt-badge"
69 [ngClass]="getStatusClass(videoChangeOwnership.status)">{{ videoChangeOwnership.status }}</span> 69 [ngClass]="getStatusClass(videoChangeOwnership.status)">{{ videoChangeOwnership.status }}</span>
70 </td> 70 </td>
71 </tr> 71 </tr>
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.scss b/client/src/app/+my-library/my-ownership/my-ownership.component.scss
index d32477270..a8450ff1b 100644
--- a/client/src/app/+my-library/my-ownership/my-ownership.component.scss
+++ b/client/src/app/+my-library/my-ownership/my-ownership.component.scss
@@ -55,13 +55,6 @@
55 color: pvar(--mainForegroundColor); 55 color: pvar(--mainForegroundColor);
56 line-height: 1rem; 56 line-height: 1rem;
57 57
58 div .glyphicon {
59 @include margin-left(0.1rem);
60
61 font-size: 80%;
62 color: #808080;
63 }
64
65 div + div { 58 div + div {
66 font-size: 80%; 59 font-size: 80%;
67 } 60 }
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.html b/client/src/app/+my-library/my-video-imports/my-video-imports.component.html
index 1525d0bd1..79fb4da26 100644
--- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.html
+++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.html
@@ -50,7 +50,7 @@
50 </td> 50 </td>
51 51
52 <td> 52 <td>
53 <span class="badge" [ngClass]="getVideoImportStateClass(videoImport.state.id)"> 53 <span class="pt-badge" [ngClass]="getVideoImportStateClass(videoImport.state.id)">
54 {{ videoImport.state.label }} 54 {{ videoImport.state.label }}
55 </span> 55 </span>
56 </td> 56 </td>
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html
index c39e90a1e..e867f63b8 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.html
@@ -20,13 +20,13 @@
20 20
21<form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 21<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
22 22
23 <div class="form-row"> <!-- playlist grid --> 23 <div class="row"> <!-- playlist grid -->
24 <div class="form-group col-12 col-lg-4 col-xl-3"> 24 <div class="col-12 col-lg-4 col-xl-3">
25 <div *ngIf="isCreation()" class="video-playlist-title" i18n>NEW PLAYLIST</div> 25 <div *ngIf="isCreation()" class="video-playlist-title" i18n>NEW PLAYLIST</div>
26 <div *ngIf="!isCreation() && videoPlaylistToUpdate" class="video-playlist-title" i18n>PLAYLIST</div> 26 <div *ngIf="!isCreation() && videoPlaylistToUpdate" class="video-playlist-title" i18n>PLAYLIST</div>
27 </div> 27 </div>
28 28
29 <div class="form-group col-12 col-lg-8 col-xl-9"> 29 <div class="col-12 col-lg-8 col-xl-9">
30 30
31 <div class="col-md-12 col-xl-6"> 31 <div class="col-md-12 col-xl-6">
32 <div class="form-group"> 32 <div class="form-group">
@@ -42,13 +42,7 @@
42 42
43 <div class="form-group"> 43 <div class="form-group">
44 <label i18n for="description">Description</label><my-help helpType="markdownText"></my-help> 44 <label i18n for="description">Description</label><my-help helpType="markdownText"></my-help>
45 <my-markdown-textarea 45 <my-markdown-textarea id="description" formControlName="description" [formError]="formErrors['description']"></my-markdown-textarea>
46 id="description" formControlName="description"
47 [ngClass]="{ 'input-error': formErrors['description'] }"
48 ></my-markdown-textarea>
49 <div *ngIf="formErrors.description" class="form-error">
50 {{ formErrors.description }}
51 </div>
52 </div> 46 </div>
53 </div> 47 </div>
54 48
@@ -88,7 +82,7 @@
88 </div> 82 </div>
89 </div> 83 </div>
90 84
91 <div class="form-row"> <!-- submit placement block --> 85 <div class="row"> <!-- submit placement block -->
92 <div class="col-md-7 col-xl-5"></div> 86 <div class="col-md-7 col-xl-5"></div>
93 <div class="col-md-5 col-xl-5 d-inline-flex"> 87 <div class="col-md-5 col-xl-5 d-inline-flex">
94 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 88 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss
index 840e449ea..93bc18fe2 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-edit.component.scss
@@ -1,11 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4label {
5 font-weight: $font-regular;
6 font-size: 100%;
7}
8
9.video-playlist-title { 4.video-playlist-title {
10 @include settings-big-title; 5 @include settings-big-title;
11} 6}
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
index 25b742bff..0091f70be 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
@@ -1,6 +1,7 @@
1<h1> 1<h1>
2 <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> 2 <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> 3 <ng-container i18n>My playlists</ng-container>
4 <span *ngIf="pagination.totalItems" class="pt-badge badge-secondary">{{ pagination.totalItems }}</span>
4</h1> 5</h1>
5 6
6<my-channels-setup-message></my-channels-setup-message> 7<my-channels-setup-message></my-channels-setup-message>
diff --git a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html
index 955fd4884..56ff0e788 100644
--- a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html
+++ b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.html
@@ -17,7 +17,7 @@
17 </div> 17 </div>
18 18
19 <div class="modal-footer"> 19 <div class="modal-footer">
20 <div class="form-group inputs"> 20 <div class="inputs">
21 <input 21 <input
22 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button" 22 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
23 (click)="dismiss()" (key.enter)="dismiss()" 23 (click)="dismiss()" (key.enter)="dismiss()"
diff --git a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss
index 0eb694162..48c073192 100644
--- a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss
+++ b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.scss
@@ -4,7 +4,3 @@
4p-autocomplete { 4p-autocomplete {
5 display: block; 5 display: block;
6} 6}
7
8.form-group {
9 margin: 20px 0;
10}
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.html b/client/src/app/+my-library/my-videos/my-videos.component.html
index 7f12e2c71..146dcf41e 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.html
+++ b/client/src/app/+my-library/my-videos/my-videos.component.html
@@ -2,7 +2,7 @@
2 <span> 2 <span>
3 <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon> 3 <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon>
4 <ng-container i18n>My videos</ng-container> 4 <ng-container i18n>My videos</ng-container>
5 <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> 5 <span *ngIf="pagination.totalItems" class="pt-badge badge-secondary"> {{ pagination.totalItems }}</span>
6 </span> 6 </span>
7 7
8 <div> 8 <div>
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts
index 91cc06702..2f1eb84ba 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.ts
+++ b/client/src/app/+my-library/my-videos/my-videos.component.ts
@@ -4,7 +4,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' 5import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core'
6import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' 6import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
7import { immutableAssign } from '@app/helpers' 7import { prepareIcu, immutableAssign } from '@app/helpers'
8import { AdvancedInputFilter } from '@app/shared/shared-forms' 8import { AdvancedInputFilter } from '@app/shared/shared-forms'
9import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 9import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
10import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' 10import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
@@ -167,7 +167,10 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
167 .map(k => parseInt(k, 10)) 167 .map(k => parseInt(k, 10))
168 168
169 const res = await this.confirmService.confirm( 169 const res = await this.confirmService.confirm(
170 $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 170 prepareIcu($localize`Do you really want to delete {length, plural, =1 {this video} other {{length} videos}}?`)(
171 { length: toDeleteVideosIds.length },
172 $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?`
173 ),
171 $localize`Delete` 174 $localize`Delete`
172 ) 175 )
173 if (res === false) return 176 if (res === false) return
@@ -184,7 +187,13 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
184 .pipe(toArray()) 187 .pipe(toArray())
185 .subscribe({ 188 .subscribe({
186 next: () => { 189 next: () => {
187 this.notifier.success($localize`${toDeleteVideosIds.length} videos deleted.`) 190 this.notifier.success(
191 prepareIcu($localize`{length, plural, =1 {Video has been deleted} other {{length} videos have been deleted}}`)(
192 { length: toDeleteVideosIds.length },
193 $localize`${toDeleteVideosIds.length} have been deleted.`
194 )
195 )
196
188 this.selection = {} 197 this.selection = {}
189 }, 198 },
190 199
diff --git a/client/src/app/+page-not-found/page-not-found.component.html b/client/src/app/+page-not-found/page-not-found.component.html
index 0333f9550..70ede26e8 100644
--- a/client/src/app/+page-not-found/page-not-found.component.html
+++ b/client/src/app/+page-not-found/page-not-found.component.html
@@ -1,7 +1,7 @@
1<div class="root"> 1<div class="root">
2 <div *ngIf="status !== 403 && status !== 418" class="box"> 2 <div *ngIf="status !== 403 && status !== 418" class="box">
3 <strong>{{ status }}.</strong> 3 <strong>{{ status }}.</strong>
4 <span class="ml-1 muted" i18n>That's an error.</span> 4 <span class="ms-1 muted" i18n>That's an error.</span>
5 5
6 <div class="text mt-4"> 6 <div class="text mt-4">
7 <ng-container *ngIf="type === 'video'" i18n>We couldn't find any video tied to the URL {{ pathname }} you were looking for.</ng-container> 7 <ng-container *ngIf="type === 'video'" i18n>We couldn't find any video tied to the URL {{ pathname }} you were looking for.</ng-container>
@@ -24,7 +24,7 @@
24 24
25 <div *ngIf="status === 403" class="box"> 25 <div *ngIf="status === 403" class="box">
26 <strong>{{ status }}.</strong> 26 <strong>{{ status }}.</strong>
27 <span class="ml-1 muted" i18n>You are not authorized here.</span> 27 <span class="ms-1 muted" i18n>You are not authorized here.</span>
28 28
29 <div class="text mt-4"> 29 <div class="text mt-4">
30 <ng-container *ngIf="type === 'video'" i18n>You might need to check your account is allowed by the video or instance owner.</ng-container> 30 <ng-container *ngIf="type === 'video'" i18n>You might need to check your account is allowed by the video or instance owner.</ng-container>
@@ -34,7 +34,7 @@
34 34
35 <div *ngIf="status === 418" class="box"> 35 <div *ngIf="status === 418" class="box">
36 <strong>{{ status }}.</strong> 36 <strong>{{ status }}.</strong>
37 <span class="ml-1 muted">I'm a teapot.</span> 37 <span class="ms-1 muted">I'm a teapot.</span>
38 38
39 <div class="text mt-4" i18n="Description of a tea flavour, keeping the 'requested entity body' as a technical expression referring to a web request"> 39 <div class="text mt-4" i18n="Description of a tea flavour, keeping the 'requested entity body' as a technical expression referring to a web request">
40 The requested entity body blends sweet bits with a mellow earthiness. 40 The requested entity body blends sweet bits with a mellow earthiness.
diff --git a/client/src/app/+remote-interaction/remote-interaction.component.html b/client/src/app/+remote-interaction/remote-interaction.component.html
index e59783b9a..135ddc00b 100644
--- a/client/src/app/+remote-interaction/remote-interaction.component.html
+++ b/client/src/app/+remote-interaction/remote-interaction.component.html
@@ -1,7 +1,5 @@
1<div class="root"> 1<div class="root">
2 2
3 <div class="alert alert-error" *ngIf="error"> 3 <div class="alert alert-danger" *ngIf="error">{{ error }}</div>
4 {{ error }}
5 </div>
6 4
7</div> 5</div>
diff --git a/client/src/app/+reset-password/reset-password.component.html b/client/src/app/+reset-password/reset-password.component.html
index 143f426ec..fa53e7526 100644
--- a/client/src/app/+reset-password/reset-password.component.html
+++ b/client/src/app/+reset-password/reset-password.component.html
@@ -1,31 +1,28 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <div i18n class="title-page title-page-single"> 2 <h1 i18n class="title-page">Reset my password</h1>
3 Reset my password
4 </div>
5 3
6 <form role="form" (ngSubmit)="resetPassword()" [formGroup]="form"> 4 <form role="form" (ngSubmit)="resetPassword()" [formGroup]="form">
7 <div class="form-group"> 5 <div class="form-group">
8 <label i18n for="password">Password</label> 6 <label i18n for="password">Password</label>
9 <my-input-toggle-hidden formControlName="password" inputId="password" 7
10 i18n-placeholder placeholder="Password" 8 <my-input-text
11 [ngClass]="{ 'input-error': formErrors['password'] }" 9 formControlName="password" inputId="password" i18n-placeholder placeholder="Password"
12 autocomplete="new-password"></my-input-toggle-hidden> 10 [formError]="formErrors['password']" autocomplete="new-password"
13 <div *ngIf="formErrors.password" class="form-error"> 11 ></my-input-text>
14 {{ formErrors.password }}
15 </div>
16 </div> 12 </div>
17 13
18 <div class="form-group"> 14 <div class="form-group">
19 <label i18n for="password-confirm">Confirm password</label> 15 <label i18n for="password-confirm">Confirm password</label>
20 <my-input-toggle-hidden formControlName="password-confirm" inputId="password-confirm" 16
21 i18n-placeholder placeholder="Confirmed password" 17 <my-input-text
22 [ngClass]="{ 'input-error': formErrors['password-confirm'] }" 18 formControlName="password-confirm" inputId="password-confirm" i18n-placeholder placeholder="Confirmed password"
23 autocomplete="new-password"></my-input-toggle-hidden> 19 [formError]="formErrors['password-confirm']" autocomplete="new-password"
24 <div *ngIf="formErrors['password-confirm']" class="form-error"> 20 ></my-input-text>
25 {{ formErrors['password-confirm'] }}
26 </div>
27 </div> 21 </div>
28 22
29 <input type="submit" i18n-value value="Reset my password" [disabled]="!form.valid || !isConfirmedPasswordValid()"> 23 <input
24 class="peertube-button orange-button" type="submit" i18n-value value="Reset my password"
25 [disabled]="!form.valid || !isConfirmedPasswordValid()"
26 >
30 </form> 27 </form>
31</div> 28</div>
diff --git a/client/src/app/+reset-password/reset-password.component.scss b/client/src/app/+reset-password/reset-password.component.scss
index 5494e29c6..dd981ca39 100644
--- a/client/src/app/+reset-password/reset-password.component.scss
+++ b/client/src/app/+reset-password/reset-password.component.scss
@@ -1,12 +1,7 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4input:not([type=submit]) { 4my-input-text {
5 @include peertube-input-text(340px); 5 max-width: 340px;
6 display: block; 6 display: block;
7} 7}
8
9input[type=submit] {
10 @include peertube-button;
11 @include orange-button;
12}
diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html
index c4861e8c4..5bce009d5 100644
--- a/client/src/app/+search/search-filters.component.html
+++ b/client/src/app/+search/search-filters.component.html
@@ -77,7 +77,7 @@
77 </div> 77 </div>
78 78
79 <div class="row"> 79 <div class="row">
80 <div class="pl-0 col-sm-6"> 80 <div class="ps-0 col-sm-6">
81 <input 81 <input
82 (change)="onDurationOrPublishedUpdated()" 82 (change)="onDurationOrPublishedUpdated()"
83 (keydown.enter)="$event.preventDefault()" 83 (keydown.enter)="$event.preventDefault()"
@@ -87,7 +87,7 @@
87 class="form-control" 87 class="form-control"
88 > 88 >
89 </div> 89 </div>
90 <div class="pr-0 col-sm-6"> 90 <div class="pe-0 col-sm-6">
91 <input 91 <input
92 (change)="onDurationOrPublishedUpdated()" 92 (change)="onDurationOrPublishedUpdated()"
93 (keydown.enter)="$event.preventDefault()" 93 (keydown.enter)="$event.preventDefault()"
diff --git a/client/src/app/+search/search-filters.component.scss b/client/src/app/+search/search-filters.component.scss
index ece4ba5b5..c55e59310 100644
--- a/client/src/app/+search/search-filters.component.scss
+++ b/client/src/app/+search/search-filters.component.scss
@@ -6,7 +6,6 @@ form {
6} 6}
7 7
8.radio-label { 8.radio-label {
9 font-size: 15px;
10 font-weight: $font-bold; 9 font-weight: $font-bold;
11} 10}
12 11
@@ -22,10 +21,6 @@ form {
22 margin-bottom: 1rem; 21 margin-bottom: 1rem;
23} 22}
24 23
25.form-group {
26 margin-bottom: 25px;
27}
28
29input[type=text] { 24input[type=text] {
30 @include peertube-input-text(100%); 25 @include peertube-input-text(100%);
31 display: block; 26 display: block;
diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html
index 2c84dd930..37da67005 100644
--- a/client/src/app/+search/search.component.html
+++ b/client/src/app/+search/search.component.html
@@ -2,22 +2,22 @@
2 <div class="results-header"> 2 <div class="results-header">
3 <div class="first-line"> 3 <div class="first-line">
4 <div class="results-counter" *ngIf="pagination.totalItems"> 4 <div class="results-counter" *ngIf="pagination.totalItems">
5 <span class="mr-1" i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}}</span> 5 <span class="me-1" i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}}</span>
6 6
7 <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span> 7 <span class="me-1" i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span>
8 <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span> 8 <span class="me-1" i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span>
9 9
10 <span *ngIf="currentSearch" i18n>for <span class="search-value">{{ currentSearch }}</span></span> 10 <span *ngIf="currentSearch" i18n>for <span class="search-value">{{ currentSearch }}</span></span>
11 </div> 11 </div>
12 12
13 <div 13 <div
14 class="results-filter-button ml-auto" (click)="isSearchFilterCollapsed = !isSearchFilterCollapsed" role="button" 14 class="results-filter-button ms-auto" (click)="isSearchFilterCollapsed = !isSearchFilterCollapsed" role="button"
15 [attr.aria-expanded]="!isSearchFilterCollapsed" aria-controls="collapseBasic" 15 [attr.aria-expanded]="!isSearchFilterCollapsed" aria-controls="collapseBasic"
16 > 16 >
17 <span class="icon icon-filter"></span> 17 <span class="icon icon-filter"></span>
18 <ng-container i18n> 18 <ng-container i18n>
19 Filters 19 Filters
20 <span *ngIf="numberOfFilters() > 0" class="badge badge-secondary">{{ numberOfFilters() }}</span> 20 <span *ngIf="numberOfFilters() > 0" class="pt-badge badge-secondary">{{ numberOfFilters() }}</span>
21 </ng-container> 21 </ng-container>
22 </div> 22 </div>
23 </div> 23 </div>
@@ -36,7 +36,7 @@
36 <ng-container *ngFor="let result of results"> 36 <ng-container *ngFor="let result of results">
37 <div *ngIf="isVideoChannel(result)" class="entry video-channel"> 37 <div *ngIf="isVideoChannel(result)" class="entry video-channel">
38 38
39 <my-actor-avatar [channel]="result" [internalHref]="getInternalChannelUrl(result)" [href]="getExternalChannelUrl(result)" size="120"></my-actor-avatar> 39 <my-actor-avatar [actor]="result" actorType="channel" [internalHref]="getInternalChannelUrl(result)" [href]="getExternalChannelUrl(result)" size="120"></my-actor-avatar>
40 40
41 <div class="video-channel-info"> 41 <div class="video-channel-info">
42 <a *ngIf="!isExternalChannelUrl()" [routerLink]="getInternalChannelUrl(result)" class="video-channel-names"> 42 <a *ngIf="!isExternalChannelUrl()" [routerLink]="getInternalChannelUrl(result)" class="video-channel-names">
diff --git a/client/src/app/+search/search.component.scss b/client/src/app/+search/search.component.scss
index cab1d0e88..11d95df6a 100644
--- a/client/src/app/+search/search.component.scss
+++ b/client/src/app/+search/search.component.scss
@@ -20,7 +20,6 @@
20} 20}
21 21
22.results-header { 22.results-header {
23 font-size: 16px;
24 padding-bottom: 20px; 23 padding-bottom: 20px;
25 margin-bottom: 30px; 24 margin-bottom: 30px;
26 border-bottom: 1px solid #DADADA; 25 border-bottom: 1px solid #DADADA;
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts
index b9ec6dbcc..62b1c4446 100644
--- a/client/src/app/+search/search.component.ts
+++ b/client/src/app/+search/search.component.ts
@@ -248,11 +248,11 @@ export class SearchComponent implements OnInit, OnDestroy {
248 } 248 }
249 249
250 private updateTitle () { 250 private updateTitle () {
251 const suffix = this.currentSearch 251 const title = this.currentSearch
252 ? ' ' + this.currentSearch 252 ? $localize`Search ${this.currentSearch}`
253 : '' 253 : $localize`Search`
254 254
255 this.metaService.setTitle($localize`Search` + suffix) 255 this.metaService.setTitle(title)
256 } 256 }
257 257
258 private updateUrlFromAdvancedSearch () { 258 private updateUrlFromAdvancedSearch () {
diff --git a/client/src/app/+signup/+register/custom-stepper.component.html b/client/src/app/+signup/+register/custom-stepper.component.html
index aad2f31d3..f43a46842 100644
--- a/client/src/app/+signup/+register/custom-stepper.component.html
+++ b/client/src/app/+signup/+register/custom-stepper.component.html
@@ -1,24 +1,29 @@
1<section class="container"> 1<section>
2 <header *ngIf="steps.length > 2"> 2 <header *ngIf="steps.length > 2">
3 <ng-container *ngFor="let step of steps; let i = index; let isLast = last;"> 3 <div class="header-steps">
4 <div 4 <ng-container *ngFor="let step of steps; let i = index; let isLast = last;">
5 class="step-info" [ngClass]="{ active: selectedIndex === i, completed: isCompleted(step), 'c-hand': isAccessible(i) }" [attr.aria-current]="selectedIndex === i" 5 <div
6 (click)="onClick(i)" 6 class="step-info" [ngClass]="{ active: selectedIndex === i, completed: isCompleted(step), 'c-hand': isAccessible(step) }" [attr.aria-current]="selectedIndex === i"
7 > 7 (click)="onClick(i)"
8 <div class="step-index"> 8 >
9 <ng-container *ngIf="!isCompleted(step)"><span class="sr-only" i18n>Step</span> {{ i + 1 }}</ng-container> 9 <div class="step-index">
10 <my-global-icon *ngIf="isCompleted(step)" iconName="tick"></my-global-icon> 10 <span class="visually-hidden" i18n>Step</span> {{ i + 1 }}
11 </div> 11
12 <div class="completed-icon" *ngIf="isCompleted(step)">
13 <my-global-icon iconName="tick"></my-global-icon>
14 </div>
15 </div>
12 16
13 <div class="step-label">{{ step.label }}</div> 17 <div class="step-label">{{ step.label }}</div>
14 </div> 18 </div>
15 19
16 <!-- Do no display if this is the last child --> 20 <!-- Do no display if this is the last child -->
17 <div *ngIf="!isLast" class="connector"></div> 21 <div *ngIf="!isLast" class="connector"></div>
18 </ng-container> 22 </ng-container>
23 </div>
19 </header> 24 </header>
20 25
21 <div [style.display]="selected ? 'block' : 'none'"> 26 <div class="margin-content" [style.display]="selected ? 'block' : 'none'">
22 <ng-container [ngTemplateOutlet]="selected.content"></ng-container> 27 <ng-container [ngTemplateOutlet]="selected.content"></ng-container>
23 </div> 28 </div>
24 29
diff --git a/client/src/app/+signup/+register/custom-stepper.component.scss b/client/src/app/+signup/+register/custom-stepper.component.scss
index 6a8815c77..dfbdc863b 100644
--- a/client/src/app/+signup/+register/custom-stepper.component.scss
+++ b/client/src/app/+signup/+register/custom-stepper.component.scss
@@ -2,76 +2,113 @@
2@use '_variables' as *; 2@use '_variables' as *;
3@use '_mixins' as *; 3@use '_mixins' as *;
4 4
5$grey-color: #9CA3AB; 5$index-block-height: 40px;
6$index-block-height: 32px;
7 6
8.container { 7header {
9 @include padding-left(0); 8 margin-bottom: 40px;
10 @include padding-right(0); 9 padding-bottom: 60px;
11 max-width: unset !important; 10 width: 100%;
11 background-color: pvar(--mainColorVeryLight);
12} 12}
13 13
14header { 14.header-steps {
15 max-width: 800px;
15 display: flex; 16 display: flex;
16 justify-content: space-between; 17 justify-content: space-between;
17 font-size: 15px; 18 margin: auto;
18 margin-bottom: 30px; 19
20 // Useful on small screens
21 padding: 0 20px;
22}
19 23
20 .step-info { 24.step-index {
21 color: $grey-color; 25 display: flex;
26 justify-content: center;
27 align-items: center;
28 width: $index-block-height;
29 height: $index-block-height;
30 border-radius: $index-block-height;
31 border: 1px solid pvar(--mainColor);
32 margin-bottom: 10px;
33 font-size: 24px;
34 position: relative;
35
36 .completed-icon {
37 width: 16px;
38 height: 16px;
39 border-radius: 16px;
40 background-color: pvar(--mainBackgroundColor);
41 position: absolute;
42 bottom: 0;
43 right: 0;
22 display: flex; 44 display: flex;
23 flex-direction: column; 45 justify-content: center;
24 align-items: center; 46 align-items: center;
25 width: $index-block-height; 47 border: 1px solid pvar(--mainColor);
48
49 my-global-icon {
50 @include apply-svg-color(pvar(--mainColor));
26 51
27 &:not(.c-hand) { 52 display: flex;
28 cursor: default; 53 width: 12px;
54 height: 12px;
29 } 55 }
56 }
57}
30 58
59.step-label {
60 width: max-content;
61 font-size: 18px;
62}
63
64.step-info {
65 color: pvar(--mainColor);
66 display: flex;
67 flex-direction: column;
68 align-items: center;
69 width: $index-block-height;
70 opacity: 0.5;
71 cursor: default;
72
73 &.c-hand {
74 cursor: pointer;
75 }
76
77 &.active,
78 &.completed {
31 .step-index { 79 .step-index {
32 display: flex; 80 background-color: pvar(--mainColor);
33 justify-content: center; 81 color: pvar(--mainBackgroundColor);
34 align-items: center;
35 width: $index-block-height;
36 height: $index-block-height;
37 border-radius: 100px;
38 border: 2px solid $grey-color;
39 margin-bottom: 10px;
40
41 my-global-icon {
42 @include apply-svg-color(pvar(--mainBackgroundColor));
43
44 width: 22px;
45 height: 22px;
46 }
47 } 82 }
48 83
49 .step-label { 84 .step-label {
50 width: max-content; 85 color: pvar(--mainColor);
51 } 86 }
87 }
52 88
53 &.active, 89 &.active {
54 &.completed { 90 opacity: 1;
55 .step-index { 91 }
56 border-color: pvar(--mainColor); 92}
57 background-color: pvar(--mainColor);
58 color: pvar(--mainBackgroundColor);
59 }
60
61 .step-label {
62 color: pvar(--mainColor);
63 }
64 }
65 93
66 &.completed { 94.connector {
67 cursor: pointer; 95 flex: auto;
68 } 96 margin: math.div($index-block-height, 2) 10px 0 10px;
97 height: 2px;
98 background-color: pvar(--mainColor);
99 opacity: 0.3;
100}
101
102@media screen and (min-width: $small-view) {
103 .margin-content {
104 max-width: 1000px;
105 margin: auto;
69 } 106 }
107}
70 108
71 .connector { 109@media screen and (max-width: $small-view) {
72 flex: auto; 110 .step-label {
73 margin: math.div($index-block-height, 2) 10px 0 10px; 111 width: auto;
74 height: 2px; 112 text-align: center;
75 background-color: $grey-color;
76 } 113 }
77} 114}
diff --git a/client/src/app/+signup/+register/custom-stepper.component.ts b/client/src/app/+signup/+register/custom-stepper.component.ts
index 3b7ba40e8..4c308f7b6 100644
--- a/client/src/app/+signup/+register/custom-stepper.component.ts
+++ b/client/src/app/+signup/+register/custom-stepper.component.ts
@@ -14,13 +14,10 @@ export class CustomStepperComponent extends CdkStepper {
14 } 14 }
15 15
16 isCompleted (step: CdkStep) { 16 isCompleted (step: CdkStep) {
17 return step.stepControl?.dirty && step.stepControl.valid 17 return step.completed
18 } 18 }
19 19
20 isAccessible (index: number) { 20 isAccessible (step: CdkStep) {
21 const stepsCompletedMap = this.steps.map(step => this.isCompleted(step)) 21 return step.editable && step.completed
22 return index === 0
23 ? true
24 : stepsCompletedMap[index - 1]
25 } 22 }
26} 23}
diff --git a/client/src/app/+signup/+register/register-step-channel.component.html b/client/src/app/+signup/+register/register-step-channel.component.html
deleted file mode 100644
index 67f332409..000000000
--- a/client/src/app/+signup/+register/register-step-channel.component.html
+++ /dev/null
@@ -1,54 +0,0 @@
1<form role="form" [formGroup]="form">
2
3 <div class="channel-explanations">
4 <p i18n>
5 A channel is an entity in which you upload your videos. Creating several of them helps you to organize and separate your content.<br />
6 For example, you could decide to have a channel to publish your piano concerts, and another channel in which you publish your videos talking about ecology.
7 </p>
8
9 <p i18n>
10 Other users can decide to subscribe any channel they want, to be notified when you publish a new video.
11 </p>
12 </div>
13
14 <div class="form-group">
15 <label for="displayName" i18n>Channel display name</label>
16
17 <div class="input-group">
18 <input
19 type="text" id="displayName"
20 formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
21 >
22 </div>
23
24 <div *ngIf="formErrors.displayName" class="form-error">
25 {{ formErrors.displayName }}
26 </div>
27 </div>
28
29 <div class="form-group">
30 <label for="name" i18n>Channel name</label>
31
32 <div class="input-group">
33 <input
34 type="text" id="name" i18n-placeholder placeholder="Example: my_super_channel"
35 formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }"
36 >
37 <div class="input-group-append">
38 <span class="input-group-text">@{{ instanceHost }}</span>
39 </div>
40 </div>
41
42 <div class="name-information" i18n>
43 The channel name is a unique identifier of your channel on this and all the other instances. It's as unique as an email address, which makes it easy for other people to interact with it.
44 </div>
45
46 <div *ngIf="formErrors.name" class="form-error">
47 {{ formErrors.name }}
48 </div>
49
50 <div *ngIf="isSameThanUsername()" class="form-error" i18n>
51 Channel name cannot be the same as your account name. You can click on the first step to update your account name.
52 </div>
53 </div>
54</form>
diff --git a/client/src/app/+signup/+register/register-step-user.component.html b/client/src/app/+signup/+register/register-step-user.component.html
deleted file mode 100644
index cab21c655..000000000
--- a/client/src/app/+signup/+register/register-step-user.component.html
+++ /dev/null
@@ -1,66 +0,0 @@
1<form role="form" [formGroup]="form">
2
3 <div class="capability-information alert alert-info" i18n *ngIf="videoUploadDisabled">
4 Video uploads are disabled on this instance, hence your account won't be able to upload videos.
5 </div>
6
7 <div class="form-group">
8 <label for="displayName" i18n>Display name</label>
9
10 <div class="input-group">
11 <input
12 type="text" id="displayName" placeholder="John Doe"
13 formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
14 >
15 </div>
16
17 <div *ngIf="formErrors.displayName" class="form-error">
18 {{ formErrors.displayName }}
19 </div>
20 </div>
21
22 <div class="form-group">
23 <label for="username" i18n>Username</label>
24
25 <div class="input-group">
26 <input
27 type="text" id="username" i18n-placeholder="Username choice placeholder in the registration form" placeholder="e.g. jane_doe"
28 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }"
29 >
30 <div class="input-group-append">
31 <span class="input-group-text">@{{ instanceHost }}</span>
32 </div>
33 </div>
34
35 <div class="name-information" i18n>
36 The username is a unique identifier of your account on this and all the other instances. It's as unique as an email address, which makes it easy for other people to interact with it.
37 </div>
38
39 <div *ngIf="formErrors.username" class="form-error">
40 {{ formErrors.username }}
41 </div>
42 </div>
43
44 <div class="form-group">
45 <label for="email" i18n>Email</label>
46 <input
47 type="text" id="email" i18n-placeholder placeholder="Email"
48 formControlName="email" class="form-control" [ngClass]="{ 'input-error': formErrors['email'] }"
49 >
50 <div *ngIf="formErrors.email" class="form-error">
51 {{ formErrors.email }}
52 </div>
53 </div>
54
55 <div class="form-group">
56 <label for="password" i18n>Password</label>
57 <my-input-toggle-hidden formControlName="password" inputId="password"
58 i18n-placeholder placeholder="Password"
59 [ngClass]="{ 'input-error': formErrors['password'] }"
60 autocomplete="new-password"></my-input-toggle-hidden>
61 <div *ngIf="formErrors.password" class="form-error">
62 {{ formErrors.password }}
63 </div>
64 </div>
65
66</form>
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html
index 2d0e6e865..442c65e2d 100644
--- a/client/src/app/+signup/+register/register.component.html
+++ b/client/src/app/+signup/+register/register.component.html
@@ -1,64 +1,121 @@
1<div class="margin-content"> 1<div>
2 2
3 <div class="signup-disabled" *ngIf="signupDisabled"> 3 <div class="margin-content signup-disabled" *ngIf="signupDisabled">
4 <div class="alert alert-warning" i18n>Signup is not enabled on this instance.</div> 4 <div class="alert alert-warning" i18n>Signup is not enabled on this instance.</div>
5 </div> 5 </div>
6 6
7 <ng-container *ngIf="!signupDisabled"> 7 <ng-container *ngIf="!signupDisabled">
8 <div i18n class="title-page title-page-single"> 8 <h1 i18n class="title-page-v2">
9 <strong class="underline-orange">{{ instanceName }}</strong>
10 >
9 Create an account 11 Create an account
10 </div> 12 </h1>
11 13
12 <my-signup-success *ngIf="signupDone" [message]="success"></my-signup-success> 14 <div class="register-content">
13 <div *ngIf="info" class="alert alert-info">{{ info }}</div> 15 <my-custom-stepper linear>
14 16
15 <div class="wrapper" [hidden]="signupDone"> 17 <cdk-step i18n-label label="About" [editable]="!signupSuccess">
16 <div class="register-form"> 18 <my-signup-step-title mascotImageName="about" i18n>
17 <my-custom-stepper linear *ngIf="!signupDone"> 19 <strong>Create an account</strong>
18 <cdk-step [stepControl]="formStepTerms" i18n-label="Stepper label for the registration page describing terms of service" label="Terms"> 20 <div>on {{ instanceName }}</div>
19 <div class="instance-information"> 21 </my-signup-step-title>
20 <my-instance-about-accordion 22
21 (init)="onInstanceAboutAccordionInit($event)" [panels]="instanceInformationPanels" 23 <my-register-step-about [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about>
22 pluginScope="signup" pluginHook="filter:signup.instance-about-plugin-panels.create.result" 24
23 ></my-instance-about-accordion> 25 <div class="step-buttons">
24 </div> 26 <a i18n class="skip-step underline-orange" routerLink="/login">
27 <strong>I already have an account</strong>, I log in
28 </a>
25 29
26 <my-register-step-terms 30 <button i18n cdkStepperNext>I create an account</button>
27 [hasCodeOfConduct]="!!aboutHtml.codeOfConduct" 31 </div>
28 [minimumAge]="minimumAge" 32 </cdk-step>
29 (formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
30 ></my-register-step-terms>
31 33
34 <cdk-step [stepControl]="formStepTerms" i18n-label label="Terms" [editable]="!signupSuccess">
35 <my-signup-step-title mascotImageName="terms" i18n>
36 <strong>Terms</strong>
37 <div>of {{ instanceName }}</div>
38 </my-signup-step-title>
39
40 <my-instance-about-accordion
41 [displayInstanceName]="false"
42 (init)="onInstanceAboutAccordionInit($event)" [panels]="instanceInformationPanels"
43 pluginScope="signup" pluginHook="filter:signup.instance-about-plugin-panels.create.result"
44 ></my-instance-about-accordion>
45
46 <my-register-step-terms
47 [hasCodeOfConduct]="!!aboutHtml.codeOfConduct"
48 [minimumAge]="minimumAge"
49 (formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
50 ></my-register-step-terms>
51
52 <div class="step-buttons">
53 <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
32 <button cdkStepperNext [disabled]="!formStepTerms || !formStepTerms.valid">{{ defaultNextStepButtonLabel }}</button> 54 <button cdkStepperNext [disabled]="!formStepTerms || !formStepTerms.valid">{{ defaultNextStepButtonLabel }}</button>
33 </cdk-step> 55 </div>
56 </cdk-step>
57
58 <cdk-step [stepControl]="formStepUser" label="My account" [editable]="!signupSuccess">
59 <my-signup-step-title mascotImageName="account" i18n>
60 <strong>Setup</strong>
61 <div>your account</div>
62 </my-signup-step-title>
34 63
35 <cdk-step [stepControl]="formStepUser" i18n-label="Stepper label for the registration page asking user informations" label="User"> 64 <my-register-step-user
36 <my-register-step-user (formBuilt)="onUserFormBuilt($event)" [videoUploadDisabled]="videoUploadDisabled"></my-register-step-user> 65 (formBuilt)="onUserFormBuilt($event)"
66 [videoUploadDisabled]="videoUploadDisabled" [requiresEmailVerification]="requiresEmailVerification"
67 ></my-register-step-user>
37 68
69 <div class="step-buttons">
38 <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button> 70 <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
39 <button cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid" (click)="videoUploadDisabled && signup()">{{ stepUserButtonLabel }}</button> 71 <button cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid" (click)="videoUploadDisabled && signup()">{{ stepUserButtonLabel }}</button>
40 </cdk-step> 72 </div>
73 </cdk-step>
41 74
42 <cdk-step [stepControl]="formStepChannel" i18n-label="Stepper label for the registration page asking information about the default channel" label="Channel" *ngIf="!videoUploadDisabled"> 75 <cdk-step *ngIf="!videoUploadDisabled" [optional]="true" [stepControl]="formStepChannel" i18n-label label="My channel" [editable]="!signupSuccess">
43 <my-register-step-channel (formBuilt)="onChannelFormBuilt($event)" [username]="getUsername()"></my-register-step-channel> 76 <my-signup-step-title mascotImageName="channel" i18n>
77 <div>Create</div>
78 <strong>your first channel</strong>
79 </my-signup-step-title>
44 80
81 <my-register-step-channel
82 (formBuilt)="onChannelFormBuilt($event)"
83 [videoQuota]="videoQuota" [instanceName]="instanceName" [username]="getUsername()"
84 ></my-register-step-channel>
85
86 <div class="step-buttons">
45 <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button> 87 <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
88
89 <div class="skip-step">
90 <span class="underline-orange" role="button" (click)="skipChannelCreation()">
91 <strong i18n>I don't want to create a channel</strong>
92 </span>
93
94 <div class="skip-step-description" i18n>You will be able to create a channel later</div>
95 </div>
96
46 <button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()" i18n> 97 <button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()" i18n>
47 Create my account 98 Create my account
48 </button> 99 </button>
49 </cdk-step> 100 </div>
101 </cdk-step>
50 102
51 <cdk-step i18n-label label="Done" editable="false"> 103 <cdk-step #lastStep i18n-label label="Done!" [editable]="false">
52 <div *ngIf="!signupDone && !error" class="done-loader"> 104 <div *ngIf="!signupSuccess && !signupError" class="done-loader">
53 <my-loader [loading]="true"></my-loader> 105 <my-loader [loading]="true"></my-loader>
54 106
55 <div i18n>PeerTube is creating your account...</div> 107 <div i18n>PeerTube is creating your account...</div>
56 </div> 108 </div>
109
110 <div *ngIf="signupError" class="alert alert-danger">{{ signupError }}</div>
57 111
58 <div *ngIf="error" class="alert alert-danger">{{ error }}</div> 112 <my-signup-success *ngIf="signupSuccess" [requiresEmailVerification]="requiresEmailVerification"></my-signup-success>
59 </cdk-step> 113
60 </my-custom-stepper> 114 <div *ngIf="signupError" class="steps-button">
61 </div> 115 <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
116 </div>
117 </cdk-step>
118 </my-custom-stepper>
62 </div> 119 </div>
63 </ng-container> 120 </ng-container>
64 121
diff --git a/client/src/app/+signup/+register/register.component.scss b/client/src/app/+signup/+register/register.component.scss
index 4be67a858..c706d6955 100644
--- a/client/src/app/+signup/+register/register.component.scss
+++ b/client/src/app/+signup/+register/register.component.scss
@@ -1,86 +1,72 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.alert {
5 font-size: 15px;
6 text-align: center;
7}
8
9.signup-disabled { 4.signup-disabled {
10 padding-top: 30vh; 5 padding-top: 30vh;
11} 6}
12 7
13.wrapper { 8.title-page-v2 {
9 background-color: pvar(--mainColorVeryLight);
10 margin: 0;
11}
12
13my-instance-about-accordion {
14 display: block;
15 margin-bottom: 25px;
16}
17
18.step-buttons {
14 display: flex; 19 display: flex;
15 flex-direction: column; 20 flex-wrap: wrap;
21 align-items: center;
16 22
17 .register-form { 23 .skip-step {
18 max-width: 600px; 24 @include margin-right(30px);
19 align-self: center;
20 }
21 25
22 .register-form, 26 display: inline-block;
23 .instance-information {
24 width: 100%;
25 } 27 }
26 28
27 .instance-information { 29 .skip-step-description {
28 margin-bottom: 15px; 30 margin-top: 5px;
31 font-size: 14px;
29 } 32 }
30}
31 33
32.form-group-terms { 34 .underline-orange {
33 margin: 30px 0; 35 color: pvar(--mainForegroundColor);
34}
35 36
36.input-group { 37 &:hover {
37 @include peertube-input-group(100%); 38 opacity: 0.8;
38} 39 }
39 40 }
40.input-group-append {
41 height: 30px;
42}
43 41
44.form-group-terms { 42 button,
45 width: 100% !important; 43 .skip-step {
46} 44 margin-top: 20px;
45 margin-bottom: 20px;
46 }
47 47
48input:not([type=submit]) { 48 .skip-step,
49 @include peertube-input-text(100%); 49 button[cdkStepperNext] {
50 display: block; 50 @include margin-left(auto);
51 }
51 52
52 &#username, 53 .skip-step + button[cdkStepperNext] {
53 &#name { 54 @include margin-left(0);
54 width: auto !important;
55 flex-grow: 1;
56 } 55 }
57} 56}
58 57
59input[type=submit],
60button { 58button {
61 @include peertube-button; 59 @include peertube-button-big;
62 60
63 &[cdkStepperNext] { 61 &[cdkStepperNext] {
64 @include orange-button; 62 @include orange-button;
65
66 // Chrome does not support inline-end
67 float: right;
68 float: inline-end;
69 } 63 }
70 64
71 &[cdkStepperPrevious] { 65 &[cdkStepperPrevious] {
72 @include grey-button; 66 @include grey-button;
73
74 // Chrome does not support inline-start
75 float: left;
76 float: inline-start;
77 } 67 }
78} 68}
79 69
80.name-information {
81 margin-top: 10px;
82}
83
84.done-loader { 70.done-loader {
85 display: flex; 71 display: flex;
86 justify-content: center; 72 justify-content: center;
@@ -89,13 +75,16 @@ button {
89 75
90 my-loader { 76 my-loader {
91 margin-bottom: 20px; 77 margin-bottom: 20px;
78 }
79}
92 80
93 ::ng-deep .loader div { 81@media screen and (max-width: $small-view) {
94 border-color: pvar(--mainColor) transparent transparent transparent; 82 .step-buttons {
95 } 83 justify-content: space-between;
96 84
97 + div { 85 .skip-step,
98 font-size: 15px; 86 button[cdkStepperNext] {
87 @include margin-left(0);
99 } 88 }
100 } 89 }
101} 90}
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts
index b4a7c0d0e..396b27e5a 100644
--- a/client/src/app/+signup/+register/register.component.ts
+++ b/client/src/app/+signup/+register/register.component.ts
@@ -1,4 +1,5 @@
1import { Component, OnInit } from '@angular/core' 1import { CdkStep } from '@angular/cdk/stepper'
2import { Component, OnInit, ViewChild } from '@angular/core'
2import { FormGroup } from '@angular/forms' 3import { FormGroup } from '@angular/forms'
3import { ActivatedRoute } from '@angular/router' 4import { ActivatedRoute } from '@angular/router'
4import { AuthService } from '@app/core' 5import { AuthService } from '@app/core'
@@ -15,13 +16,15 @@ import { ServerConfig } from '@shared/models/server'
15 styleUrls: [ './register.component.scss' ] 16 styleUrls: [ './register.component.scss' ]
16}) 17})
17export class RegisterComponent implements OnInit { 18export class RegisterComponent implements OnInit {
19 @ViewChild('lastStep') lastStep: CdkStep
20
18 accordion: NgbAccordion 21 accordion: NgbAccordion
19 info: string = null 22
20 error: string = null 23 signupError: string
21 success: string = null 24 signupSuccess = false
22 signupDone = false
23 25
24 videoUploadDisabled: boolean 26 videoUploadDisabled: boolean
27 videoQuota: number
25 28
26 formStepTerms: FormGroup 29 formStepTerms: FormGroup
27 formStepUser: FormGroup 30 formStepUser: FormGroup
@@ -39,8 +42,8 @@ export class RegisterComponent implements OnInit {
39 moderation: false 42 moderation: false
40 } 43 }
41 44
42 defaultPreviousStepButtonLabel = $localize`:Button on the registration form to go to the previous step:Back` 45 defaultPreviousStepButtonLabel = $localize`:Button on the registration form to go to the previous step:Go to the previous step`
43 defaultNextStepButtonLabel = $localize`:Button on the registration form to go to the previous step:Next` 46 defaultNextStepButtonLabel = $localize`:Button on the registration form to go to the previous step:Go to the next step`
44 stepUserButtonLabel = this.defaultNextStepButtonLabel 47 stepUserButtonLabel = this.defaultNextStepButtonLabel
45 48
46 signupDisabled = false 49 signupDisabled = false
@@ -62,7 +65,11 @@ export class RegisterComponent implements OnInit {
62 return this.serverConfig.signup.minimumAge 65 return this.serverConfig.signup.minimumAge
63 } 66 }
64 67
65 ngOnInit (): void { 68 get instanceName () {
69 return this.serverConfig.instance.name
70 }
71
72 ngOnInit () {
66 this.serverConfig = this.route.snapshot.data.serverConfig 73 this.serverConfig = this.route.snapshot.data.serverConfig
67 74
68 if (this.serverConfig.signup.allowed === false || this.serverConfig.signup.allowedForCurrentIP === false) { 75 if (this.serverConfig.signup.allowed === false || this.serverConfig.signup.allowedForCurrentIP === false) {
@@ -70,7 +77,9 @@ export class RegisterComponent implements OnInit {
70 return 77 return
71 } 78 }
72 79
73 this.videoUploadDisabled = this.serverConfig.user.videoQuota === 0 80 this.videoQuota = this.serverConfig.user.videoQuota
81 this.videoUploadDisabled = this.videoQuota === 0
82
74 this.stepUserButtonLabel = this.videoUploadDisabled 83 this.stepUserButtonLabel = this.videoUploadDisabled
75 ? $localize`:Button on the registration form to finalize the account and channel creation:Signup` 84 ? $localize`:Button on the registration form to finalize the account and channel creation:Signup`
76 : this.defaultNextStepButtonLabel 85 : this.defaultNextStepButtonLabel
@@ -120,21 +129,31 @@ export class RegisterComponent implements OnInit {
120 this.aboutHtml = instanceAboutAccordion.aboutHtml 129 this.aboutHtml = instanceAboutAccordion.aboutHtml
121 } 130 }
122 131
132 skipChannelCreation () {
133 this.formStepChannel.reset()
134 this.lastStep.select()
135 this.signup()
136 }
137
123 async signup () { 138 async signup () {
124 this.error = null 139 this.signupError = undefined
125 140
126 const body: UserRegister = await this.hooks.wrapObject( 141 const body: UserRegister = await this.hooks.wrapObject(
127 Object.assign(this.formStepUser.value, { channel: this.videoUploadDisabled ? undefined : this.formStepChannel.value }), 142 {
143 ...this.formStepUser.value,
144
145 channel: this.formStepChannel?.value?.name
146 ? this.formStepChannel.value
147 : undefined
148 },
128 'signup', 149 'signup',
129 'filter:api.signup.registration.create.params' 150 'filter:api.signup.registration.create.params'
130 ) 151 )
131 152
132 this.userSignupService.signup(body).subscribe({ 153 this.userSignupService.signup(body).subscribe({
133 next: () => { 154 next: () => {
134 this.signupDone = true
135
136 if (this.requiresEmailVerification) { 155 if (this.requiresEmailVerification) {
137 this.info = $localize`Now please check your emails to verify your account and complete signup.` 156 this.signupSuccess = true
138 return 157 return
139 } 158 }
140 159
@@ -142,17 +161,17 @@ export class RegisterComponent implements OnInit {
142 this.authService.login(body.username, body.password) 161 this.authService.login(body.username, body.password)
143 .subscribe({ 162 .subscribe({
144 next: () => { 163 next: () => {
145 this.success = $localize`You are now logged in as ${body.username}!` 164 this.signupSuccess = true
146 }, 165 },
147 166
148 error: err => { 167 error: err => {
149 this.error = err.message 168 this.signupError = err.message
150 } 169 }
151 }) 170 })
152 }, 171 },
153 172
154 error: err => { 173 error: err => {
155 this.error = err.message 174 this.signupError = err.message
156 } 175 }
157 }) 176 })
158 } 177 }
diff --git a/client/src/app/+signup/+register/register.module.ts b/client/src/app/+signup/+register/register.module.ts
index 52cdb33bc..684aae2e9 100644
--- a/client/src/app/+signup/+register/register.module.ts
+++ b/client/src/app/+signup/+register/register.module.ts
@@ -2,15 +2,15 @@ import { CdkStepperModule } from '@angular/cdk/stepper'
2import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
3import { SharedSignupModule } from '@app/+signup/shared/shared-signup.module' 3import { SharedSignupModule } from '@app/+signup/shared/shared-signup.module'
4import { SharedInstanceModule } from '@app/shared/shared-instance' 4import { SharedInstanceModule } from '@app/shared/shared-instance'
5import { SharedMainModule } from '@app/shared/shared-main'
5import { CustomStepperComponent } from './custom-stepper.component' 6import { CustomStepperComponent } from './custom-stepper.component'
6import { RegisterRoutingModule } from './register-routing.module' 7import { RegisterRoutingModule } from './register-routing.module'
7import { RegisterStepChannelComponent } from './register-step-channel.component'
8import { RegisterStepTermsComponent } from './register-step-terms.component'
9import { RegisterStepUserComponent } from './register-step-user.component'
10import { RegisterComponent } from './register.component' 8import { RegisterComponent } from './register.component'
9import { RegisterStepAboutComponent, RegisterStepChannelComponent, RegisterStepTermsComponent, RegisterStepUserComponent } from './steps'
11 10
12@NgModule({ 11@NgModule({
13 imports: [ 12 imports: [
13 SharedMainModule,
14 RegisterRoutingModule, 14 RegisterRoutingModule,
15 15
16 CdkStepperModule, 16 CdkStepperModule,
@@ -25,7 +25,8 @@ import { RegisterComponent } from './register.component'
25 CustomStepperComponent, 25 CustomStepperComponent,
26 RegisterStepChannelComponent, 26 RegisterStepChannelComponent,
27 RegisterStepTermsComponent, 27 RegisterStepTermsComponent,
28 RegisterStepUserComponent 28 RegisterStepUserComponent,
29 RegisterStepAboutComponent
29 ], 30 ],
30 31
31 exports: [ 32 exports: [
diff --git a/client/src/app/+signup/+register/steps/index.ts b/client/src/app/+signup/+register/steps/index.ts
new file mode 100644
index 000000000..b5eae7468
--- /dev/null
+++ b/client/src/app/+signup/+register/steps/index.ts
@@ -0,0 +1,4 @@
1export * from './register-step-about.component'
2export * from './register-step-channel.component'
3export * from './register-step-terms.component'
4export * from './register-step-user.component'
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.html b/client/src/app/+signup/+register/steps/register-step-about.component.html
new file mode 100644
index 000000000..f93de8ce9
--- /dev/null
+++ b/client/src/app/+signup/+register/steps/register-step-about.component.html
@@ -0,0 +1,39 @@
1<div class="why">
2 <h3 i18n>Why creating an account?</h3>
3
4 <p i18n>
5 As you probably noticed: creating an account is not necessary to watch video son {{ instanceName }}.
6 <br />
7 However, creating an account on {{ instanceName }} will allow you to:
8 </p>
9
10 <ul>
11 <li i18n><strong>Comment</strong> videos</li>
12 <li i18n><strong>Subscribe</strong> to channels to be notified of new videos</li>
13 <li i18n>Have access to your <strong>watch history</strong></li>
14 <li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li>
15 </ul>
16</div>
17
18<div>
19 <h4 i18n>You're using Mastodon, ActivityPub or a RSS feed aggregator?</h4>
20
21 <p i18n>
22 You can already follow {{ instanceName }} using your favorite tool.
23 </p>
24</div>
25
26<div class="callout callout-orange callout-light">
27 <div class="mascot-container" style="min-width: 140px">
28 <img class="mascot" width="140px" height="160px" src="/client/assets/images/mascot/happy.svg" alt="mascot"/>
29 </div>
30
31 <div class="callout-content">
32 <h4 i18>This website is a GAFAM alternative</h4>
33
34 <p i18n>
35 {{ instanceName }} has been created using <a class="link-orange" target="_blank" rel="noopener noreferrer" href="https://joinpeertube.org">PeerTube</a>, a video creation platform developed by Framasoft.
36 <a class="link-orange" target="_blank" rel="noopener noreferrer" href="https://framasoft.org">Framasoft</a> is a french non-profit organization that offers alternatives to Big Tech's digital tools
37 </p>
38 </div>
39</div>
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.scss b/client/src/app/+signup/+register/steps/register-step-about.component.scss
new file mode 100644
index 000000000..ab6d6dd4d
--- /dev/null
+++ b/client/src/app/+signup/+register/steps/register-step-about.component.scss
@@ -0,0 +1,53 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4h3 {
5 font-weight: $font-bold;
6 font-size: 24px;
7}
8
9h4 {
10 font-size: 18px;
11 font-weight: $font-bold;
12}
13
14.why {
15 margin-bottom: 30px;
16}
17
18.callout {
19 margin: 75px auto 25px;
20 border-width: 2px;
21 display: flex;
22
23 .mascot-container {
24 position: relative;
25
26 .mascot {
27 position: absolute;
28 top: -65px;
29 }
30 }
31
32 .callout-content {
33 margin-left: 30px;
34
35 p {
36 margin: 0;
37 }
38 }
39}
40
41@media screen and (max-width: $small-view) {
42 .callout {
43 margin-top: 20px;
44
45 .mascot-container {
46 display: none;
47 }
48
49 .callout-content {
50 margin-left: 0;
51 }
52 }
53}
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.ts b/client/src/app/+signup/+register/steps/register-step-about.component.ts
new file mode 100644
index 000000000..9a0941016
--- /dev/null
+++ b/client/src/app/+signup/+register/steps/register-step-about.component.ts
@@ -0,0 +1,19 @@
1import { Component, Input } from '@angular/core'
2import { ServerService } from '@app/core'
3
4@Component({
5 selector: 'my-register-step-about',
6 templateUrl: './register-step-about.component.html',
7 styleUrls: [ './register-step-about.component.scss' ]
8})
9export class RegisterStepAboutComponent {
10 @Input() videoUploadDisabled: boolean
11
12 constructor (private serverService: ServerService) {
13
14 }
15
16 get instanceName () {
17 return this.serverService.getHTMLConfig().instance.name
18 }
19}
diff --git a/client/src/app/+signup/+register/steps/register-step-channel.component.html b/client/src/app/+signup/+register/steps/register-step-channel.component.html
new file mode 100644
index 000000000..c79256c68
--- /dev/null
+++ b/client/src/app/+signup/+register/steps/register-step-channel.component.html
@@ -0,0 +1,55 @@
1<div class="mb-5">
2 <p i18n>
3 You want to <strong>publish videos</strong> on {{ instanceName }}? Then you need to create your first <strong>channel</strong>.
4 </p>
5
6 <p i18n>
7 You might want to <strong>create a channel by theme:</strong> for example, you can create a channel named "SweetMelodies"
8 to publish your piano concerts and another one "Ecology" in which you publish your videos talking about ecology.
9 </p>
10
11 <p i18n *ngIf="videoQuota !== -1">
12 {{ instanceName }} administrators allow you to publish up to <strong>{{ videoQuota | bytes: 0 }} of videos</strong> on their website.
13 </p>
14</div>
15
16<form role="form" [formGroup]="form">
17
18 <div class="row">
19
20 <div class="col-md-12 col-xl-6 form-group">
21 <label for="displayName" i18n>Channel display name</label>
22
23 <div i18n class="form-group-description">This is the name that will be publicly visible by other users.</div>
24
25 <div class="input-group">
26 <input
27 type="text" id="displayName" i18n-placeholder placeholder="Example: Sweet Melodies"
28 formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
29 >
30 </div>
31
32 <div *ngIf="formErrors.displayName" class="form-error">{{ formErrors.displayName }}</div>
33 </div>
34
35 <div class="col-md-12 col-xl-6 form-group">
36 <label for="name" i18n>Channel identifier</label>
37
38 <div i18n class="form-group-description">This is the name that will be displayed in your profile URL.</div>
39
40 <div class="input-group">
41 <input
42 type="text" id="name" i18n-placeholder placeholder="Example: sweetmelodies24"
43 formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }"
44 >
45 <div class="input-group-text">@{{ instanceHost }}</div>
46 </div>
47
48 <div *ngIf="formErrors.name" class="form-error">{{ formErrors.name }}</div>
49
50 <div *ngIf="isSameThanUsername()" class="form-error" i18n>
51 Channel identifier cannot be the same as your account name. You can click on the first step to update your account name.
52 </div>
53 </div>
54 </div>
55</form>
diff --git a/client/src/app/+signup/+register/register-step-channel.component.ts b/client/src/app/+signup/+register/steps/register-step-channel.component.ts
index 1bc0ccfd3..c10b568ba 100644
--- a/client/src/app/+signup/+register/register-step-channel.component.ts
+++ b/client/src/app/+signup/+register/steps/register-step-channel.component.ts
@@ -9,10 +9,13 @@ import { UserSignupService } from '@app/shared/shared-users'
9@Component({ 9@Component({
10 selector: 'my-register-step-channel', 10 selector: 'my-register-step-channel',
11 templateUrl: './register-step-channel.component.html', 11 templateUrl: './register-step-channel.component.html',
12 styleUrls: [ './register.component.scss' ] 12 styleUrls: [ './step.component.scss' ]
13}) 13})
14export class RegisterStepChannelComponent extends FormReactive implements OnInit { 14export class RegisterStepChannelComponent extends FormReactive implements OnInit {
15 @Input() username: string 15 @Input() username: string
16 @Input() instanceName: string
17 @Input() videoQuota: number
18
16 @Output() formBuilt = new EventEmitter<FormGroup>() 19 @Output() formBuilt = new EventEmitter<FormGroup>()
17 20
18 constructor ( 21 constructor (
diff --git a/client/src/app/+signup/+register/register-step-terms.component.html b/client/src/app/+signup/+register/steps/register-step-terms.component.html
index 28a6e0021..f54ca77e2 100644
--- a/client/src/app/+signup/+register/register-step-terms.component.html
+++ b/client/src/app/+signup/+register/steps/register-step-terms.component.html
@@ -1,18 +1,16 @@
1<form role="form" [formGroup]="form"> 1<form role="form" [formGroup]="form">
2 <div class="form-group form-group-terms"> 2 <div class="form-group">
3 <my-peertube-checkbox inputName="terms" formControlName="terms"> 3 <my-peertube-checkbox inputName="terms" formControlName="terms">
4 <ng-template ptTemplate="label"> 4 <ng-template ptTemplate="label">
5 <ng-container i18n> 5 <ng-container i18n>
6 I am at least {{ minimumAge }} years old and agree 6 I am at least {{ minimumAge }} years old and agree
7 to the <a class="terms-anchor" (click)="onTermsClick($event)" href='#'>Terms</a> 7 to the <a class="link-orange" (click)="onTermsClick($event)" href='#'>Terms</a>
8 <ng-container *ngIf="hasCodeOfConduct"> and to the <a (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container> 8 <ng-container *ngIf="hasCodeOfConduct"> and to the <a (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container>
9 of this instance 9 of this instance
10 </ng-container> 10 </ng-container>
11 </ng-template> 11 </ng-template>
12 </my-peertube-checkbox> 12 </my-peertube-checkbox>
13 13
14 <div *ngIf="formErrors.terms" class="form-error"> 14 <div *ngIf="formErrors.terms" class="form-error">{{ formErrors.terms }}</div>
15 {{ formErrors.terms }}
16 </div>
17 </div> 15 </div>
18</form> 16</form>
diff --git a/client/src/app/+signup/+register/register-step-terms.component.ts b/client/src/app/+signup/+register/steps/register-step-terms.component.ts
index 20c1ae1c4..87d16696e 100644
--- a/client/src/app/+signup/+register/register-step-terms.component.ts
+++ b/client/src/app/+signup/+register/steps/register-step-terms.component.ts
@@ -8,7 +8,7 @@ import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
8@Component({ 8@Component({
9 selector: 'my-register-step-terms', 9 selector: 'my-register-step-terms',
10 templateUrl: './register-step-terms.component.html', 10 templateUrl: './register-step-terms.component.html',
11 styleUrls: [ './register.component.scss' ] 11 styleUrls: [ './step.component.scss' ]
12}) 12})
13export class RegisterStepTermsComponent extends FormReactive implements OnInit { 13export class RegisterStepTermsComponent extends FormReactive implements OnInit {
14 @Input() hasCodeOfConduct = false 14 @Input() hasCodeOfConduct = false
diff --git a/client/src/app/+signup/+register/steps/register-step-user.component.html b/client/src/app/+signup/+register/steps/register-step-user.component.html
new file mode 100644
index 000000000..3e0d35006
--- /dev/null
+++ b/client/src/app/+signup/+register/steps/register-step-user.component.html
@@ -0,0 +1,71 @@
1<div class="alert pt-alert-primary" i18n *ngIf="videoUploadDisabled">
2 Video uploads are disabled on this instance, hence your account won't be able to upload videos.
3</div>
4
5<form role="form" [formGroup]="form">
6 <div class="row">
7
8 <div class="col-md-12 col-xl-6 form-group">
9 <label for="displayName" i18n>Public name</label>
10
11 <div class="form-group-description" i18n>
12 This is the name that will be publicly visible by other users.
13 </div>
14
15 <div class="input-group">
16 <input
17 type="text" id="displayName" i18n-placeholder placeholder="Example: John Doe"
18 formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
19 >
20 </div>
21
22 <div *ngIf="formErrors.displayName" class="form-error">{{ formErrors.displayName }}</div>
23 </div>
24
25 <div class="col-md-12 col-xl-6 form-group">
26 <label for="username" i18n>Username</label>
27
28 <div class="form-group-description" i18n>
29 This is the name that will be displayed in your profile URL.
30 </div>
31
32 <div class="input-group">
33 <input
34 type="text" id="username" i18n-placeholder placeholder="Example: john_doe58"
35 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }"
36 >
37 <span class="input-group-text">@{{ instanceHost }}</span>
38 </div>
39
40 <div *ngIf="formErrors.username" class="form-error">{{ formErrors.username }}</div>
41 </div>
42 </div>
43
44 <div class="row">
45 <div class="col-md-12 col-xl-6 form-group">
46 <label for="email" i18n>Email</label>
47
48 <div *ngIf="requiresEmailVerification" class="form-group-description" i18n>
49 This email address will be used to validate your account.
50 </div>
51
52 <input
53 type="text" id="email" i18n-placeholder placeholder="Example: john@example.com"
54 formControlName="email" class="form-control" [ngClass]="{ 'input-error': formErrors['email'] }"
55 >
56
57 <div *ngIf="formErrors.email" class="form-error">{{ formErrors.email }}</div>
58 </div>
59
60 <div class="col-md-12 col-xl-6 form-group">
61 <label for="password" i18n>Password</label>
62
63 <div class="form-group-description">{{ getMinPasswordLengthMessage() }}</div>
64
65 <my-input-text
66 formControlName="password" inputId="password"
67 [formError]="formErrors['password']" autocomplete="new-password"
68 ></my-input-text>
69 </div>
70 </div>
71</form>
diff --git a/client/src/app/+signup/+register/register-step-user.component.ts b/client/src/app/+signup/+register/steps/register-step-user.component.ts
index 92ddfca2e..b89e38a28 100644
--- a/client/src/app/+signup/+register/register-step-user.component.ts
+++ b/client/src/app/+signup/+register/steps/register-step-user.component.ts
@@ -14,10 +14,11 @@ import { UserSignupService } from '@app/shared/shared-users'
14@Component({ 14@Component({
15 selector: 'my-register-step-user', 15 selector: 'my-register-step-user',
16 templateUrl: './register-step-user.component.html', 16 templateUrl: './register-step-user.component.html',
17 styleUrls: [ './register.component.scss' ] 17 styleUrls: [ './step.component.scss' ]
18}) 18})
19export class RegisterStepUserComponent extends FormReactive implements OnInit { 19export class RegisterStepUserComponent extends FormReactive implements OnInit {
20 @Input() videoUploadDisabled = false 20 @Input() videoUploadDisabled = false
21 @Input() requiresEmailVerification = false
21 22
22 @Output() formBuilt = new EventEmitter<FormGroup>() 23 @Output() formBuilt = new EventEmitter<FormGroup>()
23 24
@@ -49,6 +50,10 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
49 .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue)) 50 .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue))
50 } 51 }
51 52
53 getMinPasswordLengthMessage () {
54 return USER_PASSWORD_VALIDATOR.MESSAGES.minlength
55 }
56
52 private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { 57 private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
53 const username = this.form.value['username'] || '' 58 const username = this.form.value['username'] || ''
54 59
diff --git a/client/src/app/+signup/+register/steps/step.component.scss b/client/src/app/+signup/+register/steps/step.component.scss
new file mode 100644
index 000000000..35cfdae91
--- /dev/null
+++ b/client/src/app/+signup/+register/steps/step.component.scss
@@ -0,0 +1,27 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4input:not([type=submit]) {
5 @include peertube-input-text(100%);
6 display: block;
7
8 &#username,
9 &#name {
10 width: auto !important;
11 flex-grow: 1;
12 }
13}
14
15input[type=submit],
16button {
17 @include peertube-button;
18}
19
20label {
21 font-size: 18px;
22 margin-bottom: 5px;
23}
24
25.row {
26 margin-bottom: 30px;
27}
diff --git a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html
index ece9d1022..023082f61 100644
--- a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html
+++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html
@@ -1,21 +1,21 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <div i18n class="title-page title-page-single"> 2 <h1 i18n class="title-page">Request email for account verification</h1>
3 Request email for account verification
4 </div>
5 3
6 <form *ngIf="requiresEmailVerification; else emailVerificationNotRequired" role="form" (ngSubmit)="askSendVerifyEmail()" [formGroup]="form"> 4 <form *ngIf="requiresEmailVerification; else emailVerificationNotRequired" role="form" (ngSubmit)="askSendVerifyEmail()" [formGroup]="form">
7 <div class="form-group"> 5 <div class="form-group">
8 <label i18n for="verify-email-email">Email</label> 6 <label i18n for="verify-email-email">Email</label>
7
9 <input 8 <input
10 type="email" id="verify-email-email" i18n-placeholder placeholder="Email address" required 9 type="email" id="verify-email-email" i18n-placeholder placeholder="Email address" required
11 formControlName="verify-email-email" class="form-control" [ngClass]="{ 'input-error': formErrors['verify-email-email'] }" 10 formControlName="verify-email-email" class="form-control" [ngClass]="{ 'input-error': formErrors['verify-email-email'] }"
12 > 11 >
13 <div *ngIf="formErrors['verify-email-email']" class="form-error"> 12
14 {{ formErrors['verify-email-email'] }} 13 <div *ngIf="formErrors['verify-email-email']" class="form-error">{{ formErrors['verify-email-email'] }}</div>
15 </div>
16 </div> 14 </div>
17 <input type="submit" i18n-value value="Send verification email" [disabled]="!form.valid"> 15
16 <input class="peertube-button orange-button" type="submit" i18n-value value="Send verification email" [disabled]="!form.valid">
18 </form> 17 </form>
18
19 <ng-template #emailVerificationNotRequired> 19 <ng-template #emailVerificationNotRequired>
20 <div i18n>This instance does not require email verification.</div> 20 <div i18n>This instance does not require email verification.</div>
21 </ng-template> 21 </ng-template>
diff --git a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss
index 5494e29c6..c542f3d8a 100644
--- a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss
+++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss
@@ -1,12 +1,8 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4input:not([type=submit]) { 4input[type=email] {
5 @include peertube-input-text(340px); 5 @include peertube-input-text(340px);
6 display: block;
7}
8 6
9input[type=submit] { 7 display: block;
10 @include peertube-button;
11 @include orange-button;
12} 8}
diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html
index 47519c943..122f3c28c 100644
--- a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html
+++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html
@@ -1,18 +1,14 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <div i18n class="title-page title-page-single"> 2 <h1 i18n class="title-page">Verify account email confirmation</h1>
3 Verify account email confirmation
4 </div>
5 3
6 <my-signup-success i18n *ngIf="!isPendingEmail && success" message="Your email has been verified and you may now login."> 4 <my-signup-success i18n *ngIf="!isPendingEmail && success" [requiresEmailVerification]="false">
7 </my-signup-success> 5 </my-signup-success>
8 6
9 <div i18n class="alert alert-success" *ngIf="isPendingEmail && success"> 7 <div i18n class="alert alert-success" *ngIf="isPendingEmail && success">Email updated.</div>
10 Email updated.
11 </div>
12 8
13 <div *ngIf="failed"> 9 <div class="alert alert-danger" *ngIf="failed">
14 <span i18n>An error occurred.</span> 10 <span i18n>An error occurred.</span>
15 11
16 <a i18n routerLink="/verify-account/ask-send-email" [queryParams]="{ isPendingEmail: isPendingEmail }">Request new verification email.</a> 12 <a i18n class="ms-1 link-orange" routerLink="/verify-account/ask-send-email" [queryParams]="{ isPendingEmail: isPendingEmail }">Request new verification email</a>
17 </div> 13 </div>
18</div> 14</div>
diff --git a/client/src/app/+signup/+verify-account/verify-account-routing.module.ts b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts
index 1bc636345..d53a5ed78 100644
--- a/client/src/app/+signup/+verify-account/verify-account-routing.module.ts
+++ b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts
@@ -21,7 +21,7 @@ const verifyAccountRoutes: Routes = [
21 component: VerifyAccountAskSendEmailComponent, 21 component: VerifyAccountAskSendEmailComponent,
22 data: { 22 data: {
23 meta: { 23 meta: {
24 title: $localize`Ask to send an email to verify you account` 24 title: $localize`Ask to send an email to verify your account`
25 } 25 }
26 } 26 }
27 } 27 }
diff --git a/client/src/app/+signup/shared/shared-signup.module.ts b/client/src/app/+signup/shared/shared-signup.module.ts
index f8b224c71..0aa08f3e2 100644
--- a/client/src/app/+signup/shared/shared-signup.module.ts
+++ b/client/src/app/+signup/shared/shared-signup.module.ts
@@ -3,6 +3,8 @@ import { 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 { SharedUsersModule } from '@app/shared/shared-users' 5import { SharedUsersModule } from '@app/shared/shared-users'
6import { SignupMascotComponent } from './signup-mascot.component'
7import { SignupStepTitleComponent } from './signup-step-title.component'
6import { SignupSuccessComponent } from './signup-success.component' 8import { SignupSuccessComponent } from './signup-success.component'
7 9
8@NgModule({ 10@NgModule({
@@ -14,7 +16,9 @@ import { SignupSuccessComponent } from './signup-success.component'
14 ], 16 ],
15 17
16 declarations: [ 18 declarations: [
17 SignupSuccessComponent 19 SignupSuccessComponent,
20 SignupStepTitleComponent,
21 SignupMascotComponent
18 ], 22 ],
19 23
20 exports: [ 24 exports: [
@@ -22,7 +26,9 @@ import { SignupSuccessComponent } from './signup-success.component'
22 SharedFormModule, 26 SharedFormModule,
23 SharedGlobalIconModule, 27 SharedGlobalIconModule,
24 28
25 SignupSuccessComponent 29 SignupSuccessComponent,
30 SignupStepTitleComponent,
31 SignupMascotComponent
26 ], 32 ],
27 33
28 providers: [ 34 providers: [
diff --git a/client/src/app/+signup/shared/signup-mascot.component.scss b/client/src/app/+signup/shared/signup-mascot.component.scss
new file mode 100644
index 000000000..5eebfb014
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-mascot.component.scss
@@ -0,0 +1,11 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4.root {
5 display: inline-block;
6 width: 270px;
7}
8
9div ::ng-deep svg {
10 color: pvar(--mainColor);
11}
diff --git a/client/src/app/+signup/shared/signup-mascot.component.ts b/client/src/app/+signup/shared/signup-mascot.component.ts
new file mode 100644
index 000000000..a96ccffee
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-mascot.component.ts
@@ -0,0 +1,29 @@
1import { Component, Input } from '@angular/core'
2import { DomSanitizer } from '@angular/platform-browser'
3
4const images = {
5 about: require('!!raw-loader?!../../../assets/images/mascot/register/about.svg').default,
6 terms: require('!!raw-loader?!../../../assets/images/mascot/register/terms.svg').default,
7 success: require('!!raw-loader?!../../../assets/images/mascot/register/success.svg').default,
8 channel: require('!!raw-loader?!../../../assets/images/mascot/register/channel.svg').default,
9 account: require('!!raw-loader?!../../../assets/images/mascot/register/account.svg').default
10}
11
12export type MascotImageName = keyof typeof images
13
14@Component({
15 selector: 'my-signup-mascot',
16 styleUrls: [ './signup-mascot.component.scss' ],
17 template: `<div class="root" [innerHTML]="html"></div>`
18})
19export class SignupMascotComponent {
20 @Input() imageName: MascotImageName
21
22 constructor (private sanitize: DomSanitizer) {
23
24 }
25
26 get html () {
27 return this.sanitize.bypassSecurityTrustHtml(images[this.imageName])
28 }
29}
diff --git a/client/src/app/+signup/shared/signup-step-title.component.html b/client/src/app/+signup/shared/signup-step-title.component.html
new file mode 100644
index 000000000..9cf4c4826
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-step-title.component.html
@@ -0,0 +1,9 @@
1<div class="step-content-title">
2 <my-signup-mascot [imageName]="mascotImageName"></my-signup-mascot>
3
4 <h2>
5 <ng-content></ng-content>
6 </h2>
7
8 <div class="step-content-title-separator"></div>
9</div>
diff --git a/client/src/app/+signup/shared/signup-step-title.component.scss b/client/src/app/+signup/shared/signup-step-title.component.scss
new file mode 100644
index 000000000..1e0cb2440
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-step-title.component.scss
@@ -0,0 +1,23 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4.step-content-title {
5 text-align: center;
6 margin: auto;
7 margin-bottom: 45px;
8
9 h2 {
10 font-size: 32px;
11 font-weight: normal;
12 max-width: 300px;
13 margin: 15px auto 0;
14 }
15}
16
17.step-content-title-separator {
18 height: 6px;
19 width: 60px;
20 border-radius: 4px;
21 background-color: pvar(--mainColor);
22 margin: 5px auto 0;
23}
diff --git a/client/src/app/+signup/shared/signup-step-title.component.ts b/client/src/app/+signup/shared/signup-step-title.component.ts
new file mode 100644
index 000000000..9664eb7f3
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-step-title.component.ts
@@ -0,0 +1,12 @@
1import { Component, Input } from '@angular/core'
2import { MascotImageName } from './signup-mascot.component'
3
4@Component({
5 selector: 'my-signup-step-title',
6 templateUrl: './signup-step-title.component.html',
7 styleUrls: [ './signup-step-title.component.scss' ]
8})
9export class SignupStepTitleComponent {
10 @Input() mascotImageName: MascotImageName
11
12}
diff --git a/client/src/app/+signup/shared/signup-success.component.html b/client/src/app/+signup/shared/signup-success.component.html
index d66e8b568..c14889c72 100644
--- a/client/src/app/+signup/shared/signup-success.component.html
+++ b/client/src/app/+signup/shared/signup-success.component.html
@@ -1,20 +1,22 @@
1<!-- Thanks: Amit Singh Sansoya from https://codepen.io/amit3200/pen/zWMJOO --> 1<my-signup-step-title mascotImageName="success" i18n>
2 <strong>Welcome</strong>
3 <div>on {{ instanceName }}</div>
4</my-signup-step-title>
2 5
3<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130.2 130.2"> 6<div class="alert pt-alert-primary">
4 <circle class="path circle" fill="none" stroke="#73AF55" stroke-width="6" stroke-miterlimit="10" cx="65.1" cy="65.1" r="62.1"/> 7 <p i18n>Your account has been created!</p>
5 <polyline class="path check" fill="none" stroke="#73AF55" stroke-width="6" stroke-linecap="round" stroke-miterlimit="10" points="100.2,40.2 51.5,88.8 29.8,67.5 "/>
6</svg>
7 8
8<p i18n class="bottom-message">Welcome to PeerTube!</p> 9 <p i18n *ngIf="requiresEmailVerification">
9 10 <strong>Check your emails</strong> to validate your account and complete your inscription.
10<div *ngIf="message" class="alert alert-success">
11 <p>{{ message }}</p>
12
13 <p i18n>
14 If you need help to use PeerTube, you can have a look at the <a href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>.
15 </p> 11 </p>
16 12
17 <p i18n> 13 <ng-container *ngIf="!requiresEmailVerification">
18 To help moderators and other users to know <strong>who you are</strong>, don't forget to <a routerLink="/my-account/settings">set up your account profile</a> by adding an <strong>avatar</strong> and a <strong>description</strong>. 14 <p i18n>
19 </p> 15 If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>.
16 </p>
17
18 <p i18n>
19 To help moderators and other users to know <strong>who you are</strong>, don't forget to <a class="link-orange" routerLink="/my-account/settings">set up your account profile</a> by adding an <strong>avatar</strong> and a <strong>description</strong>.
20 </p>
21 </ng-container>
20</div> 22</div>
diff --git a/client/src/app/+signup/shared/signup-success.component.scss b/client/src/app/+signup/shared/signup-success.component.scss
index b302366e2..918349ba0 100644
--- a/client/src/app/+signup/shared/signup-success.component.scss
+++ b/client/src/app/+signup/shared/signup-success.component.scss
@@ -1,54 +1,6 @@
1svg {
2 width: 100px;
3 display: block;
4 margin: 40px auto 0;
5}
6
7.path {
8 stroke-dasharray: 1000;
9 stroke-dashoffset: 0;
10
11 &.circle {
12 animation: dash .9s ease-in-out;
13 }
14
15 &.line {
16 stroke-dashoffset: 1000;
17 animation: dash .9s .35s ease-in-out forwards;
18 }
19
20 &.check {
21 stroke-dashoffset: -100;
22 animation: dash-check .9s .35s ease-in-out forwards;
23 }
24}
25
26.bottom-message {
27 text-align: center;
28 margin: 20px 0 60px;
29 font-size: 1.25em;
30 color: #73AF55;
31}
32
33.alert { 1.alert {
34 font-size: 15px; 2 font-size: 18px;
3 max-width: 900px;
35 text-align: center; 4 text-align: center;
36} 5 margin: auto;
37
38@keyframes dash {
39 0% {
40 stroke-dashoffset: 1000;
41 }
42 100% {
43 stroke-dashoffset: 0;
44 }
45}
46
47@keyframes dash-check {
48 0% {
49 stroke-dashoffset: -100;
50 }
51 100% {
52 stroke-dashoffset: 900;
53 }
54} 6}
diff --git a/client/src/app/+signup/shared/signup-success.component.ts b/client/src/app/+signup/shared/signup-success.component.ts
index 19fb5922a..a03f3819d 100644
--- a/client/src/app/+signup/shared/signup-success.component.ts
+++ b/client/src/app/+signup/shared/signup-success.component.ts
@@ -1,4 +1,5 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { ServerService } from '@app/core'
2 3
3@Component({ 4@Component({
4 selector: 'my-signup-success', 5 selector: 'my-signup-success',
@@ -6,5 +7,13 @@ import { Component, Input } from '@angular/core'
6 styleUrls: [ './signup-success.component.scss' ] 7 styleUrls: [ './signup-success.component.scss' ]
7}) 8})
8export class SignupSuccessComponent { 9export class SignupSuccessComponent {
9 @Input() message: string 10 @Input() requiresEmailVerification: boolean
11
12 constructor (private serverService: ServerService) {
13
14 }
15
16 get instanceName () {
17 return this.serverService.getHTMLConfig().instance.name
18 }
10} 19}
diff --git a/client/src/app/+stats/video/video-stats.component.html b/client/src/app/+stats/video/video-stats.component.html
index adfe095a3..242a5a7a2 100644
--- a/client/src/app/+stats/video/video-stats.component.html
+++ b/client/src/app/+stats/video/video-stats.component.html
@@ -1,5 +1,5 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <h1 class="title-page title-page-single" i18n>{{ video.name }}</h1> 2 <h1 class="title-page" i18n>{{ video.name }}</h1>
3 3
4 <div class="stats-embed"> 4 <div class="stats-embed">
5 <div class="global-stats"> 5 <div class="global-stats">
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 fa58963ce..dd0b10d66 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,7 +1,7 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <div i18n class="title-page title-page-single" *ngIf="pagination.totalItems"> 2 <h1 i18n class="title-page" *ngIf="pagination.totalItems">
3 Created {pagination.totalItems, plural, =1 {1 playlist} other {{{ pagination.totalItems }} playlists}} 3 Created {pagination.totalItems, plural, =1 {1 playlist} other {{{ pagination.totalItems }} playlists}}
4 </div> 4 </h1>
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
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index 212e2f867..d92aa072d 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -23,7 +23,7 @@
23 <div class="section-label" i18n>OWNER ACCOUNT</div> 23 <div class="section-label" i18n>OWNER ACCOUNT</div>
24 24
25 <div class="avatar-row"> 25 <div class="avatar-row">
26 <my-actor-avatar class="account-avatar" [account]="ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar> 26 <my-actor-avatar class="account-avatar" [actor]="ownerAccount" actorType="account" [internalHref]="getAccountUrl()"></my-actor-avatar>
27 27
28 <div class="actor-info"> 28 <div class="actor-info">
29 <h4> 29 <h4>
@@ -51,7 +51,7 @@
51 </ng-template> 51 </ng-template>
52 52
53 <div class="channel-avatar-row"> 53 <div class="channel-avatar-row">
54 <my-actor-avatar class="main-avatar" [channel]="videoChannel"></my-actor-avatar> 54 <my-actor-avatar class="main-avatar" [actor]="videoChannel" actorType="channel"></my-actor-avatar>
55 55
56 <div> 56 <div>
57 <div class="section-label" i18n>VIDEO CHANNEL</div> 57 <div class="section-label" i18n>VIDEO CHANNEL</div>
@@ -67,15 +67,15 @@
67 <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()" 67 <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()"
68 class="btn btn-outline-secondary btn-sm copy-button" title="Copy channel handle" i18n-title 68 class="btn btn-outline-secondary btn-sm copy-button" title="Copy channel handle" i18n-title
69 > 69 >
70 <span class="glyphicon glyphicon-duplicate"></span> 70 <my-global-icon iconName="copy"></my-global-icon>
71 </button> 71 </button>
72 </div> 72 </div>
73 73
74 <div class="actor-counters"> 74 <div class="actor-counters">
75 <span i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span> 75 <span i18n>{videoChannel.followersCount, plural, =0 {No subscribers} =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span>
76 76
77 <span class="videos-count" *ngIf="channelVideosCount !== undefined" i18n> 77 <span class="videos-count" *ngIf="channelVideosCount !== undefined" i18n>
78 {channelVideosCount, plural, =1 {1 videos} other {{{ channelVideosCount }} videos}} 78 {channelVideosCount, plural, =0 {No videos} =1 {1 video} other {{{ channelVideosCount }} videos}}
79 </span> 79 </span>
80 </div> 80 </div>
81 </div> 81 </div>
@@ -113,7 +113,7 @@
113 113
114 <div class="links"> 114 <div class="links">
115 <ng-template #linkTemplate let-item="item"> 115 <ng-template #linkTemplate let-item="item">
116 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> 116 <a [routerLink]="item.routerLink" routerLinkActive="active" class="sub-menu-entry">{{ item.label }}</a>
117 </ng-template> 117 </ng-template>
118 118
119 <my-list-overflow [items]="links" [itemTemplate]="linkTemplate"></my-list-overflow> 119 <my-list-overflow [items]="links" [itemTemplate]="linkTemplate"></my-list-overflow>
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss
index c00dacae5..945d9a13d 100644
--- a/client/src/app/+video-channels/video-channels.component.scss
+++ b/client/src/app/+video-channels/video-channels.component.scss
@@ -150,7 +150,13 @@
150} 150}
151 151
152.copy-button { 152.copy-button {
153 @include margin-left(3px);
154
153 border: 0; 155 border: 0;
156
157 my-global-icon {
158 width: 15px;
159 }
154} 160}
155 161
156@media screen and (max-width: 1400px) { 162@media screen and (max-width: 1400px) {
diff --git a/client/src/app/+video-studio/edit/video-studio-edit.component.html b/client/src/app/+video-studio/edit/video-studio-edit.component.html
index acfc1a452..fe74062d2 100644
--- a/client/src/app/+video-studio/edit/video-studio-edit.component.html
+++ b/client/src/app/+video-studio/edit/video-studio-edit.component.html
@@ -1,5 +1,5 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <h1 class="title-page title-page-single" i18n>Studio for {{ video.name }}</h1> 2 <h1 class="title-page" i18n>Studio for {{ video.name }}</h1>
3 3
4 <div class="columns"> 4 <div class="columns">
5 <form role="form" [formGroup]="form"> 5 <form role="form" [formGroup]="form">
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss
index 4ce2c6758..c0b670c65 100644
--- a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss
+++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss
@@ -1,11 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4label {
5 font-weight: $font-regular;
6 font-size: 100%;
7}
8
9.caption-file { 4.caption-file {
10 margin-top: 20px; 5 margin-top: 20px;
11 width: max-content; 6 width: max-content;
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
index 44004eb21..650448a74 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
@@ -51,11 +51,10 @@
51 </ng-template> 51 </ng-template>
52 </my-help> 52 </my-help>
53 53
54 <my-markdown-textarea [truncate]="250" formControlName="description" [markdownVideo]="videoToUpdate"></my-markdown-textarea> 54 <my-markdown-textarea
55 55 formControlName="description" [markdownVideo]="videoToUpdate"
56 <div *ngIf="formErrors.description" class="form-error"> 56 [formError]="formErrors.description" [truncate]="250"
57 {{ formErrors.description }} 57 ></my-markdown-textarea>
58 </div>
59 </div> 58 </div>
60 </div> 59 </div>
61 60
@@ -237,23 +236,23 @@
237 <ng-template ngbNavContent> 236 <ng-template ngbNavContent>
238 <div class="row live-settings"> 237 <div class="row live-settings">
239 <div class="col-md-12"> 238 <div class="col-md-12">
240 <div class="alert alert-info"> 239 <div class="alert pt-alert-primary">
241 <my-live-documentation-link></my-live-documentation-link> 240 <my-live-documentation-link></my-live-documentation-link>
242 </div> 241 </div>
243 242
244 <div *ngIf="liveVideo.rtmpUrl" class="form-group"> 243 <div *ngIf="liveVideo.rtmpUrl" class="form-group">
245 <label for="liveVideoRTMPUrl" i18n>Live RTMP Url</label> 244 <label for="liveVideoRTMPUrl" i18n>Live RTMP Url</label>
246 <my-input-toggle-hidden inputId="liveVideoRTMPUrl" [value]="liveVideo.rtmpUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-toggle-hidden> 245 <my-input-text inputId="liveVideoRTMPUrl" [value]="liveVideo.rtmpUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-text>
247 </div> 246 </div>
248 247
249 <div *ngIf="liveVideo.rtmpsUrl" class="form-group"> 248 <div *ngIf="liveVideo.rtmpsUrl" class="form-group">
250 <label for="liveVideoRTMPSUrl" i18n>Live RTMPS Url</label> 249 <label for="liveVideoRTMPSUrl" i18n>Live RTMPS Url</label>
251 <my-input-toggle-hidden inputId="liveVideoRTMPSUrl" [value]="liveVideo.rtmpsUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-toggle-hidden> 250 <my-input-text inputId="liveVideoRTMPSUrl" [value]="liveVideo.rtmpsUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-text>
252 </div> 251 </div>
253 252
254 <div class="form-group"> 253 <div class="form-group">
255 <label for="liveVideoStreamKey" i18n>Live stream key</label> 254 <label for="liveVideoStreamKey" i18n>Live stream key</label>
256 <my-input-toggle-hidden inputId="liveVideoStreamKey" [value]="liveVideo.streamKey" [withCopy]="true" [readonly]="true"></my-input-toggle-hidden> 255 <my-input-text inputId="liveVideoStreamKey" [value]="liveVideo.streamKey" [withCopy]="true" [readonly]="true"></my-input-text>
257 256
258 <div class="form-group-description" i18n>⚠️ Never share your stream key with anyone.</div> 257 <div class="form-group-description" i18n>⚠️ Never share your stream key with anyone.</div>
259 </div> 258 </div>
@@ -332,17 +331,30 @@
332 </ng-container> 331 </ng-container>
333 </ng-template> 332 </ng-template>
334 </my-help> 333 </my-help>
334
335 <my-markdown-textarea 335 <my-markdown-textarea
336 id="support" formControlName="support" markdownType="enhanced" 336 id="support" formControlName="support" markdownType="enhanced"
337 [classes]="{ 'input-error': formErrors['support'] }" 337 [formError]="formErrors['support']"
338 ></my-markdown-textarea> 338 ></my-markdown-textarea>
339 <div *ngIf="formErrors.support" class="form-error">
340 {{ formErrors.support }}
341 </div>
342 </div> 339 </div>
343 </div> 340 </div>
344 341
345 <div class="col-md-12 col-xl-4"> 342 <div class="col-md-12 col-xl-4">
343
344 <div *ngIf="videoSource" class="form-group">
345 <label i18n for="filename">Filename</label>
346
347 <my-help>
348 <ng-template ptTemplate="preHtml">
349 <ng-container i18n>
350 Name of the uploaded file
351 </ng-container>
352 </ng-template>
353 </my-help>
354
355 <input type="text" [disabled]="true" id="filename" class="form-control" [value]="videoSource.filename" />
356 </div>
357
346 <div class="form-group originally-published-at"> 358 <div class="form-group originally-published-at">
347 <label i18n for="originallyPublishedAt">Original publication date</label> 359 <label i18n for="originallyPublishedAt">Original publication date</label>
348 <my-help> 360 <my-help>
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
index 5344e5431..e8a6c6e42 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
@@ -1,25 +1,10 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4label,
5my-dynamic-form-field ::ng-deep label {
6 font-weight: $font-regular;
7 font-size: 100%;
8}
9
10.peertube-select-container { 4.peertube-select-container {
11 @include peertube-select-container(auto); 5 @include peertube-select-container(auto);
12} 6}
13 7
14.title-page a {
15 color: pvar(--mainForegroundColor);
16
17 &:hover {
18 text-decoration: none;
19 opacity: .8;
20 }
21}
22
23my-peertube-checkbox { 8my-peertube-checkbox {
24 display: block; 9 display: block;
25 margin-bottom: 1rem; 10 margin-bottom: 1rem;
@@ -33,22 +18,10 @@ my-peertube-checkbox {
33 height: 100%; 18 height: 100%;
34 min-height: 300px; 19 min-height: 300px;
35 20
36 .form-group {
37 margin-bottom: 25px;
38 }
39
40 input { 21 input {
41 @include peertube-input-text(100%); 22 @include peertube-input-text(100%);
42 display: block; 23 display: block;
43 } 24 }
44
45 .label-tags + span {
46 font-size: 15px;
47 }
48
49 .advanced-settings .form-group {
50 margin-bottom: 20px;
51 }
52} 25}
53 26
54.captions-header { 27.captions-header {
@@ -79,7 +52,6 @@ my-peertube-checkbox {
79 .caption-entry-label { 52 .caption-entry-label {
80 @include margin-right(20px); 53 @include margin-right(20px);
81 54
82 font-size: 15px;
83 font-weight: bold; 55 font-weight: bold;
84 width: 150px; 56 width: 150px;
85 } 57 }
@@ -108,7 +80,6 @@ my-peertube-checkbox {
108 80
109.no-caption { 81.no-caption {
110 text-align: center; 82 text-align: center;
111 font-size: 15px;
112} 83}
113 84
114.submit-container { 85.submit-container {
@@ -119,7 +90,6 @@ my-peertube-checkbox {
119 90
120 display: inline-block; 91 display: inline-block;
121 color: pvar(--greyForegroundColor); 92 color: pvar(--greyForegroundColor);
122 font-size: 15px;
123 } 93 }
124} 94}
125 95
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
index 16b964482..c74ef5731 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
@@ -37,6 +37,7 @@ import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
37import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 37import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
38import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component' 38import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
39import { VideoEditType } from './video-edit.type' 39import { VideoEditType } from './video-edit.type'
40import { VideoSource } from '@shared/models/videos/video-source'
40 41
41type VideoLanguages = VideoConstant<string> & { group?: string } 42type VideoLanguages = VideoConstant<string> & { group?: string }
42type PluginField = { 43type PluginField = {
@@ -61,6 +62,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
61 @Input() forbidScheduledPublication = true 62 @Input() forbidScheduledPublication = true
62 63
63 @Input() videoCaptions: VideoCaptionWithPathEdit[] = [] 64 @Input() videoCaptions: VideoCaptionWithPathEdit[] = []
65 @Input() videoSource: VideoSource
64 66
65 @Input() waitTranscodingEnabled = true 67 @Input() waitTranscodingEnabled = true
66 @Input() type: VideoEditType 68 @Input() type: VideoEditType
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html
index ecec6045b..f537b939f 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html
@@ -44,7 +44,7 @@
44 {{ error }} 44 {{ error }}
45</div> 45</div>
46 46
47<div class="alert alert-info" i18n *ngIf="isInUpdateForm && getMaxLiveDuration() >= 0"> 47<div class="alert pt-alert-primary" i18n *ngIf="isInUpdateForm && getMaxLiveDuration() >= 0">
48 Max live duration is {{ getMaxLiveDuration() | myDurationFormatter }}. 48 Max live duration is {{ getMaxLiveDuration() | myDurationFormatter }}.
49 If your live reaches this limit, it will be automatically terminated. 49 If your live reaches this limit, it will be automatically terminated.
50</div> 50</div>
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
index 14a7a287a..aa34d644a 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -52,7 +52,7 @@
52 {{ error }} 52 {{ error }}
53</div> 53</div>
54 54
55<div *ngIf="hasImportedVideo && !error" class="alert alert-info" i18n> 55<div *ngIf="hasImportedVideo && !error" class="alert pt-alert-primary" i18n>
56 Congratulations, the video will be imported with BitTorrent! You can already add information about this video. 56 Congratulations, the video will be imported with BitTorrent! You can already add information about this video.
57</div> 57</div>
58 58
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
index 60a43c870..67e1cb418 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
@@ -45,7 +45,7 @@
45 {{ error }} 45 {{ error }}
46</div> 46</div>
47 47
48<div *ngIf="!error && hasImportedVideo" class="alert alert-info" i18n> 48<div *ngIf="!error && hasImportedVideo" class="alert pt-alert-primary" i18n>
49 Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video. 49 Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
50</div> 50</div>
51 51
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 0c78669c1..4c74eda84 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
@@ -78,7 +78,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterV
78 .pipe( 78 .pipe(
79 switchMap(res => { 79 switchMap(res => {
80 return this.videoCaptionService 80 return this.videoCaptionService
81 .listCaptions(res.video.id) 81 .listCaptions(res.video.uuid)
82 .pipe( 82 .pipe(
83 map(result => ({ video: res.video, videoCaptions: result.data })) 83 map(result => ({ video: res.video, videoCaptions: result.data }))
84 ) 84 )
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
index 860fb76fa..728884986 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
@@ -87,7 +87,7 @@
87 {{ error }} 87 {{ error }}
88</div> 88</div>
89 89
90<div *ngIf="videoUploaded && !error" class="alert alert-info" i18n> 90<div *ngIf="videoUploaded && !error" class="alert pt-alert-primary" i18n>
91 Congratulations! Your video is now available in your private library. 91 Congratulations! Your video is now available in your private library.
92</div> 92</div>
93 93
diff --git a/client/src/app/+videos/+video-edit/video-add.component.html b/client/src/app/+videos/+video-edit/video-add.component.html
index 29cf08e75..27ad14d63 100644
--- a/client/src/app/+videos/+video-edit/video-add.component.html
+++ b/client/src/app/+videos/+video-edit/video-add.component.html
@@ -40,7 +40,7 @@
40<div *ngIf="!user.isUploadDisabled()" class="margin-content"> 40<div *ngIf="!user.isUploadDisabled()" class="margin-content">
41 <my-user-quota *ngIf="!isInSecondStep() || secondStepType === 'go-live'" [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-quota> 41 <my-user-quota *ngIf="!isInSecondStep() || secondStepType === 'go-live'" [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-quota>
42 42
43 <div class="title-page title-page-single" *ngIf="isInSecondStep()"> 43 <div class="title-page" *ngIf="isInSecondStep()">
44 <ng-container *ngIf="secondStepType === 'import-url' || secondStepType === 'import-torrent'" i18n>Import {{ videoName }}</ng-container> 44 <ng-container *ngIf="secondStepType === 'import-url' || secondStepType === 'import-torrent'" i18n>Import {{ videoName }}</ng-container>
45 <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container> 45 <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container>
46 </div> 46 </div>
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 dda868789..461a38204 100644
--- a/client/src/app/+videos/+video-edit/video-add.component.scss
+++ b/client/src/app/+videos/+video-edit/video-add.component.scss
@@ -10,7 +10,6 @@ $nav-link-height: 40px;
10.upload-message { 10.upload-message {
11 width: 100%; 11 width: 100%;
12 text-align: center; 12 text-align: center;
13 font-size: 15px;
14 margin-bottom: 0; 13 margin-bottom: 0;
15 border-radius: 0; 14 border-radius: 0;
16 15
diff --git a/client/src/app/+videos/+video-edit/video-update.component.html b/client/src/app/+videos/+video-edit/video-update.component.html
index 4376f6fe4..ffd125695 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.html
+++ b/client/src/app/+videos/+video-edit/video-update.component.html
@@ -1,6 +1,6 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <div class="title-page title-page-single"> 2 <div class="title-page">
3 <span class="mr-1" i18n>Update</span> 3 <span class="me-1" i18n>Update</span>
4 <a [routerLink]="getVideoUrl()">{{ video?.name }}</a> 4 <a [routerLink]="getVideoUrl()">{{ video?.name }}</a>
5 </div> 5 </div>
6 6
@@ -12,6 +12,7 @@
12 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="isWaitTranscodingEnabled()" 12 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="isWaitTranscodingEnabled()"
13 type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()" 13 type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
14 [liveVideo]="liveVideo" [videoToUpdate]="videoDetails" 14 [liveVideo]="liveVideo" [videoToUpdate]="videoDetails"
15 [videoSource]="videoSource"
15 16
16 (formBuilt)="onFormBuilt()" 17 (formBuilt)="onFormBuilt()"
17 ></my-video-edit> 18 ></my-video-edit>
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts
index 9c4998f2e..43e8ba3e5 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.ts
+++ b/client/src/app/+videos/+video-edit/video-update.component.ts
@@ -10,6 +10,7 @@ import { LiveVideoService } from '@app/shared/shared-video-live'
10import { LoadingBarService } from '@ngx-loading-bar/core' 10import { LoadingBarService } from '@ngx-loading-bar/core'
11import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models' 11import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models'
12import { hydrateFormFromVideo } from './shared/video-edit-utils' 12import { hydrateFormFromVideo } from './shared/video-edit-utils'
13import { VideoSource } from '@shared/models/videos/video-source'
13 14
14@Component({ 15@Component({
15 selector: 'my-videos-update', 16 selector: 'my-videos-update',
@@ -19,6 +20,7 @@ import { hydrateFormFromVideo } from './shared/video-edit-utils'
19export class VideoUpdateComponent extends FormReactive implements OnInit { 20export class VideoUpdateComponent extends FormReactive implements OnInit {
20 video: VideoEdit 21 video: VideoEdit
21 videoDetails: VideoDetails 22 videoDetails: VideoDetails
23 videoSource: VideoSource
22 userVideoChannels: SelectChannelItem[] = [] 24 userVideoChannels: SelectChannelItem[] = []
23 videoCaptions: VideoCaptionEdit[] = [] 25 videoCaptions: VideoCaptionEdit[] = []
24 liveVideo: LiveVideo 26 liveVideo: LiveVideo
@@ -46,13 +48,14 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
46 this.buildForm({}) 48 this.buildForm({})
47 49
48 const { videoData } = this.route.snapshot.data 50 const { videoData } = this.route.snapshot.data
49 const { video, videoChannels, videoCaptions, liveVideo } = videoData 51 const { video, videoChannels, videoCaptions, videoSource, liveVideo } = videoData
50 52
51 this.video = new VideoEdit(video) 53 this.video = new VideoEdit(video)
52 this.videoDetails = video 54 this.videoDetails = video
53 55
54 this.userVideoChannels = videoChannels 56 this.userVideoChannels = videoChannels
55 this.videoCaptions = videoCaptions 57 this.videoCaptions = videoCaptions
58 this.videoSource = videoSource
56 this.liveVideo = liveVideo 59 this.liveVideo = liveVideo
57 60
58 this.forbidScheduledPublication = this.video.privacy !== VideoPrivacy.PRIVATE 61 this.forbidScheduledPublication = this.video.privacy !== VideoPrivacy.PRIVATE
diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts
index 82dae5c1c..524ceae10 100644
--- a/client/src/app/+videos/+video-edit/video-update.resolver.ts
+++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts
@@ -23,7 +23,8 @@ export class VideoUpdateResolver implements Resolve<any> {
23 return this.videoService.getVideo({ videoId: uuid }) 23 return this.videoService.getVideo({ videoId: uuid })
24 .pipe( 24 .pipe(
25 switchMap(video => forkJoin(this.buildVideoObservables(video))), 25 switchMap(video => forkJoin(this.buildVideoObservables(video))),
26 map(([ video, videoChannels, videoCaptions, liveVideo ]) => ({ video, videoChannels, videoCaptions, liveVideo })) 26 map(([ video, videoSource, videoChannels, videoCaptions, liveVideo ]) =>
27 ({ video, videoChannels, videoCaptions, videoSource, liveVideo }))
27 ) 28 )
28 } 29 }
29 30
@@ -33,10 +34,12 @@ export class VideoUpdateResolver implements Resolve<any> {
33 .loadCompleteDescription(video.descriptionPath) 34 .loadCompleteDescription(video.descriptionPath)
34 .pipe(map(description => Object.assign(video, { description }))), 35 .pipe(map(description => Object.assign(video, { description }))),
35 36
37 this.videoService.getSource(video.id),
38
36 listUserChannelsForSelect(this.authService), 39 listUserChannelsForSelect(this.authService),
37 40
38 this.videoCaptionService 41 this.videoCaptionService
39 .listCaptions(video.id) 42 .listCaptions(video.uuid)
40 .pipe( 43 .pipe(
41 map(result => result.data) 44 map(result => result.data)
42 ), 45 ),
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html
index f23efca98..cf32e371a 100644
--- a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html
+++ b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.html
@@ -1,5 +1,5 @@
1<div class="video-actions-rates"> 1<div class="video-actions-rates">
2 <div class="video-actions full-width justify-content-end"> 2 <div class="video-actions justify-content-end">
3 <my-video-rate 3 <my-video-rate
4 [video]="video" [isUserLoggedIn]="isUserLoggedIn" 4 [video]="video" [isUserLoggedIn]="isUserLoggedIn"
5 (rateUpdated)="onRateUpdated($event)" (userRatingLoaded)="onRateUpdated($event)" 5 (rateUpdated)="onRateUpdated($event)" (userRatingLoaded)="onRateUpdated($event)"
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.scss b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.scss
index fdf4e3edb..786d10f73 100644
--- a/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.scss
+++ b/client/src/app/+videos/+video-watch/shared/action-buttons/action-buttons.component.scss
@@ -5,6 +5,9 @@
5 height: 40px; // Align with the title 5 height: 40px; // Align with the title
6 display: flex; 6 display: flex;
7 align-items: center; 7 align-items: center;
8 width: 100%;
9 margin: 0 auto;
10 max-width: initial;
8 11
9 .action-button:not(:first-child), 12 .action-button:not(:first-child),
10 .action-dropdown, 13 .action-dropdown,
diff --git a/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts b/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts
index 48d48f33f..0fef246b3 100644
--- a/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts
+++ b/client/src/app/+videos/+video-watch/shared/action-buttons/video-rate.component.ts
@@ -89,7 +89,7 @@ export class VideoRateComponent implements OnInit, OnChanges, OnDestroy {
89 // Unlogged users do not have ratings 89 // Unlogged users do not have ratings
90 if (this.isUserLoggedIn === false) return 90 if (this.isUserLoggedIn === false) return
91 91
92 this.videoService.getUserVideoRating(this.video.id) 92 this.videoService.getUserVideoRating(this.video.uuid)
93 .subscribe({ 93 .subscribe({
94 next: ratingObject => { 94 next: ratingObject => {
95 if (!ratingObject) return 95 if (!ratingObject) return
@@ -103,13 +103,13 @@ export class VideoRateComponent implements OnInit, OnChanges, OnDestroy {
103 } 103 }
104 104
105 private setRating (nextRating: UserVideoRateType) { 105 private setRating (nextRating: UserVideoRateType) {
106 const ratingMethods: { [id in UserVideoRateType]: (id: number) => Observable<any> } = { 106 const ratingMethods: { [id in UserVideoRateType]: (id: string) => Observable<any> } = {
107 like: this.videoService.setVideoLike, 107 like: this.videoService.setVideoLike,
108 dislike: this.videoService.setVideoDislike, 108 dislike: this.videoService.setVideoDislike,
109 none: this.videoService.unsetVideoLike 109 none: this.videoService.unsetVideoLike
110 } 110 }
111 111
112 ratingMethods[nextRating].call(this.videoService, this.video.id) 112 ratingMethods[nextRating].call(this.videoService, this.video.uuid)
113 .subscribe({ 113 .subscribe({
114 next: () => { 114 next: () => {
115 // Update the video like attribute 115 // Update the video like attribute
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html
index 3ee818c8b..6bc201f32 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.html
@@ -1,8 +1,8 @@
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 <my-actor-avatar [account]="user?.account" size="25"></my-actor-avatar> 3 <my-actor-avatar [actor]="user?.account" [actorType]="getAvatarActorType()" size="25"></my-actor-avatar>
4 4
5 <div class="form-group"> 5 <div class="textarea-wrapper">
6 <textarea i18n-placeholder placeholder="Add comment..." myAutoResize 6 <textarea i18n-placeholder placeholder="Add comment..." myAutoResize
7 [readonly]="(user === null) ? true : false" 7 [readonly]="(user === null) ? true : false"
8 (click)="openVisitorModal($event)" 8 (click)="openVisitorModal($event)"
@@ -88,8 +88,8 @@
88 </div> 88 </div>
89 <div class="modal-body"> 89 <div class="modal-body">
90 <div class="emoji-flex"> 90 <div class="emoji-flex">
91 <div class="emoji-flex-item" *ngFor="let emojiMarkup of emojiMarkupList"> 91 <div class="emoji-flex-item" *ngFor="let emojiMarkup of getEmojiMarkupList()">
92 {{ emojiMarkup[0] }} <code>:{{ emojiMarkup[1] }}:</code> 92 {{ emojiMarkup.emoji }} <code>:{{ emojiMarkup.name }}:</code>
93 </div> 93 </div>
94 </div> 94 </div>
95 </div> 95 </div>
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.scss b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.scss
index ae889dd38..023d625e9 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.scss
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.scss
@@ -17,9 +17,8 @@ form {
17 @include margin-right(10px); 17 @include margin-right(10px);
18 } 18 }
19 19
20 .form-group { 20 .textarea-wrapper {
21 flex-grow: 1; 21 flex-grow: 1;
22 margin: 0;
23 position: relative; 22 position: relative;
24 } 23 }
25 24
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts
index 85da83a4c..fd3614297 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts
@@ -45,6 +45,8 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
45 addingComment = false 45 addingComment = false
46 addingCommentButtonValue: string 46 addingCommentButtonValue: string
47 47
48 private emojiMarkupList: { emoji: string, name: string }[]
49
48 constructor ( 50 constructor (
49 protected formValidatorService: FormValidatorService, 51 protected formValidatorService: FormValidatorService,
50 private notifier: Notifier, 52 private notifier: Notifier,
@@ -56,21 +58,6 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
56 super() 58 super()
57 } 59 }
58 60
59 get emojiMarkupList () {
60 const emojiMarkupObjectList = require('markdown-it-emoji/lib/data/light.json')
61
62 // Populate emoji-markup-list from object to array to avoid keys alphabetical order
63 const emojiMarkupArrayList = []
64 for (const emojiMarkupName in emojiMarkupObjectList) {
65 if (emojiMarkupName) {
66 const emoji = emojiMarkupObjectList[emojiMarkupName]
67 emojiMarkupArrayList.push([ emoji, emojiMarkupName ])
68 }
69 }
70
71 return emojiMarkupArrayList
72 }
73
74 ngOnInit () { 61 ngOnInit () {
75 this.buildForm({ 62 this.buildForm({
76 text: VIDEO_COMMENT_TEXT_VALIDATOR 63 text: VIDEO_COMMENT_TEXT_VALIDATOR
@@ -96,6 +83,20 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
96 } 83 }
97 } 84 }
98 85
86 getEmojiMarkupList () {
87 if (this.emojiMarkupList) return this.emojiMarkupList
88
89 const emojiMarkupObjectList = require('markdown-it-emoji/lib/data/light.json')
90
91 this.emojiMarkupList = []
92 for (const name of Object.keys(emojiMarkupObjectList)) {
93 const emoji = emojiMarkupObjectList[name]
94 this.emojiMarkupList.push({ emoji, name })
95 }
96
97 return this.emojiMarkupList
98 }
99
99 onValidKey () { 100 onValidKey () {
100 this.forceCheck() 101 this.forceCheck()
101 if (!this.form.valid) return 102 if (!this.form.valid) return
@@ -174,14 +175,20 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
174 return getLocaleDirection(this.localeId) === 'rtl' 175 return getLocaleDirection(this.localeId) === 'rtl'
175 } 176 }
176 177
178 getAvatarActorType () {
179 if (this.user) return 'account'
180
181 return 'unlogged'
182 }
183
177 private addCommentReply (commentCreate: VideoCommentCreate) { 184 private addCommentReply (commentCreate: VideoCommentCreate) {
178 return this.videoCommentService 185 return this.videoCommentService
179 .addCommentReply(this.video.id, this.parentComment.id, commentCreate) 186 .addCommentReply(this.video.uuid, this.parentComment.id, commentCreate)
180 } 187 }
181 188
182 private addCommentThread (commentCreate: VideoCommentCreate) { 189 private addCommentThread (commentCreate: VideoCommentCreate) {
183 return this.videoCommentService 190 return this.videoCommentService
184 .addCommentThread(this.video.id, commentCreate) 191 .addCommentThread(this.video.uuid, commentCreate)
185 } 192 }
186 193
187 private initTextValue () { 194 private initTextValue () {
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html
index 5014b9692..da35a9a2e 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html
@@ -1,6 +1,10 @@
1<div *ngIf="isCommentDisplayed()" class="root-comment" [ngClass]="{ 'is-child': isChild() }"> 1<div *ngIf="isCommentDisplayed()" class="root-comment" [ngClass]="{ 'is-child': isChild() }">
2 <div class="left"> 2 <div class="left">
3 <my-actor-avatar *ngIf="!comment.isDeleted" [href]="comment.account.url" [account]="comment.account" [size]="isChild() ? '25' : '36'"></my-actor-avatar> 3 <my-actor-avatar
4 *ngIf="!comment.isDeleted" [href]="comment.account.url"
5 [actor]="comment.account" actorType="account" [size]="isChild() ? '25' : '36'"
6 ></my-actor-avatar>
7
4 <div class="vertical-border"></div> 8 <div class="vertical-border"></div>
5 </div> 9 </div>
6 10
@@ -16,7 +20,7 @@
16 {{ comment.account.displayName }} 20 {{ comment.account.displayName }}
17 </span> 21 </span>
18 22
19 <span class="comment-account-fid ml-1">{{ comment.by }}</span> 23 <span class="comment-account-fid ms-1">{{ comment.by }}</span>
20 </a> 24 </a>
21 </div> 25 </div>
22 26
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss
index 54f828014..8b5034083 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss
@@ -2,7 +2,6 @@
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.root-comment { 4.root-comment {
5 font-size: 15px;
6 display: flex; 5 display: flex;
7 6
8 .left { 7 .left {
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html
index 0e00c9c0e..e27942e66 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html
@@ -1,12 +1,12 @@
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">
4 {totalNotDeletedComments, plural, =0 {Comments} =1 {1 Comment} other {{{totalNotDeletedComments}} Comments}} 4 {totalNotDeletedComments, plural, =0 {Comments} =1 {1 Comment} other {{{totalNotDeletedComments}} Comments}}
5 </h2> 5 </h2>
6 6
7 <my-feed [syndicationItems]="syndicationItems"></my-feed> 7 <my-feed [syndicationItems]="syndicationItems"></my-feed>
8 8
9 <div ngbDropdown class="d-inline-block ml-4 dropdown-root"> 9 <div ngbDropdown class="d-inline-block ms-4 dropdown-root">
10 <button class="btn btn-sm btn-outline-secondary" id="dropdown-sort-comments" ngbDropdownToggle i18n> 10 <button class="btn btn-sm btn-outline-secondary" id="dropdown-sort-comments" ngbDropdownToggle i18n>
11 SORT BY 11 SORT BY
12 </button> 12 </button>
@@ -65,7 +65,7 @@
65 [redraftValue]="commentReplyRedraftValue" 65 [redraftValue]="commentReplyRedraftValue"
66 > 66 >
67 <div *ngIf="comment.totalReplies !== 0 && !threadComments[comment.id]" (click)="viewReplies(comment.id)" class="view-replies mb-2"> 67 <div *ngIf="comment.totalReplies !== 0 && !threadComments[comment.id]" (click)="viewReplies(comment.id)" class="view-replies mb-2">
68 <span class="glyphicon glyphicon-menu-down"></span> 68 <span class="chevron-down"></span>
69 69
70 <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container> 70 <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container>
71 71
@@ -80,7 +80,7 @@
80 80
81 <ng-template i18n #noAuthorComments>View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}}</ng-template> 81 <ng-template i18n #noAuthorComments>View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}}</ng-template>
82 82
83 <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader> 83 <my-loader size="sm" class="ms-1" [loading]="threadLoading[comment.id]"></my-loader>
84 </div> 84 </div>
85 </my-video-comment> 85 </my-video-comment>
86 86
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.scss b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.scss
index 31aa73937..638147dfe 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.scss
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.scss
@@ -7,18 +7,9 @@
7 7
8.view-replies { 8.view-replies {
9 font-weight: $font-semibold; 9 font-weight: $font-semibold;
10 font-size: 15px;
11 cursor: pointer; 10 cursor: pointer;
12} 11}
13 12
14.glyphicon,
15.comment-thread-loading {
16 @include margin-right(5px);
17
18 display: inline-block;
19 font-size: 13px;
20}
21
22.title-block { 13.title-block {
23 .title-page { 14 .title-page {
24 @include margin-right(0); 15 @include margin-right(0);
@@ -41,10 +32,9 @@
41} 32}
42 33
43#dropdown-sort-comments { 34#dropdown-sort-comments {
44 font-weight: 600; 35 font-weight: $font-semibold;
45 text-transform: uppercase; 36 text-transform: uppercase;
46 border: 0; 37 border: 0;
47 transform: translateY(-7%);
48} 38}
49 39
50@media screen and (max-width: 600px) { 40@media screen and (max-width: 600px) {
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts
index 17e0af3bc..8e556c58f 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts
@@ -78,7 +78,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
78 this.threadLoading[commentId] = true 78 this.threadLoading[commentId] = true
79 79
80 const params = { 80 const params = {
81 videoId: this.video.id, 81 videoId: this.video.uuid,
82 threadId: commentId 82 threadId: commentId
83 } 83 }
84 84
@@ -110,7 +110,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
110 110
111 loadMoreThreads () { 111 loadMoreThreads () {
112 const params = { 112 const params = {
113 videoId: this.video.id, 113 videoId: this.video.uuid,
114 componentPagination: this.componentPagination, 114 componentPagination: this.componentPagination,
115 sort: this.sort 115 sort: this.sort
116 } 116 }
diff --git a/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html b/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html
index d579aaddb..b64d45564 100644
--- a/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html
+++ b/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html
@@ -1,6 +1,6 @@
1<div class="privacy-concerns" *ngIf="display"> 1<div class="privacy-concerns" *ngIf="display">
2 <div class="privacy-concerns-text"> 2 <div class="privacy-concerns-text">
3 <span class="mr-2"> 3 <span class="me-2">
4 <strong i18n>Friendly Reminder: </strong> 4 <strong i18n>Friendly Reminder: </strong>
5 <ng-container i18n> 5 <ng-container i18n>
6 the sharing system used for this video implies that some technical information about your system (such as a public IP address) can be sent to other peers. 6 the sharing system used for this video implies that some technical information about your system (such as a public IP address) can be sent to other peers.
diff --git a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html
index be726c990..79b83811d 100644
--- a/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html
+++ b/client/src/app/+videos/+video-watch/shared/information/video-alert.component.html
@@ -22,15 +22,15 @@
22 The video is being moved to an external server, it may not work properly. 22 The video is being moved to an external server, it may not work properly.
23</div> 23</div>
24 24
25<div i18n class="alert alert-info" *ngIf="hasVideoScheduledPublication()"> 25<div i18n class="alert pt-alert-primary" *ngIf="hasVideoScheduledPublication()">
26 This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}. 26 This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}.
27</div> 27</div>
28 28
29<div i18n class="alert alert-info" *ngIf="isWaitingForLive()"> 29<div i18n class="alert pt-alert-primary" *ngIf="isWaitingForLive()">
30 This live has not started yet. 30 This live has not started yet.
31</div> 31</div>
32 32
33<div i18n class="alert alert-info" *ngIf="isLiveEnded()"> 33<div i18n class="alert pt-alert-primary" *ngIf="isLiveEnded()">
34 This live has ended. 34 This live has ended.
35</div> 35</div>
36 36
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html b/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html
index 10ff46595..52ad1999d 100644
--- a/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html
@@ -11,9 +11,11 @@
11 >{{ video.originInstanceHost }}</a> 11 >{{ video.originInstanceHost }}</a>
12 12
13 <a 13 <a
14 i18n-title title="Open the video on the origin instance" class="glyphicon glyphicon-new-window" 14 i18n-title title="Open the video on the origin instance"
15 target="_blank" rel="noopener noreferrer" [href]="video.url" 15 target="_blank" rel="noopener noreferrer" [href]="video.url"
16 ></a> 16 >
17 <my-global-icon iconName="external-link"></my-global-icon>
18 </a>
17</div> 19</div>
18 20
19<div *ngIf="!!video.originallyPublishedAt" class="attribute attribute-originally-published-at"> 21<div *ngIf="!!video.originallyPublishedAt" class="attribute attribute-originally-published-at">
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.scss b/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.scss
index 26bead124..1470a9f6d 100644
--- a/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.scss
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.scss
@@ -33,12 +33,6 @@ a.attribute-value {
33 } 33 }
34} 34}
35 35
36.glyphicon-new-window {
37 color: pvar(--inputPlaceholderColor);
38 margin-left: 5px;
39 font-size: 12px;
40}
41
42@media screen and (max-width: 1600px) { 36@media screen and (max-width: 1600px) {
43 .attributes .attribute { 37 .attributes .attribute {
44 margin-bottom: 5px; 38 margin-bottom: 5px;
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html
index a23152b67..a608a22f6 100644
--- a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html
@@ -2,7 +2,7 @@
2 <my-actor-avatar 2 <my-actor-avatar
3 *ngIf="showChannel" 3 *ngIf="showChannel"
4 class="channel" 4 class="channel"
5 [channel]="video.channel" 5 [actor]="video.channel" actorType="channel"
6 [internalHref]="[ '/c', video.byVideoChannel ]" 6 [internalHref]="[ '/c', video.byVideoChannel ]"
7 [title]="channelLinkTitle" 7 [title]="channelLinkTitle"
8 size="35" 8 size="35"
@@ -12,7 +12,7 @@
12 *ngIf="showAccount" 12 *ngIf="showAccount"
13 class="account" 13 class="account"
14 [class.second-avatar]="showChannel" 14 [class.second-avatar]="showChannel"
15 [account]="video.account" 15 [actor]="video.account" actorType="account"
16 [internalHref]="[ '/a', video.byAccount ]" 16 [internalHref]="[ '/a', video.byAccount ]"
17 [title]="accountLinkTitle" 17 [title]="accountLinkTitle"
18 size="35"> 18 size="35">
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss
index 80711ff32..fd9dd1a6a 100644
--- a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss
@@ -1,14 +1,5 @@
1@use '_mixins' as *; 1@use '_mixins' as *;
2 2
3@mixin secondary {
4 height: 60%;
5 width: 60%;
6 position: absolute;
7 bottom: -5px;
8 right: -5px;
9 background-color: rgba(0, 0, 0, 0);
10}
11
12.wrapper { 3.wrapper {
13 @include margin-right(5px); 4 @include margin-right(5px);
14 5
@@ -16,6 +7,11 @@
16 margin-bottom: 5px; 7 margin-bottom: 5px;
17 8
18 .second-avatar { 9 .second-avatar {
19 @include secondary(); 10 height: 60%;
11 width: 60%;
12 position: absolute;
13 bottom: -5px;
14 right: -5px;
15 background-color: rgba(0, 0, 0, 0);
20 } 16 }
21} 17}
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html
index 2cfaad8f6..fa4dbb3ca 100644
--- a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html
@@ -8,12 +8,12 @@
8 8
9 <div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description?.length >= 250" (click)="showMoreDescription()"> 9 <div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description?.length >= 250" (click)="showMoreDescription()">
10 <ng-container i18n>Show more</ng-container> 10 <ng-container i18n>Show more</ng-container>
11 <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span> 11 <span *ngIf="descriptionLoading === false" class="chevron-down"></span>
12 <my-small-loader class="description-loading" [loading]="descriptionLoading"></my-small-loader> 12 <my-loader size="sm" class="description-loading" [loading]="descriptionLoading"></my-loader>
13 </div> 13 </div>
14 14
15 <div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-info-description-more"> 15 <div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-info-description-more">
16 <ng-container i18n>Show less</ng-container> 16 <ng-container i18n>Show less</ng-container>
17 <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span> 17 <span *ngIf="descriptionLoading === false" class="chevron-up"></span>
18 </div> 18 </div>
19</div> 19</div>
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss
index fc8b4574c..b503a94cb 100644
--- a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss
@@ -7,7 +7,6 @@
7 7
8 margin-top: 20px; 8 margin-top: 20px;
9 margin-bottom: 20px; 9 margin-bottom: 20px;
10 font-size: 15px;
11 10
12 .video-info-description-html { 11 .video-info-description-html {
13 @include peertube-word-wrap; 12 @include peertube-word-wrap;
@@ -17,13 +16,8 @@
17 } 16 }
18 } 17 }
19 18
20 .glyphicon,
21 .description-loading { 19 .description-loading {
22 @include margin-left(3px); 20 @include margin-left(5px);
23 }
24
25 .description-loading {
26 display: inline-block;
27 } 21 }
28 22
29 .video-info-description-more { 23 .video-info-description-more {
@@ -31,11 +25,6 @@
31 font-weight: $font-semibold; 25 font-weight: $font-semibold;
32 color: pvar(--greyForegroundColor); 26 color: pvar(--greyForegroundColor);
33 font-size: 14px; 27 font-size: 14px;
34
35 .glyphicon {
36 position: relative;
37 top: 2px;
38 }
39 } 28 }
40} 29}
41 30
diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html
index f5dd352a3..b04bd3548 100644
--- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html
+++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html
@@ -6,9 +6,9 @@
6 <div class="playlist-display-name"> 6 <div class="playlist-display-name">
7 {{ playlist.displayName }} 7 {{ playlist.displayName }}
8 8
9 <span *ngIf="isUnlistedPlaylist()" class="badge badge-warning" i18n>Unlisted</span> 9 <span *ngIf="isUnlistedPlaylist()" class="pt-badge badge-warning" i18n>Unlisted</span>
10 <span *ngIf="isPrivatePlaylist()" class="badge badge-danger" i18n>Private</span> 10 <span *ngIf="isPrivatePlaylist()" class="pt-badge badge-danger" i18n>Private</span>
11 <span *ngIf="isPublicPlaylist()" class="badge badge-info" i18n>Public</span> 11 <span *ngIf="isPublicPlaylist()" class="pt-badge badge-info" i18n>Public</span>
12 </div> 12 </div>
13 13
14 <div class="playlist-by-index"> 14 <div class="playlist-by-index">
diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss
index 5c3453e4b..0f0ac1979 100644
--- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss
+++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.scss
@@ -15,43 +15,47 @@
15 .playlist-info { 15 .playlist-info {
16 padding: 5px 30px; 16 padding: 5px 30px;
17 background-color: pvar(--greyBackgroundColor); 17 background-color: pvar(--greyBackgroundColor);
18 }
19
20 .playlist-display-name {
21 font-size: 18px;
22 font-weight: $font-semibold;
23 margin-bottom: 5px;
18 24
19 .playlist-display-name { 25 .pt-badge {
20 font-size: 18px; 26 @include margin-left(5px);
21 font-weight: $font-semibold;
22 margin-bottom: 5px;
23 } 27 }
28 }
24 29
25 .playlist-by-index { 30 .playlist-by-index {
26 color: pvar(--greyForegroundColor); 31 color: pvar(--greyForegroundColor);
27 display: flex; 32 display: flex;
28 33
29 .playlist-by { 34 .playlist-by {
30 @include margin-right(5px); 35 @include margin-right(5px);
31 } 36 }
32 37
33 .playlist-index span:first-child::after { 38 .playlist-index span:first-child::after {
34 content: '/'; 39 content: '/';
35 margin: 0 3px; 40 margin: 0 3px;
36 }
37 } 41 }
42 }
38 43
39 .playlist-controls { 44 .playlist-controls {
40 display: flex; 45 display: flex;
41 margin: 10px 0; 46 margin: 10px 0;
42 47
43 my-global-icon:not(:last-child) { 48 my-global-icon:not(:last-child) {
44 @include margin-right(.5rem); 49 @include margin-right(.5rem);
45 } 50 }
46 51
47 my-global-icon { 52 my-global-icon {
48 &:not(.active) { 53 &:not(.active) {
49 opacity: .5; 54 opacity: .5;
50 } 55 }
51 56
52 ::ng-deep { 57 ::ng-deep {
53 cursor: pointer; 58 cursor: pointer;
54 }
55 } 59 }
56 } 60 }
57 } 61 }
diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.html b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.html
index e493ad8d7..5ac801622 100644
--- a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.html
+++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.html
@@ -1,9 +1,8 @@
1<div class="other-videos" [ngClass]="{ 'display-as-row': displayAsRow }"> 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">Other videos</h2>
5 Other videos 5
6 </h2>
7 <div *ngIf="!playlist" class="title-page-autoplay" 6 <div *ngIf="!playlist" class="title-page-autoplay"
8 [ngbTooltip]="autoPlayNextVideoTooltip" placement="bottom-right auto" 7 [ngbTooltip]="autoPlayNextVideoTooltip" placement="bottom-right auto"
9 > 8 >
diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss
index 784d0e961..5b1bf9cab 100644
--- a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss
+++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss
@@ -8,18 +8,14 @@
8 margin-bottom: 25px; 8 margin-bottom: 25px;
9 flex-wrap: wrap-reverse; 9 flex-wrap: wrap-reverse;
10 10
11 .title-page.active, 11 .title-page {
12 .title-page.title-page-single {
13 @include margin-right(.5rem !important); 12 @include margin-right(.5rem !important);
14 13
15 margin-bottom: unset; 14 margin-bottom: unset;
15 margin-top: 0;
16 } 16 }
17} 17}
18 18
19.title-page {
20 margin-top: 0;
21}
22
23.title-page-autoplay { 19.title-page-autoplay {
24 @include margin-left(auto); 20 @include margin-left(auto);
25 21
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html
index 1ea0cf6b8..461891779 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch.component.html
@@ -61,7 +61,7 @@
61 <div class="video-info-channel-left d-flex"> 61 <div class="video-info-channel-left d-flex">
62 <my-video-avatar-channel [video]="video" [showChannel]="!isChannelDisplayNameGeneric()"></my-video-avatar-channel> 62 <my-video-avatar-channel [video]="video" [showChannel]="!isChannelDisplayNameGeneric()"></my-video-avatar-channel>
63 63
64 <div class="video-info-channel-left-links ml-1"> 64 <div class="video-info-channel-left-links ms-1">
65 <ng-container *ngIf="!isChannelDisplayNameGeneric()"> 65 <ng-container *ngIf="!isChannelDisplayNameGeneric()">
66 <a [routerLink]="[ '/c', video.byVideoChannel ]" i18n-title title="Channel page"> 66 <a [routerLink]="[ '/c', video.byVideoChannel ]" i18n-title title="Channel page">
67 {{ video.channel.displayName }} 67 {{ video.channel.displayName }}
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.scss b/client/src/app/+videos/+video-watch/video-watch.component.scss
index 6818a4257..d438facd3 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/+videos/+video-watch/video-watch.component.scss
@@ -112,7 +112,6 @@ $video-height: 66vh;
112 margin-top: 50px; 112 margin-top: 50px;
113 text-align: center; 113 text-align: center;
114 font-weight: $font-semibold; 114 font-weight: $font-semibold;
115 font-size: 15px;
116} 115}
117 116
118.video-bottom { 117.video-bottom {
@@ -158,12 +157,11 @@ $video-height: 66vh;
158 157
159 margin-bottom: 10px; 158 margin-bottom: 10px;
160 align-self: start; 159 align-self: start;
161 font-size: 1em; 160 font-size: 14px;
162} 161}
163 162
164.video-info-channel { 163.video-info-channel {
165 font-weight: $font-semibold; 164 font-weight: $font-semibold;
166 font-size: 15px;
167 165
168 a { 166 a {
169 @include disable-default-a-behaviour; 167 @include disable-default-a-behaviour;
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.html b/client/src/app/+videos/video-list/overview/video-overview.component.html
index f250c2407..b38516af8 100644
--- a/client/src/app/+videos/video-list/overview/video-overview.component.html
+++ b/client/src/app/+videos/video-list/overview/video-overview.component.html
@@ -1,4 +1,4 @@
1<h1 class="sr-only" i18n>Discover</h1> 1<h1 class="visually-hidden" i18n>Discover</h1>
2<div class="margin-content"> 2<div class="margin-content">
3 3
4 <div class="no-results" i18n *ngIf="notResults">No results.</div> 4 <div class="no-results" i18n *ngIf="notResults">No results.</div>
@@ -10,7 +10,7 @@
10 10
11 <div class="section videos" *ngFor="let object of overview.categories"> 11 <div class="section videos" *ngFor="let object of overview.categories">
12 <h1 class="section-title"> 12 <h1 class="section-title">
13 <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a> 13 <a class="link-orange" routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a>
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)">
@@ -21,7 +21,7 @@
21 21
22 <div class="section videos" *ngFor="let object of overview.tags"> 22 <div class="section videos" *ngFor="let object of overview.tags">
23 <h2 class="section-title"> 23 <h2 class="section-title">
24 <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a> 24 <a class="link-orange" routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a>
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)">
@@ -32,8 +32,8 @@
32 32
33 <div class="section channel videos" *ngFor="let object of overview.channels"> 33 <div class="section channel videos" *ngFor="let object of overview.channels">
34 <div class="section-title"> 34 <div class="section-title">
35 <a [routerLink]="[ '/c', buildVideoChannelBy(object) ]"> 35 <a class="link-orange" [routerLink]="[ '/c', buildVideoChannelBy(object) ]">
36 <my-actor-avatar [channel]="buildVideoChannel(object)" size="28"></my-actor-avatar> 36 <my-actor-avatar [actor]="buildVideoChannel(object)" actorType="channel" size="28"></my-actor-avatar>
37 37
38 <h2 class="section-title">{{ object.channel.displayName }}</h2> 38 <h2 class="section-title">{{ object.channel.displayName }}</h2>
39 </a> 39 </a>
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 8b2aa88f2..5a789b66d 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
@@ -13,7 +13,7 @@
13 13
14.section { 14.section {
15 &:first-child { 15 &:first-child {
16 padding-top: 30px; 16 padding-top: 15px;
17 17
18 .section-title { 18 .section-title {
19 border-top: 0 !important; 19 border-top: 0 !important;
@@ -22,66 +22,27 @@
22 22
23 .section-title { 23 .section-title {
24 font-size: 24px; 24 font-size: 24px;
25 font-weight: $font-semibold; 25 padding-top: 20px;
26 padding-top: 15px; 26 margin-bottom: 30px;
27 margin-bottom: 15px;
28 display: flex;
29 justify-content: space-between;
30 27
31 &:not(h2) { 28 &:not(h2) {
32 border-top: 1px solid $separator-border-color; 29 border-top: 1px solid $separator-border-color;
33 } 30 }
34 31
35 a { 32 a > h2 {
36 color: pvar(--mainForegroundColor); 33 margin-bottom: 0;
37 34 display: inline-block;
38 &:hover, 35 font-weight: $font-bold;
39 &:focus:not(.focus-visible),
40 &:active {
41 text-decoration: none;
42 outline: none;
43 }
44 } 36 }
45 }
46
47 &.channel {
48 .section-title {
49 a {
50 display: flex;
51 width: fit-content;
52 align-items: center;
53
54 my-actor-avatar {
55 @include margin-right(8px);
56
57 font-size: initial;
58 }
59 }
60 37
61 .followers { 38 my-actor-avatar {
62 @include margin-left(10px); 39 @include margin-right(8px);
63 40
64 color: pvar(--greyForegroundColor); 41 position: relative;
65 font-weight: normal; 42 top: -2px;
66 font-size: 14px;
67 position: relative;
68 top: 2px;
69 }
70 } 43 }
71 } 44 }
72 45
73 .show-more {
74 position: relative;
75 top: -5px;
76 display: inline-block;
77 font-size: 16px;
78 text-transform: uppercase;
79 color: pvar(--greyForegroundColor);
80 margin-bottom: 10px;
81 font-weight: $font-semibold;
82 text-decoration: none;
83 }
84
85 @media screen and (max-width: $mobile-view) { 46 @media screen and (max-width: $mobile-view) {
86 max-height: initial; 47 max-height: initial;
87 overflow: initial; 48 overflow: initial;
diff --git a/client/src/app/+videos/video-list/videos-list-common-page.component.ts b/client/src/app/+videos/video-list/videos-list-common-page.component.ts
index d2782036b..c8fa8ef30 100644
--- a/client/src/app/+videos/video-list/videos-list-common-page.component.ts
+++ b/client/src/app/+videos/video-list/videos-list-common-page.component.ts
@@ -204,13 +204,28 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable
204 if ([ 'hot', 'trending', 'likes', 'views' ].includes(sanitizedSort)) { 204 if ([ 'hot', 'trending', 'likes', 'views' ].includes(sanitizedSort)) {
205 this.title = $localize`Trending` 205 this.title = $localize`Trending`
206 206
207 if (sanitizedSort === 'hot') this.titleTooltip = $localize`Videos with the most interactions for recent videos` 207 if (sanitizedSort === 'hot') {
208 if (sanitizedSort === 'likes') this.titleTooltip = $localize`Videos that have the most likes` 208 this.titleTooltip = $localize`Videos with the most interactions for recent videos`
209 if (sanitizedSort === 'views') this.titleTooltip = undefined 209 return
210 }
211
212 if (sanitizedSort === 'likes') {
213 this.titleTooltip = $localize`Videos that have the most likes`
214 return
215 }
216
217 if (sanitizedSort === 'views') {
218 this.titleTooltip = undefined
219 return
220 }
210 221
211 if (sanitizedSort === 'trending') { 222 if (sanitizedSort === 'trending') {
212 if (this.trendingDays === 1) this.titleTooltip = $localize`Videos with the most views during the last 24 hours` 223 if (this.trendingDays === 1) {
213 else this.titleTooltip = $localize`Videos with the most views during the last ${this.trendingDays} days` 224 this.titleTooltip = $localize`Videos with the most views during the last 24 hours`
225 return
226 }
227
228 this.titleTooltip = $localize`Videos with the most views during the last ${this.trendingDays} days`
214 } 229 }
215 230
216 return 231 return
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index 6969329e8..629c04e6b 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -53,9 +53,7 @@
53 <p>{{ message.detail }}</p> 53 <p>{{ message.detail }}</p>
54 </div> 54 </div>
55 55
56 <span *ngIf="message.severity === 'success'" class="glyphicon glyphicon-ok"></span> 56 <my-global-icon [iconName]="getNotificationIcon(message)"></my-global-icon>
57 <span *ngIf="message.severity === 'info'" class="glyphicon glyphicon-info-sign"></span>
58 <span *ngIf="message.severity === 'error'" class="glyphicon glyphicon-remove"></span>
59 </div> 57 </div>
60 </ng-template> 58 </ng-template>
61</p-toast> 59</p-toast>
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss
index 5f3e15d80..31e9987c6 100644
--- a/client/src/app/app.component.scss
+++ b/client/src/app/app.component.scss
@@ -103,14 +103,4 @@
103 cursor: pointer; 103 cursor: pointer;
104 width: 20px; 104 width: 20px;
105 } 105 }
106
107 ::ng-deep {
108 p {
109 font-size: 16px;
110 }
111
112 p:last-child {
113 margin-bottom: 0;
114 }
115 }
116} 106}
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index a60138af9..8fdab0c40 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -31,6 +31,7 @@ import { BroadcastMessageLevel, HTMLServerConfig, UserRole } from '@shared/model
31import { MenuService } from './core/menu/menu.service' 31import { MenuService } from './core/menu/menu.service'
32import { POP_STATE_MODAL_DISMISS } from './helpers' 32import { POP_STATE_MODAL_DISMISS } from './helpers'
33import { InstanceService } from './shared/shared-instance' 33import { InstanceService } from './shared/shared-instance'
34import { GlobalIconName } from './shared/shared-icons'
34 35
35@Component({ 36@Component({
36 selector: 'my-app', 37 selector: 'my-app',
@@ -150,6 +151,17 @@ export class AppComponent implements OnInit, AfterViewInit {
150 this.screenService.isBroadcastMessageDisplayed = false 151 this.screenService.isBroadcastMessageDisplayed = false
151 } 152 }
152 153
154 getNotificationIcon (message: { severity: 'success' | 'error' | 'info' }): GlobalIconName {
155 switch (message.severity) {
156 case 'error':
157 return 'cross'
158 case 'success':
159 return 'tick'
160 case 'info':
161 return 'help'
162 }
163 }
164
153 private initRouteEvents () { 165 private initRouteEvents () {
154 const eventsObs = this.router.events 166 const eventsObs = this.router.events
155 167
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index d80f95ed6..4a4c2321b 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -41,7 +41,9 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra
41 ToastModule, 41 ToastModule,
42 42
43 HotkeyModule.forRoot({ 43 HotkeyModule.forRoot({
44 cheatSheetCloseEsc: true 44 cheatSheetCloseEsc: true,
45 cheatSheetDescription: $localize`Show/hide this help menu`,
46 cheatSheetCloseEscDescription: $localize`Hide this help menu`
45 }) 47 })
46 ], 48 ],
47 49
diff --git a/client/src/app/core/menu/menu.service.ts b/client/src/app/core/menu/menu.service.ts
index f0dc4fcaa..81837db7e 100644
--- a/client/src/app/core/menu/menu.service.ts
+++ b/client/src/app/core/menu/menu.service.ts
@@ -132,7 +132,7 @@ export class MenuService {
132 path: '/videos/trending' 132 path: '/videos/trending'
133 }, 133 },
134 { 134 {
135 icon: 'recently-added' as 'recently-added', 135 icon: 'add' as 'add',
136 label: $localize`Recently added videos`, 136 label: $localize`Recently added videos`,
137 shortLabel: $localize`Recently added`, 137 shortLabel: $localize`Recently added`,
138 path: '/videos/recently-added' 138 path: '/videos/recently-added'
diff --git a/client/src/app/core/notification/peertube-socket.service.ts b/client/src/app/core/notification/peertube-socket.service.ts
index 0db86d8e7..50a11e948 100644
--- a/client/src/app/core/notification/peertube-socket.service.ts
+++ b/client/src/app/core/notification/peertube-socket.service.ts
@@ -1,5 +1,5 @@
1import { Subject } from 'rxjs' 1import { Subject } from 'rxjs'
2import { io, Socket } from 'socket.io-client' 2import { ManagerOptions, Socket, SocketOptions } from 'socket.io-client'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { LiveVideoEventPayload, LiveVideoEventType, UserNotification as UserNotificationServer } from '@shared/models' 4import { LiveVideoEventPayload, LiveVideoEventType, UserNotification as UserNotificationServer } from '@shared/models'
5import { environment } from '../../../environments/environment' 5import { environment } from '../../../environments/environment'
@@ -9,7 +9,7 @@ export type NotificationEvent = 'new' | 'read' | 'read-all'
9 9
10@Injectable() 10@Injectable()
11export class PeerTubeSocket { 11export class PeerTubeSocket {
12 private io: typeof io 12 private io: (uri: string, opts?: Partial<ManagerOptions & SocketOptions>) => Socket
13 13
14 private notificationSubject = new Subject<{ type: NotificationEvent, notification?: UserNotificationServer }>() 14 private notificationSubject = new Subject<{ type: NotificationEvent, notification?: UserNotificationServer }>()
15 private liveVideosSubject = new Subject<{ type: LiveVideoEventType, payload: LiveVideoEventPayload }>() 15 private liveVideosSubject = new Subject<{ type: LiveVideoEventType, payload: LiveVideoEventPayload }>()
diff --git a/client/src/app/core/rest/rest-extractor.service.ts b/client/src/app/core/rest/rest-extractor.service.ts
index 17053811c..86c7484a5 100644
--- a/client/src/app/core/rest/rest-extractor.service.ts
+++ b/client/src/app/core/rest/rest-extractor.service.ts
@@ -34,49 +34,17 @@ export class RestExtractor {
34 return target 34 return target
35 } 35 }
36 36
37 handleError (err: any) { 37 redirectTo404IfNotFound (obj: { status: number }, type: 'video' | 'other', status = [ HttpStatusCode.NOT_FOUND_404 ]) {
38 let errorMessage 38 if (obj?.status && status.includes(obj.status)) {
39 // Do not use redirectService to avoid circular dependencies
40 this.router.navigate([ '/404' ], { state: { type, obj }, skipLocationChange: true })
41 }
39 42
40 if (err.error instanceof Error) { 43 return observableThrowError(() => obj)
41 // A client-side or network error occurred. Handle it accordingly. 44 }
42 errorMessage = err.error.detail || err.error.title
43 console.error('An error occurred:', errorMessage)
44 } else if (typeof err.error === 'string') {
45 errorMessage = err.error
46 } else if (err.status !== undefined) {
47 // A server-side error occurred.
48 if (err.error?.errors) {
49 const errors = err.error.errors
50 const errorsArray: string[] = []
51
52 Object.keys(errors).forEach(key => {
53 errorsArray.push(errors[key].msg)
54 })
55
56 errorMessage = errorsArray.join('. ')
57 } else if (err.error?.error) {
58 errorMessage = err.error.error
59 } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
60 // eslint-disable-next-line max-len
61 errorMessage = $localize`Media is too large for the server. Please contact you administrator if you want to increase the limit size.`
62 } else if (err.status === HttpStatusCode.TOO_MANY_REQUESTS_429) {
63 const secondsLeft = err.headers.get('retry-after')
64 if (secondsLeft) {
65 const minutesLeft = Math.floor(parseInt(secondsLeft, 10) / 60)
66 errorMessage = $localize`Too many attempts, please try again after ${minutesLeft} minutes.`
67 } else {
68 errorMessage = $localize`Too many attempts, please try again later.`
69 }
70 } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
71 errorMessage = $localize`Server error. Please retry later.`
72 }
73 45
74 errorMessage = errorMessage || 'Unknown error.' 46 handleError (err: any) {
75 console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) 47 const errorMessage = this.buildErrorMessage(err)
76 } else {
77 console.error(err)
78 errorMessage = err
79 }
80 48
81 const errorObj: { message: string, status: string, body: string } = { 49 const errorObj: { message: string, status: string, body: string } = {
82 message: errorMessage, 50 message: errorMessage,
@@ -92,12 +60,63 @@ export class RestExtractor {
92 return observableThrowError(() => errorObj) 60 return observableThrowError(() => errorObj)
93 } 61 }
94 62
95 redirectTo404IfNotFound (obj: { status: number }, type: 'video' | 'other', status = [ HttpStatusCode.NOT_FOUND_404 ]) { 63 private buildErrorMessage (err: any) {
96 if (obj?.status && status.includes(obj.status)) { 64 if (err.error instanceof Error) {
97 // Do not use redirectService to avoid circular dependencies 65 // A client-side or network error occurred. Handle it accordingly.
98 this.router.navigate([ '/404' ], { state: { type, obj }, skipLocationChange: true }) 66 const errorMessage = err.error.detail || err.error.title
67 console.error('An error occurred:', errorMessage)
68
69 return errorMessage
99 } 70 }
100 71
101 return observableThrowError(() => obj) 72 if (typeof err.error === 'string') {
73 return err.error
74 }
75
76 if (err.status !== undefined) {
77 const errorMessage = this.buildServerErrorMessage(err)
78 console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`)
79
80 return errorMessage
81 }
82
83 console.error(err)
84 return err
85 }
86
87 private buildServerErrorMessage (err: any) {
88 // A server-side error occurred.
89 if (err.error?.errors) {
90 const errors = err.error.errors
91
92 return Object.keys(errors)
93 .map(key => errors[key].msg)
94 .join('. ')
95 }
96
97 if (err.error?.error) {
98 return err.error.error
99 }
100
101 if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
102 return $localize`Media is too large for the server. Please contact you administrator if you want to increase the limit size.`
103 }
104
105 if (err.status === HttpStatusCode.TOO_MANY_REQUESTS_429) {
106 const secondsLeft = err.headers.get('retry-after')
107
108 if (secondsLeft) {
109 const minutesLeft = Math.floor(parseInt(secondsLeft, 10) / 60)
110 return $localize`Too many attempts, please try again after ${minutesLeft} minutes.`
111 }
112
113 return $localize`Too many attempts, please try again later.`
114 }
115
116 if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
117 return $localize`Server error. Please retry later.`
118 }
119
120 return $localize`Unknown server error`
102 } 121 }
103} 122}
diff --git a/client/src/app/core/rest/rest-table.ts b/client/src/app/core/rest/rest-table.ts
index d8b039187..7b765f7fc 100644
--- a/client/src/app/core/rest/rest-table.ts
+++ b/client/src/app/core/rest/rest-table.ts
@@ -39,6 +39,10 @@ export abstract class RestTable {
39 } 39 }
40 } 40 }
41 41
42 saveSort () {
43 peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort))
44 }
45
42 loadLazy (event: LazyLoadEvent) { 46 loadLazy (event: LazyLoadEvent) {
43 logger('Load lazy %o.', event) 47 logger('Load lazy %o.', event)
44 48
@@ -60,10 +64,6 @@ export abstract class RestTable {
60 this.saveSort() 64 this.saveSort()
61 } 65 }
62 66
63 saveSort () {
64 peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort))
65 }
66
67 onSearch (search: string) { 67 onSearch (search: string) {
68 this.search = search 68 this.search = search
69 this.reloadData() 69 this.reloadData()
diff --git a/client/src/app/core/routing/scroll.service.ts b/client/src/app/core/routing/scroll.service.ts
index bd5076502..6d37fde71 100644
--- a/client/src/app/core/routing/scroll.service.ts
+++ b/client/src/app/core/routing/scroll.service.ts
@@ -67,7 +67,7 @@ export class ScrollService {
67 private consumeScroll () { 67 private consumeScroll () {
68 // Handle anchors/restore position 68 // Handle anchors/restore position
69 this.peertubeRouter.getScrollEvents().subscribe(e => { 69 this.peertubeRouter.getScrollEvents().subscribe(e => {
70 logger('Will schedule scroll after router event %o.', e) 70 logger('Will schedule scroll after router event %o.', { e, resetScroll: this.resetScroll })
71 71
72 // scrollToAnchor first to preserve anchor position when using history navigation 72 // scrollToAnchor first to preserve anchor position when using history navigation
73 if (e.anchor) { 73 if (e.anchor) {
diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts
index e88511054..40939ecb8 100644
--- a/client/src/app/core/theme/theme.service.ts
+++ b/client/src/app/core/theme/theme.service.ts
@@ -1,4 +1,5 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { capitalizeFirstLetter } from '@root-helpers/string'
2import { UserLocalStorageKeys } from '@root-helpers/users' 3import { UserLocalStorageKeys } from '@root-helpers/users'
3import { HTMLServerConfig, ServerConfigTheme } from '@shared/models' 4import { HTMLServerConfig, ServerConfigTheme } from '@shared/models'
4import { environment } from '../../../environments/environment' 5import { environment } from '../../../environments/environment'
@@ -40,6 +41,19 @@ export class ThemeService {
40 this.listenUserTheme() 41 this.listenUserTheme()
41 } 42 }
42 43
44 getDefaultThemeLabel () {
45 if (this.hasDarkTheme()) {
46 return $localize`Light/Orange or Dark`
47 }
48
49 return $localize`Light/Orange`
50 }
51
52 buildAvailableThemes () {
53 return this.serverConfig.theme.registered
54 .map(t => ({ id: t.name, label: capitalizeFirstLetter(t.name) }))
55 }
56
43 private injectThemes (themes: ServerConfigTheme[], fromLocalStorage = false) { 57 private injectThemes (themes: ServerConfigTheme[], fromLocalStorage = false) {
44 this.themes = themes 58 this.themes = themes
45 59
@@ -81,10 +95,7 @@ export class ThemeService {
81 if (instanceTheme !== 'default') return instanceTheme 95 if (instanceTheme !== 'default') return instanceTheme
82 96
83 // Default to dark theme if available and wanted by the user 97 // Default to dark theme if available and wanted by the user
84 if ( 98 if (this.hasDarkTheme() && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
85 this.themes.find(t => t.name === 'dark') &&
86 window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
87 ) {
88 return 'dark' 99 return 'dark'
89 } 100 }
90 101
@@ -193,4 +204,8 @@ export class ThemeService {
193 private getTheme (name: string) { 204 private getTheme (name: string) {
194 return this.themes.find(t => t.name === name) 205 return this.themes.find(t => t.name === name)
195 } 206 }
207
208 private hasDarkTheme () {
209 return this.serverConfig.theme.registered.some(t => t.name === 'dark')
210 }
196} 211}
diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss
index de483086b..8a4111c5a 100644
--- a/client/src/app/header/header.component.scss
+++ b/client/src/app/header/header.component.scss
@@ -2,7 +2,11 @@
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4my-search-typeahead { 4my-search-typeahead {
5 @include margin-right(15px); 5 @include margin-right(80px);
6
7 @media screen and (max-width: $small-view) {
8 @include margin-right(15px);
9 }
6} 10}
7 11
8.publish-button { 12.publish-button {
@@ -11,7 +15,7 @@ my-search-typeahead {
11 @include button-with-icon(22px, 3px, -1px); 15 @include button-with-icon(22px, 3px, -1px);
12 @include margin-right(25px); 16 @include margin-right(25px);
13 17
14 @media screen and (max-width: 600px) { 18 @media screen and (max-width: $mobile-view) {
15 @include margin-right(10px); 19 @include margin-right(10px);
16 20
17 padding: 0 10px; 21 padding: 0 10px;
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html
index 7a9b6c51f..783b4b53b 100644
--- a/client/src/app/header/search-typeahead.component.html
+++ b/client/src/app/header/search-typeahead.component.html
@@ -25,8 +25,7 @@
25 <div class="d-flex justify-content-between"> 25 <div class="d-flex justify-content-between">
26 <label class="small-title" i18n>GLOBAL SEARCH</label> 26 <label class="small-title" i18n>GLOBAL SEARCH</label>
27 <div class="advanced-search-status muted"> 27 <div class="advanced-search-status muted">
28 <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.search.searchIndex.url }}</span> 28 <span *ngIf="serverConfig" class="me-1" i18n>using {{ serverConfig.search.searchIndex.url }}</span>
29 <i class="glyphicon glyphicon-globe"></i>
30 </div> 29 </div>
31 </div> 30 </div>
32 <div class="muted" i18n>Results will be augmented with those of a third-party index. Only data necessary to make the query will be sent.</div> 31 <div class="muted" i18n>Results will be augmented with those of a third-party index. Only data necessary to make the query will be sent.</div>
@@ -39,9 +38,8 @@
39 <label class="small-title" i18n>ADVANCED SEARCH</label> 38 <label class="small-title" i18n>ADVANCED SEARCH</label>
40 <div class="advanced-search-status c-help"> 39 <div class="advanced-search-status c-help">
41 <span [ngClass]="canSearchAnyURI ? 'text-success' : 'muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows."> 40 <span [ngClass]="canSearchAnyURI ? 'text-success' : 'muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.">
42 <span *ngIf="canSearchAnyURI()" class="mr-1" i18n>any instance</span> 41 <span *ngIf="canSearchAnyURI()" class="me-1" i18n>any instance</span>
43 <span *ngIf="!canSearchAnyURI()" class="mr-1" i18n>only followed instances</span> 42 <span *ngIf="!canSearchAnyURI()" class="me-1" i18n>only followed instances</span>
44 <i [ngClass]="canSearchAnyURI() ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
45 </span> 43 </span>
46 </div> 44 </div>
47 </div> 45 </div>
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss
index 5114ec3a7..e4c9c602c 100644
--- a/client/src/app/header/search-typeahead.component.scss
+++ b/client/src/app/header/search-typeahead.component.scss
@@ -129,10 +129,6 @@ li.suggestion {
129 } 129 }
130} 130}
131 131
132.glyphicon {
133 top: 3px;
134}
135
136.advanced-search-status { 132.advanced-search-status {
137 height: max-content; 133 height: max-content;
138 cursor: default; 134 cursor: default;
diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html
index 4ac9809e1..3f85ed6ae 100644
--- a/client/src/app/header/suggestion.component.html
+++ b/client/src/app/header/suggestion.component.html
@@ -1,16 +1,16 @@
1<a tabindex="-1" class="d-flex flex-auto flex-items-center p-2" [class.focus-visible]="active"> 1<a tabindex="-1" class="d-flex flex-auto flex-items-center p-2" [class.focus-visible]="active">
2 <div class="flex-shrink-0 mr-2 text-center"> 2 <div class="flex-shrink-0 me-2 text-center">
3 <my-global-icon iconName="search"></my-global-icon> 3 <my-global-icon iconName="search"></my-global-icon>
4 </div> 4 </div>
5 5
6 <img class="avatar mr-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28"> 6 <img class="avatar me-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28">
7 7
8 <div 8 <div
9 class="flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target" 9 class="flex-auto overflow-hidden text-start no-wrap css-truncate css-truncate-target"
10 [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : highlight" 10 [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : highlight"
11 ></div> 11 ></div>
12 12
13 <div class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6"> 13 <div class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ms-1 f6">
14 <span *ngIf="result.type === 'search-instance'" i18n>In this instance's network</span> 14 <span *ngIf="result.type === 'search-instance'" i18n>In this instance's network</span>
15 <span *ngIf="result.type === 'search-index'" i18n>In the vidiverse</span> 15 <span *ngIf="result.type === 'search-index'" i18n>In the vidiverse</span>
16 </div> 16 </div>
diff --git a/client/src/app/helpers/i18n-utils.ts b/client/src/app/helpers/i18n-utils.ts
index bbfb12959..2017a31ea 100644
--- a/client/src/app/helpers/i18n-utils.ts
+++ b/client/src/app/helpers/i18n-utils.ts
@@ -1,4 +1,5 @@
1import { environment } from '../../environments/environment' 1import { environment } from '../../environments/environment'
2import IntlMessageFormat from 'intl-messageformat'
2 3
3function isOnDevLocale () { 4function isOnDevLocale () {
4 return environment.production === false && window.location.search === '?lang=fr' 5 return environment.production === false && window.location.search === '?lang=fr'
@@ -8,7 +9,31 @@ function getDevLocale () {
8 return 'fr-FR' 9 return 'fr-FR'
9} 10}
10 11
12function prepareIcu (icu: string) {
13 let alreadyWarned = false
14
15 try {
16 const msg = new IntlMessageFormat(icu, $localize.locale)
17
18 return (context: { [id: string]: number | string }, fallback: string) => {
19 try {
20 return msg.format(context) as string
21 } catch (err) {
22 if (!alreadyWarned) console.warn('Cannot format ICU %s.', icu, err)
23
24 alreadyWarned = true
25 return fallback
26 }
27 }
28 } catch (err) {
29 console.warn('Cannot build intl message %s.', icu, err)
30
31 return (_context: unknown, fallback: string) => fallback
32 }
33}
34
11export { 35export {
12 getDevLocale, 36 getDevLocale,
37 prepareIcu,
13 isOnDevLocale 38 isOnDevLocale
14} 39}
diff --git a/client/src/app/helpers/utils/upload.ts b/client/src/app/helpers/utils/upload.ts
index a3fce7fee..5c2600a0d 100644
--- a/client/src/app/helpers/utils/upload.ts
+++ b/client/src/app/helpers/utils/upload.ts
@@ -2,36 +2,43 @@ import { HttpErrorResponse } from '@angular/common/http'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { HttpStatusCode } from '@shared/models' 3import { HttpStatusCode } from '@shared/models'
4 4
5function genericUploadErrorHandler (parameters: { 5function genericUploadErrorHandler (options: {
6 err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'> 6 err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'>
7 name: string 7 name: string
8 notifier: Notifier 8 notifier: Notifier
9 sticky?: boolean 9 sticky?: boolean
10}) { 10}) {
11 const { err, name, notifier, sticky } = { sticky: false, ...parameters } 11 const { err, name, notifier, sticky = false } = options
12 const title = $localize`The upload failed` 12 const title = $localize`Upload failed`
13 let message = err.message 13 const message = buildMessage(name, err)
14
15 if (err instanceof ErrorEvent) { // network error
16 message = $localize`The connection was interrupted`
17 notifier.error(message, title, null, sticky)
18 } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
19 message = $localize`The server encountered an error`
20 notifier.error(message, title, null, sticky)
21 } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) {
22 message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)`
23 notifier.error(message, title, null, sticky)
24 } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
25 const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G'
26 message = $localize`Your ${name} file was too large (max. size: ${maxFileSize})`
27 notifier.error(message, title, null, sticky)
28 } else {
29 notifier.error(err.message, title)
30 }
31 14
15 notifier.error(message, title, null, sticky)
32 return message 16 return message
33} 17}
34 18
35export { 19export {
36 genericUploadErrorHandler 20 genericUploadErrorHandler
37} 21}
22
23// ---------------------------------------------------------------------------
24
25function buildMessage (name: string, err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'>) {
26 if (err instanceof ErrorEvent) { // network error
27 return $localize`The connection was interrupted`
28 }
29
30 if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
31 return $localize`The server encountered an error`
32 }
33
34 if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) {
35 return $localize`Your ${name} file couldn't be transferred before the server proxy timeout`
36 }
37
38 if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
39 const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G'
40 return $localize`Your ${name} file was too large (max. size: ${maxFileSize})`
41 }
42
43 return err.message
44}
diff --git a/client/src/app/menu/language-chooser.component.scss b/client/src/app/menu/language-chooser.component.scss
index 04b18af33..28d58a678 100644
--- a/client/src/app/menu/language-chooser.component.scss
+++ b/client/src/app/menu/language-chooser.component.scss
@@ -18,7 +18,6 @@
18 18
19 a { 19 a {
20 display: block; 20 display: block;
21 font-size: 16px;
22 margin: 15px; 21 margin: 15px;
23 } 22 }
24} 23}
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index b8f8d68ab..1a9ac9e8b 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -8,7 +8,8 @@
8 [container]="dropdownContainer" (openChange)="onDropdownOpenChange($event)" autoClose="outside" 8 [container]="dropdownContainer" (openChange)="onDropdownOpenChange($event)" autoClose="outside"
9 > 9 >
10 <div ngbDropdownToggle> 10 <div ngbDropdownToggle>
11 <my-actor-avatar [account]="user.account" size="34"></my-actor-avatar> 11 <my-actor-avatar [actor]="user.account" actorType="account" size="34"></my-actor-avatar>
12
12 <div class="logged-in-info"> 13 <div class="logged-in-info">
13 <div class="logged-in-display-name">{{ user.account?.displayName }}</div> 14 <div class="logged-in-display-name">{{ user.account?.displayName }}</div>
14 15
@@ -16,7 +17,7 @@
16 </div> 17 </div>
17 18
18 <div class="dropdown-toggle-indicator"> 19 <div class="dropdown-toggle-indicator">
19 <span class="glyphicon glyphicon-chevron-down"></span> 20 <span class="chevron-down"></span>
20 </div> 21 </div>
21 </div> 22 </div>
22 23
@@ -36,31 +37,31 @@
36 > 37 >
37 <my-global-icon iconName="language" aria-hidden="true"></my-global-icon> 38 <my-global-icon iconName="language" aria-hidden="true"></my-global-icon>
38 <span i18n>Interface:</span> 39 <span i18n>Interface:</span>
39 <span class="ml-auto muted">{{ currentInterfaceLanguage }}</span> 40 <span class="ms-auto muted">{{ currentInterfaceLanguage }}</span>
40 </a> 41 </a>
41 42
42 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account/settings" fragment="video-languages-subtitles" 43 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account/settings" fragment="video-languages-subtitles"
43 #settingsLanguagesSubtitles (click)="onActiveLinkScrollToAnchor(settingsLanguagesSubtitles)"> 44 #settingsLanguagesSubtitles (click)="onActiveLinkScrollToAnchor(settingsLanguagesSubtitles)">
44 <my-global-icon iconName="video-lang" aria-hidden="true"></my-global-icon> 45 <my-global-icon iconName="video-lang" aria-hidden="true"></my-global-icon>
45 <span i18n>Videos:</span> 46 <span i18n>Videos:</span>
46 <span class="ml-auto muted">{{ videoLanguages.join(', ') }}</span> 47 <span class="ms-auto muted">{{ videoLanguages.join(', ') }}</span>
47 </a> 48 </a>
48 49
49 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item settings-sensitive" routerLink="/my-account/settings" 50 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item settings-sensitive" routerLink="/my-account/settings"
50 fragment="video-sensitive-content-policy" #settingsSensitiveContentPolicy 51 fragment="video-sensitive-content-policy" #settingsSensitiveContentPolicy
51 (click)="onActiveLinkScrollToAnchor(settingsSensitiveContentPolicy)" 52 (click)="onActiveLinkScrollToAnchor(settingsSensitiveContentPolicy)"
52 > 53 >
53 <my-global-icon class="hover-display-toggle" [hidden]="user.nsfwPolicy === 'display'" iconName="sensitive" aria-hidden="true"></my-global-icon> 54 <my-global-icon class="hover-display-toggle" [hidden]="user.nsfwPolicy === 'display'" iconName="eye-open" aria-hidden="true"></my-global-icon>
54 <my-global-icon class="hover-display-toggle" [hidden]="user.nsfwPolicy !== 'display'" iconName="unsensitive" aria-hidden="true"></my-global-icon> 55 <my-global-icon class="hover-display-toggle" [hidden]="user.nsfwPolicy !== 'display'" iconName="eye-close" aria-hidden="true"></my-global-icon>
55 <span i18n>Sensitive:</span> 56 <span i18n>Sensitive:</span>
56 <span class="ml-auto muted">{{ nsfwPolicy }}</span> 57 <span class="ms-auto muted">{{ nsfwPolicy }}</span>
57 </a> 58 </a>
58 59
59 <a ngbDropdownItem class="dropdown-item" (click)="toggleUseP2P()"> 60 <a ngbDropdownItem class="dropdown-item" (click)="toggleUseP2P()">
60 <my-global-icon iconName="p2p" aria-hidden="true"></my-global-icon> 61 <my-global-icon iconName="p2p" aria-hidden="true"></my-global-icon>
61 <ng-container i18n>Help share videos</ng-container> 62 <ng-container i18n>Help share videos</ng-container>
62 63
63 <my-input-switch class="ml-auto" [checked]="user.p2pEnabled"></my-input-switch> 64 <my-input-switch class="ms-auto" [checked]="user.p2pEnabled"></my-input-switch>
64 </a> 65 </a>
65 66
66 <div class="dropdown-divider"></div> 67 <div class="dropdown-divider"></div>
@@ -149,7 +150,7 @@
149 150
150 <div class="footer-copyleft"> 151 <div class="footer-copyleft">
151 <small class="d-inline" i18n-title title="powered by PeerTube - CopyLeft 2015-2022"> 152 <small class="d-inline" i18n-title title="powered by PeerTube - CopyLeft 2015-2022">
152 <a href="https://joinpeertube.org" class="mr-1" target="_blank" rel="noopener noreferrer" i18n>powered by PeerTube</a> 153 <a href="https://joinpeertube.org" class="me-1" target="_blank" rel="noopener noreferrer" i18n>powered by PeerTube</a>
153 154
154 <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" target="_blank" rel="noopener noreferrer"> 155 <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" target="_blank" rel="noopener noreferrer">
155 <span aria-label="copyleft" class="d-inline-block" style="transform: rotateY(180deg)">&copy;</span> 2015-2022 156 <span aria-label="copyleft" class="d-inline-block" style="transform: rotateY(180deg)">&copy;</span> 2015-2022
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index 808a33a4a..a548c08cf 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -16,7 +16,6 @@ $footer-links-base-opacity: .8;
16 16
17 color: pvar(--menuForegroundColor); 17 color: pvar(--menuForegroundColor);
18 cursor: pointer; 18 cursor: pointer;
19 font-size: 16px;
20 white-space: normal; 19 white-space: normal;
21 word-break: break-word; 20 word-break: break-word;
22 transition: background-color .1s ease-in-out; 21 transition: background-color .1s ease-in-out;
@@ -106,6 +105,7 @@ my-notification {
106} 105}
107 106
108.logged-in-more { 107.logged-in-more {
108
109 @mixin display-hints($is-mobile: false) { 109 @mixin display-hints($is-mobile: false) {
110 background-color: rgba(255, 255, 255, 0.15); 110 background-color: rgba(255, 255, 255, 0.15);
111 111
@@ -114,18 +114,16 @@ my-notification {
114 display: inherit !important; 114 display: inherit !important;
115 } 115 }
116 116
117 .dropdown-toggle { 117 > .dropdown-toggle {
118 max-width: 88% !important; 118 max-width: 88% !important;
119 } 119 }
120 } 120 }
121 } 121 }
122 122
123 $main-radius: 25px;
124
125 @include margin-left(13px); 123 @include margin-left(13px);
126 124
127 flex: 1; 125 flex: 1;
128 border-radius: $main-radius; 126 border-radius: 25px;
129 transition: all .1s ease-in-out; 127 transition: all .1s ease-in-out;
130 cursor: pointer; 128 cursor: pointer;
131 line-height: 1; 129 line-height: 1;
@@ -157,7 +155,6 @@ my-notification {
157 155
158 .dropdown-toggle-indicator { 156 .dropdown-toggle-indicator {
159 position: relative; 157 position: relative;
160 width: 0;
161 display: none; 158 display: none;
162 159
163 span { 160 span {
@@ -165,21 +162,17 @@ my-notification {
165 right: -35px; 162 right: -35px;
166 top: -8px; 163 top: -8px;
167 color: #808080; 164 color: #808080;
168 width: $main-radius;
169 } 165 }
170 } 166 }
171 167
172 .dropdown-toggle { 168 .dropdown-toggle::after {
173 &::after { 169 border: 0;
174 border: 0;
175 }
176 } 170 }
177 171
178 .dropdown-toggle:first-child { 172 > .dropdown-toggle:first-child {
179 display: flex; 173 display: flex;
180 align-items: center; 174 align-items: center;
181 padding: 5px 7px; 175 padding: 5px 7px;
182 border-radius: $main-radius;
183 } 176 }
184} 177}
185 178
@@ -201,7 +194,6 @@ my-actor-avatar {
201.logged-in-display-name { 194.logged-in-display-name {
202 @include disable-default-a-behaviour; 195 @include disable-default-a-behaviour;
203 196
204 font-size: 16px;
205 font-weight: $font-semibold; 197 font-weight: $font-semibold;
206 color: pvar(--menuForegroundColor); 198 color: pvar(--menuForegroundColor);
207} 199}
@@ -325,7 +317,7 @@ my-actor-avatar {
325 color: pvar(--menuForegroundColor); 317 color: pvar(--menuForegroundColor);
326 opacity: $footer-links-base-opacity; 318 opacity: $footer-links-base-opacity;
327 white-space: nowrap; 319 white-space: nowrap;
328 font-size: 90%; 320 font-size: 0.75rem;
329 font-weight: 500; 321 font-weight: 500;
330 line-height: 1.4rem; 322 line-height: 1.4rem;
331 } 323 }
@@ -358,10 +350,6 @@ my-actor-avatar {
358 display: flex; 350 display: flex;
359 align-items: center; 351 align-items: center;
360 352
361 i.glyphicon-menu-right {
362 opacity: .4;
363 }
364
365 &:hover { 353 &:hover {
366 .hover-display-toggle { 354 .hover-display-toggle {
367 display: none; 355 display: none;
@@ -396,13 +384,6 @@ my-actor-avatar {
396 .dropdown-menu { 384 .dropdown-menu {
397 width: calc(100vw - 30px); 385 width: calc(100vw - 30px);
398 } 386 }
399
400 .dropdown-item:hover,
401 .dropdown-item:active {
402 &.settings-sensitive my-global-icon ::ng-deep svg {
403 margin-top: 0 !important;
404 }
405 }
406} 387}
407 388
408my-global-icon { 389my-global-icon {
diff --git a/client/src/app/menu/notification.component.html b/client/src/app/menu/notification.component.html
index beda1c43c..890b086f1 100644
--- a/client/src/app/menu/notification.component.html
+++ b/client/src/app/menu/notification.component.html
@@ -24,19 +24,24 @@
24 <div> 24 <div>
25 <button 25 <button
26 *ngIf="unreadNotifications" 26 *ngIf="unreadNotifications"
27 i18n-title title="Mark all as read" class="glyphicon glyphicon-ok mr-2" 27 i18n-title title="Mark all as read" class="me-2"
28 (click)="markAllAsRead()" 28 (click)="markAllAsRead()"
29 ></button> 29 >
30 <my-global-icon iconName="tick"></my-global-icon>
31 </button>
32
30 <a 33 <a
31 i18n-title title="Update your notification preferences" class="glyphicon glyphicon-cog" 34 i18n-title title="Update your notification preferences"
32 routerLink="/my-account/settings" fragment="notifications" 35 routerLink="/my-account/settings" fragment="notifications"
33 #settingsNotifications (click)="onNavigate(settingsNotifications)" 36 #settingsNotifications (click)="onNavigate(settingsNotifications)"
34 ></a> 37 >
38 <my-global-icon iconName="cog"></my-global-icon>
39 </a>
35 </div> 40 </div>
36 </div> 41 </div>
37 42
38 <div *ngIf="!loaded" class="loader mt-4"> 43 <div *ngIf="!loaded" class="loader mt-4">
39 <my-loader [loading]="!loaded"></my-loader> 44 <my-loader size="xl" [loading]="!loaded"></my-loader>
40 </div> 45 </div>
41 46
42 <my-user-notifications 47 <my-user-notifications
@@ -45,7 +50,7 @@
45 ></my-user-notifications> 50 ></my-user-notifications>
46 51
47 <a *ngIf="loaded" class="all-notifications" routerLink="/my-account/notifications" #notifications (click)="onNavigate(notifications)"> 52 <a *ngIf="loaded" class="all-notifications" routerLink="/my-account/notifications" #notifications (click)="onNavigate(notifications)">
48 <my-global-icon class="mr-1" iconName="bell" aria-hidden="true"></my-global-icon> 53 <my-global-icon class="me-1" iconName="bell" aria-hidden="true"></my-global-icon>
49 <span i18n>See all your notifications</span> 54 <span i18n>See all your notifications</span>
50 </a> 55 </a>
51 </div> 56 </div>
diff --git a/client/src/app/menu/notification.component.scss b/client/src/app/menu/notification.component.scss
index 3515c3e19..7f72c98d1 100644
--- a/client/src/app/menu/notification.component.scss
+++ b/client/src/app/menu/notification.component.scss
@@ -119,6 +119,10 @@
119 color: rgba(20, 20, 20, 0.8); 119 color: rgba(20, 20, 20, 0.8);
120 } 120 }
121 } 121 }
122
123 my-global-icon {
124 width: 20px;
125 }
122 } 126 }
123 127
124 .all-notifications { 128 .all-notifications {
diff --git a/client/src/app/modal/account-setup-warning-modal.component.scss b/client/src/app/modal/account-setup-warning-modal.component.scss
index d99edaf7a..25e67b08f 100644
--- a/client/src/app/modal/account-setup-warning-modal.component.scss
+++ b/client/src/app/modal/account-setup-warning-modal.component.scss
@@ -2,7 +2,6 @@
2@use '_variables' as *; 2@use '_variables' as *;
3 3
4.modal-body { 4.modal-body {
5 font-size: 15px;
6 display: flex; 5 display: flex;
7 flex-direction: column; 6 flex-direction: column;
8 align-items: center; 7 align-items: center;
@@ -24,7 +23,6 @@
24.subtitle { 23.subtitle {
25 font-weight: $font-semibold; 24 font-weight: $font-semibold;
26 margin-bottom: 10px; 25 margin-bottom: 10px;
27 font-size: 16px;
28} 26}
29 27
30li { 28li {
diff --git a/client/src/app/modal/admin-welcome-modal.component.html b/client/src/app/modal/admin-welcome-modal.component.html
index f5d2b8799..b74d73185 100644
--- a/client/src/app/modal/admin-welcome-modal.component.html
+++ b/client/src/app/modal/admin-welcome-modal.component.html
@@ -56,7 +56,7 @@
56 56
57 <p i18n> 57 <p i18n>
58 Choosing your <strong>instance name</strong>, <strong>setting up a description</strong>, specifying <strong>who you are</strong>, 58 Choosing your <strong>instance name</strong>, <strong>setting up a description</strong>, specifying <strong>who you are</strong>,
59 why <strong>you created your instance</strong> and <strong>how long</strong> you plan to <strong>maintain your it</strong> 59 why <strong>you created your instance</strong> and <strong>how long</strong> you plan to <strong>maintain it</strong>
60 is very important for visitors to understand on what type of instance they are. 60 is very important for visitors to understand on what type of instance they are.
61 </p> 61 </p>
62 62
diff --git a/client/src/app/modal/admin-welcome-modal.component.scss b/client/src/app/modal/admin-welcome-modal.component.scss
index 242a498d0..716f1fff0 100644
--- a/client/src/app/modal/admin-welcome-modal.component.scss
+++ b/client/src/app/modal/admin-welcome-modal.component.scss
@@ -1,10 +1,6 @@
1@use '_mixins' as *; 1@use '_mixins' as *;
2@use '_variables' as *; 2@use '_variables' as *;
3 3
4.modal-body {
5 font-size: 15px;
6}
7
8.two-columns { 4.two-columns {
9 display: flex; 5 display: flex;
10 align-items: center; 6 align-items: center;
@@ -26,7 +22,6 @@
26.subtitle { 22.subtitle {
27 font-weight: $font-semibold; 23 font-weight: $font-semibold;
28 margin-bottom: 10px; 24 margin-bottom: 10px;
29 font-size: 16px;
30} 25}
31 26
32.block-documentation { 27.block-documentation {
@@ -63,7 +58,6 @@ li {
63 } 58 }
64 59
65 .link-title { 60 .link-title {
66 font-size: 16px;
67 font-weight: $font-semibold; 61 font-weight: $font-semibold;
68 display: flex; 62 display: flex;
69 justify-content: center; 63 justify-content: center;
diff --git a/client/src/app/modal/confirm.component.html b/client/src/app/modal/confirm.component.html
index f07501726..c59c25770 100644
--- a/client/src/app/modal/confirm.component.html
+++ b/client/src/app/modal/confirm.component.html
@@ -9,7 +9,7 @@
9 <div class="modal-body" > 9 <div class="modal-body" >
10 <div [innerHtml]="message"></div> 10 <div [innerHtml]="message"></div>
11 11
12 <div *ngIf="inputLabel && expectedInputValue" class="form-group"> 12 <div *ngIf="inputLabel && expectedInputValue" class="form-group mt-3">
13 <label for="confirmInput">{{ inputLabel }}</label> 13 <label for="confirmInput">{{ inputLabel }}</label>
14 <input type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" /> 14 <input type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" />
15 </div> 15 </div>
diff --git a/client/src/app/modal/confirm.component.scss b/client/src/app/modal/confirm.component.scss
index 77ea4d307..3372baf1a 100644
--- a/client/src/app/modal/confirm.component.scss
+++ b/client/src/app/modal/confirm.component.scss
@@ -1,10 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.modal-body {
5 font-size: 15px;
6}
7
8.button { 4.button {
9 padding: 0 13px; 5 padding: 0 13px;
10} 6}
@@ -13,7 +9,3 @@ input[type=text] {
13 @include peertube-input-text(100%); 9 @include peertube-input-text(100%);
14 display: block; 10 display: block;
15} 11}
16
17.form-group {
18 margin: 20px 0;
19}
diff --git a/client/src/app/modal/custom-modal.component.scss b/client/src/app/modal/custom-modal.component.scss
index 15cbadfc0..c5b77994e 100644
--- a/client/src/app/modal/custom-modal.component.scss
+++ b/client/src/app/modal/custom-modal.component.scss
@@ -1,10 +1,6 @@
1@use '_mixins' as *; 1@use '_mixins' as *;
2@use '_variables' as *; 2@use '_variables' as *;
3 3
4.modal-body {
5 font-size: 15px;
6}
7
8li { 4li {
9 margin-bottom: 10px; 5 margin-bottom: 10px;
10} 6}
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 3e6c6d716..b650f4e69 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@use '_mixins' as *; 1@use '_mixins' as *;
2@use '_variables' as *; 2@use '_variables' as *;
3 3
4.modal-body {
5 font-size: 15px;
6}
7
8li { 4li {
9 margin-bottom: 10px; 5 margin-bottom: 10px;
10} 6}
diff --git a/client/src/app/modal/quick-settings-modal.component.html b/client/src/app/modal/quick-settings-modal.component.html
index 8fa3aee50..5c55ee028 100644
--- a/client/src/app/modal/quick-settings-modal.component.html
+++ b/client/src/app/modal/quick-settings-modal.component.html
@@ -5,21 +5,17 @@
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
8 <div i18n class="mb-4 font-italic">These settings apply only to your session on this instance.</div> 8 <div i18n class="alert pt-alert-primary">These settings apply only to your session on this instance.</div>
9 9
10 <h6 i18n class="mb-4">Display settings</h6> 10 <h5 i18n class="mt-4 mb-2">Videos</h5>
11 11
12 <my-user-video-settings 12 <my-user-video-settings
13 *ngIf="!isUserLoggedIn()" 13 *ngIf="!isUserLoggedIn()"
14 [user]="user" [userInformationLoaded]="userInformationLoaded" [reactiveUpdate]="true" [notifyOnUpdate]="true" 14 [user]="user" [userInformationLoaded]="userInformationLoaded" [reactiveUpdate]="true" [notifyOnUpdate]="true"
15 > 15 >
16
17 <ng-container ngProjectAs="inner-title">
18 <h6 i18n class="mb-4 mt-4">Video settings</h6>
19 </ng-container>
20 </my-user-video-settings> 16 </my-user-video-settings>
21 17
22 <h6 i18n class="mb-4 mt-4">Interface settings</h6> 18 <h5 i18n class="mt-4 mb-2">Interface</h5>
23 19
24 <my-user-interface-settings 20 <my-user-interface-settings
25 *ngIf="!isUserLoggedIn()" 21 *ngIf="!isUserLoggedIn()"
diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts
index 6d0dea64e..3262853d8 100644
--- a/client/src/app/shared/form-validators/user-validators.ts
+++ b/client/src/app/shared/form-validators/user-validators.ts
@@ -61,7 +61,7 @@ export const USER_EXISTING_PASSWORD_VALIDATOR: BuildFormValidator = {
61 } 61 }
62} 62}
63 63
64export const USER_PASSWORD_VALIDATOR: BuildFormValidator = { 64export const USER_PASSWORD_VALIDATOR = {
65 VALIDATORS: [ 65 VALIDATORS: [
66 Validators.required, 66 Validators.required,
67 Validators.minLength(6), 67 Validators.minLength(6),
diff --git a/client/src/app/shared/form-validators/video-channel-validators.ts b/client/src/app/shared/form-validators/video-channel-validators.ts
index 48f5b1a2c..163faf270 100644
--- a/client/src/app/shared/form-validators/video-channel-validators.ts
+++ b/client/src/app/shared/form-validators/video-channel-validators.ts
@@ -45,6 +45,6 @@ export const VIDEO_CHANNEL_SUPPORT_VALIDATOR: BuildFormValidator = {
45 ], 45 ],
46 MESSAGES: { 46 MESSAGES: {
47 minlength: $localize`Support text must be at least 3 characters long.`, 47 minlength: $localize`Support text must be at least 3 characters long.`,
48 maxlength: $localize`Support text cannot be more than 1000 characters long` 48 maxlength: $localize`Support text cannot be more than 1000 characters long.`
49 } 49 }
50} 50}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
index 986de15ed..fdf700f7c 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,16 +10,17 @@
10 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" 10 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
11 class="chip" 11 class="chip"
12 > 12 >
13 <my-actor-avatar size="18" [account]="abuse.reporterAccount"></my-actor-avatar> 13 <my-actor-avatar size="18" [actor]="abuse.reporterAccount" actorType="account"></my-actor-avatar>
14 <div> 14 <div>
15 <span class="muted">{{ abuse.reporterAccount.nameWithHost }}</span> 15 <span class="muted">{{ abuse.reporterAccount.nameWithHost }}</span>
16 </div> 16 </div>
17 </a> 17 </a>
18 18
19 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" 19 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
20 class="ml-auto muted abuse-details-links" i18n 20 class="ms-auto muted abuse-details-links" i18n
21 > 21 >
22 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> 22 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}
23 <my-global-icon iconName="flag"></my-global-icon>
23 </a> 24 </a>
24 </span> 25 </span>
25 </div> 26 </div>
@@ -30,16 +31,17 @@
30 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }" 31 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
31 class="chip" 32 class="chip"
32 > 33 >
33 <my-actor-avatar size="18" [account]="abuse.flaggedAccount"></my-actor-avatar> 34 <my-actor-avatar size="18" [actor]="abuse.flaggedAccount" actorType="account"></my-actor-avatar>
34 <div> 35 <div>
35 <span class="muted">{{ abuse.flaggedAccount ? abuse.flaggedAccount.nameWithHost : '' }}</span> 36 <span class="muted">{{ abuse.flaggedAccount ? abuse.flaggedAccount.nameWithHost : '' }}</span>
36 </div> 37 </div>
37 </a> 38 </a>
38 39
39 <a *ngIf="isAdminView" [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }" 40 <a *ngIf="isAdminView" [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
40 class="ml-auto muted abuse-details-links" i18n 41 class="ms-auto muted abuse-details-links" i18n
41 > 42 >
42 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> 43 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}
44 <my-global-icon iconName="flag"></my-global-icon>
43 </a> 45 </a>
44 </span> 46 </span>
45 </div> 47 </div>
@@ -53,7 +55,7 @@
53 <div class="mt-3 d-flex"> 55 <div class="mt-3 d-flex">
54 <span class="moderation-expanded-label"> 56 <span class="moderation-expanded-label">
55 <ng-container i18n>Report</ng-container> 57 <ng-container i18n>Report</ng-container>
56 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 muted">#{{ abuse.id }}</a> 58 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ms-1 muted">#{{ abuse.id }}</a>
57 </span> 59 </span>
58 <span class="moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span> 60 <span class="moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
59 </div> 61 </div>
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.scss b/client/src/app/shared/shared-abuse-list/abuse-details.component.scss
index 37bf4cc56..bd43ed459 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-details.component.scss
+++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.scss
@@ -15,3 +15,7 @@
15.abuse-details-links { 15.abuse-details-links {
16 @include disable-default-a-behaviour; 16 @include disable-default-a-behaviour;
17} 17}
18
19my-global-icon[iconName=flag] {
20 width: 15px;
21}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
index f0a27c6e2..5f9db2b3b 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
@@ -8,7 +8,7 @@
8> 8>
9 <ng-template pTemplate="caption"> 9 <ng-template pTemplate="caption">
10 <div class="caption"> 10 <div class="caption">
11 <div class="ml-auto"> 11 <div class="ms-auto">
12 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> 12 <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
13 </div> 13 </div>
14 </div> 14 </div>
@@ -43,7 +43,7 @@
43 <td *ngIf="isAdminView()"> 43 <td *ngIf="isAdminView()">
44 <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> 44 <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
45 <div class="chip two-lines"> 45 <div class="chip two-lines">
46 <my-actor-avatar [account]="abuse.reporterAccount" size="32"></my-actor-avatar> 46 <my-actor-avatar [actor]="abuse.reporterAccount" actorType="account" size="32"></my-actor-avatar>
47 <div> 47 <div>
48 {{ abuse.reporterAccount.displayName }} 48 {{ abuse.reporterAccount.displayName }}
49 <span>{{ abuse.reporterAccount.nameWithHost }}</span> 49 <span>{{ abuse.reporterAccount.nameWithHost }}</span>
@@ -70,7 +70,7 @@
70 </span> 70 </span>
71 71
72 <span name> 72 <span name>
73 <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span> 73 <my-global-icon *ngIf="abuse.video.blacklisted" iconName="no" i18n-title title="The video was blocked"></my-global-icon>
74 </span> 74 </span>
75 </my-video-cell> 75 </my-video-cell>
76 </td> 76 </td>
@@ -80,7 +80,7 @@
80 <div class="table-video-text"> 80 <div class="table-video-text">
81 <div> 81 <div>
82 {{ abuse.video.name }} 82 {{ abuse.video.name }}
83 <span class="glyphicon glyphicon-trash"></span> 83 <my-global-icon iconName="delete"></my-global-icon>
84 </div> 84 </div>
85 <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div> 85 <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
86 </div> 86 </div>
@@ -116,8 +116,8 @@
116 <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td> 116 <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
117 117
118 <td class="c-hand abuse-states" [pRowToggler]="abuse"> 118 <td class="c-hand abuse-states" [pRowToggler]="abuse">
119 <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span> 119 <my-global-icon *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" iconName="tick"></my-global-icon>
120 <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span> 120 <my-global-icon *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" iconName="cross"></my-global-icon>
121 </td> 121 </td>
122 122
123 <td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)"> 123 <td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)">
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.scss b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.scss
index 2d8acae58..4852f2e8b 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.scss
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.scss
@@ -7,10 +7,6 @@
7 color: var(--greyForegroundColor); 7 color: var(--greyForegroundColor);
8} 8}
9 9
10.abuse-states .glyphicon-comment {
11 @include margin-left(0.5rem);
12}
13
14.abuse-messages { 10.abuse-messages {
15 my-global-icon { 11 my-global-icon {
16 @include margin-left(3px); 12 @include margin-left(3px);
@@ -20,3 +16,10 @@
20 top: -2px; 16 top: -2px;
21 } 17 }
22} 18}
19
20.table-video-text my-global-icon,
21my-video-cell my-global-icon {
22 width: 15px;
23 position: relative;
24 top: -2px;
25}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss
index d77e44a9d..3b43a4a4d 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss
@@ -25,7 +25,6 @@ textarea {
25 25
26.no-messages { 26.no-messages {
27 display: flex; 27 display: flex;
28 font-size: 15px;
29 justify-content: center; 28 justify-content: center;
30} 29}
31 30
@@ -45,10 +44,6 @@ textarea {
45 color: var(--mainForegroundColor); 44 color: var(--mainForegroundColor);
46 background-color: var(--greyBackgroundColor); 45 background-color: var(--greyBackgroundColor);
47 46
48 .content {
49 font-size: 15px;
50 }
51
52 .date { 47 .date {
53 font-size: 13px; 48 font-size: 13px;
54 color: var(--greyForegroundColor); 49 color: var(--greyForegroundColor);
diff --git a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html
index e9c5fadcf..6459c5ffe 100644
--- a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html
+++ b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.html
@@ -1,12 +1,12 @@
1<div class="actor" *ngIf="actor"> 1<div class="actor" *ngIf="actor">
2 <div class="d-flex"> 2 <div class="d-flex">
3 <my-actor-avatar [channel]="getChannel()" [account]="getAccount()" [previewImage]="preview" size="100"></my-actor-avatar> 3 <my-actor-avatar [actor]="actor" [actorType]="getActorType()" [previewImage]="preview" size="100"></my-actor-avatar>
4 4
5 <div class="actor-img-edit-container"> 5 <div class="actor-img-edit-container">
6 6
7 <div *ngIf="editable && !hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body"> 7 <div *ngIf="editable && !hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body">
8 <my-global-icon iconName="upload"></my-global-icon> 8 <my-global-icon iconName="upload"></my-global-icon>
9 <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label> 9 <label class="visually-hidden" for="avatarfile" i18n>Upload a new avatar</label>
10 <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/> 10 <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
11 </div> 11 </div>
12 12
@@ -15,7 +15,7 @@
15 #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right" 15 #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right"
16 > 16 >
17 <my-global-icon iconName="edit"></my-global-icon> 17 <my-global-icon iconName="edit"></my-global-icon>
18 <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label> 18 <label class="visually-hidden" for="avatarMenu" i18n>Change your avatar</label>
19 </div> 19 </div>
20 20
21 </div> 21 </div>
diff --git a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.scss b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.scss
index 923f0029b..fd8cd7ffc 100644
--- a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.scss
+++ b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.scss
@@ -3,37 +3,36 @@
3 3
4.actor { 4.actor {
5 display: flex; 5 display: flex;
6}
6 7
7 my-actor-avatar { 8my-actor-avatar {
8 @include margin-right(15px); 9 @include margin-right(15px);
9 } 10}
10 11
11 .actor-info { 12.actor-info {
12 display: inline-flex; 13 display: inline-flex;
13 flex-direction: column; 14 flex-direction: column;
15}
14 16
15 .actor-info-display-name { 17.actor-info-display-name {
16 @include peertube-word-wrap; 18 @include peertube-word-wrap;
17 19
18 font-size: 20px; 20 font-size: 20px;
19 font-weight: $font-bold; 21 font-weight: $font-bold;
20 22
21 @media screen and (max-width: $small-view) { 23 @media screen and (max-width: $small-view) {
22 font-size: 16px; 24 font-size: 16px;
23 } 25 }
24 } 26}
25 27
26 .actor-info-username { 28.actor-info-username {
27 position: relative; 29 position: relative;
28 font-size: 14px; 30 font-size: 14px;
29 color: pvar(--greyForegroundColor); 31 color: pvar(--greyForegroundColor);
30 } 32}
31 33
32 .actor-info-followers { 34.actor-info-followers {
33 font-size: 15px; 35 padding-bottom: .5rem;
34 padding-bottom: .5rem;
35 }
36 }
37} 36}
38 37
39.actor-img-edit-container { 38.actor-img-edit-container {
@@ -46,3 +45,7 @@
46 right: 45px; 45 right: 45px;
47 border-radius: 50%; 46 border-radius: 50%;
48} 47}
48
49.dropdown-item {
50 @include dropdown-with-icon-item;
51}
diff --git a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts
index 01bb401fb..b71a3c485 100644
--- a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts
+++ b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts
@@ -75,19 +75,9 @@ export class ActorAvatarEditComponent implements OnInit {
75 return !!this.preview || this.actor.avatars.length !== 0 75 return !!this.preview || this.actor.avatars.length !== 0
76 } 76 }
77 77
78 isChannel () { 78 getActorType () {
79 return !!(this.actor as VideoChannel).ownerAccount 79 if ((this.actor as VideoChannel).ownerAccount) return 'channel'
80 }
81
82 getChannel (): VideoChannel {
83 if (this.isChannel()) return this.actor as VideoChannel
84
85 return undefined
86 }
87
88 getAccount (): Account {
89 if (this.isChannel()) return undefined
90 80
91 return this.actor as Account 81 return 'account'
92 } 82 }
93} 83}
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar.component.html b/client/src/app/shared/shared-actor-image/actor-avatar.component.html
index c285b6cc3..fb9efc20a 100644
--- a/client/src/app/shared/shared-actor-image/actor-avatar.component.html
+++ b/client/src/app/shared/shared-actor-image/actor-avatar.component.html
@@ -1,19 +1,21 @@
1<ng-template #img> 1<ng-template #img>
2 <img *ngIf="previewImage || avatarUrl || !initial" [class]="getClass('avatar')" [src]="previewImage || avatarUrl || defaultAvatarUrl" [alt]="alt" /> 2 <img *ngIf="displayImage()" [class]="classes" [src]="previewImage || avatarUrl || defaultAvatarUrl" [alt]="alt" />
3 3
4 <div *ngIf="!avatarUrl && initial" [ngClass]="getClass('initial')"> 4 <div *ngIf="displayActorInitial()" [ngClass]="classes">
5 <span>{{ initial }}</span> 5 <span>{{ getActorInitial() }}</span>
6 </div> 6 </div>
7
8 <div *ngIf="displayPlaceholder()" [ngClass]="classes"></div>
7</ng-template> 9</ng-template>
8 10
9<a *ngIf="hasActor() && href" [href]="href" target="_blank" rel="noopener noreferrer" [title]="title"> 11<a *ngIf="actor && href" [href]="href" target="_blank" rel="noopener noreferrer" [title]="title">
10 <ng-template *ngTemplateOutlet="img"></ng-template> 12 <ng-template *ngTemplateOutlet="img"></ng-template>
11</a> 13</a>
12 14
13<a *ngIf="hasActor() && internalHref" [routerLink]="internalHref" [title]="title"> 15<a *ngIf="actor && internalHref" [routerLink]="internalHref" [title]="title">
14 <ng-template *ngTemplateOutlet="img"></ng-template> 16 <ng-template *ngTemplateOutlet="img"></ng-template>
15</a> 17</a>
16 18
17<ng-container *ngIf="!hasActor() || (!href && !internalHref)"> 19<ng-container *ngIf="!actor || (!href && !internalHref)">
18 <ng-template *ngTemplateOutlet="img"></ng-template> 20 <ng-template *ngTemplateOutlet="img"></ng-template>
19</ng-container> 21</ng-container>
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar.component.ts
index 600984aa2..437de1d50 100644
--- a/client/src/app/shared/shared-actor-image/actor-avatar.component.ts
+++ b/client/src/app/shared/shared-actor-image/actor-avatar.component.ts
@@ -1,4 +1,4 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input, OnChanges } from '@angular/core'
2import { VideoChannel } from '../shared-main' 2import { VideoChannel } from '../shared-main'
3import { Account } from '../shared-main/account/account.model' 3import { Account } from '../shared-main/account/account.model'
4 4
@@ -15,11 +15,11 @@ export type ActorAvatarSize = '18' | '25' | '28' | '32' | '34' | '35' | '36' | '
15 styleUrls: [ './actor-avatar.component.scss' ], 15 styleUrls: [ './actor-avatar.component.scss' ],
16 templateUrl: './actor-avatar.component.html' 16 templateUrl: './actor-avatar.component.html'
17}) 17})
18export class ActorAvatarComponent { 18export class ActorAvatarComponent implements OnChanges {
19 private _title: string 19 private _title: string
20 20
21 @Input() account: ActorInput 21 @Input() actor: ActorInput
22 @Input() channel: ActorInput 22 @Input() actorType: 'channel' | 'account' | 'unlogged'
23 23
24 @Input() previewImage: string 24 @Input() previewImage: string
25 25
@@ -36,56 +36,84 @@ export class ActorAvatarComponent {
36 36
37 get title () { 37 get title () {
38 if (this._title) return this._title 38 if (this._title) return this._title
39 if (this.account) return $localize`${this.account.name} (account page)` 39 if (this.isAccount()) return $localize`${this.actor.name} (account page)`
40 if (this.channel) return $localize`${this.channel.name} (channel page)` 40 if (this.isChannel()) return $localize`${this.actor.name} (channel page)`
41 41
42 return '' 42 return ''
43 } 43 }
44 44
45 classes: string[] = []
46
45 get alt () { 47 get alt () {
46 if (this.account) return $localize`Account avatar` 48 if (this.isAccount()) return $localize`Account avatar`
47 if (this.channel) return $localize`Channel avatar` 49 if (this.isChannel()) return $localize`Channel avatar`
48 50
49 return '' 51 return ''
50 } 52 }
51 53
52 get defaultAvatarUrl () { 54 get defaultAvatarUrl () {
53 if (this.account) return Account.GET_DEFAULT_AVATAR_URL(this.getSizeNumber()) 55 if (this.isChannel()) return VideoChannel.GET_DEFAULT_AVATAR_URL(this.getSizeNumber())
54 if (this.channel) return VideoChannel.GET_DEFAULT_AVATAR_URL(this.getSizeNumber()) 56
57 // account or unlogged
58 return Account.GET_DEFAULT_AVATAR_URL(this.getSizeNumber())
55 } 59 }
56 60
57 get avatarUrl () { 61 get avatarUrl () {
58 if (this.account) return Account.GET_ACTOR_AVATAR_URL(this.account, this.getSizeNumber()) 62 if (!this.actor) return ''
59 if (this.channel) return VideoChannel.GET_ACTOR_AVATAR_URL(this.channel, this.getSizeNumber()) 63
64 if (this.isAccount()) return Account.GET_ACTOR_AVATAR_URL(this.actor, this.getSizeNumber())
65 if (this.isChannel()) return VideoChannel.GET_ACTOR_AVATAR_URL(this.actor, this.getSizeNumber())
60 66
61 return '' 67 return ''
62 } 68 }
63 69
64 get initial () { 70 ngOnChanges () {
65 const name = this.account?.name 71 this.classes = [ 'avatar' ]
66 if (!name) return ''
67 72
68 return name.slice(0, 1) 73 if (this.size) {
74 this.classes.push(`avatar-${this.size}`)
75 }
76
77 if (this.isChannel()) {
78 this.classes.push('channel')
79 } else {
80 this.classes.push('account')
81 }
82
83 // No avatar, use actor name initial
84 if (this.displayActorInitial()) {
85 this.classes.push('initial')
86 this.classes.push(this.getColorTheme())
87 }
69 } 88 }
70 89
71 getClass (type: 'avatar' | 'initial') { 90 displayImage () {
72 const base = [ 'avatar' ] 91 if (this.actorType === 'unlogged') return true
73 92
74 if (this.size) base.push(`avatar-${this.size}`) 93 return !!(this.actor && this.avatarUrl)
94 }
75 95
76 if (this.channel) base.push('channel') 96 displayActorInitial () {
77 else base.push('account') 97 return this.actor && !this.avatarUrl
98 }
78 99
79 if (type === 'initial' && this.initial) { 100 displayPlaceholder () {
80 base.push('initial') 101 return this.actorType !== 'unlogged' && !this.actor
81 base.push(this.getColorTheme()) 102 }
82 } 103
104 getActorInitial () {
105 const name = this.actor?.name
106 if (!name) return ''
107
108 return name.slice(0, 1)
109 }
83 110
84 return base 111 private isAccount () {
112 return this.actorType === 'account'
85 } 113 }
86 114
87 hasActor () { 115 private isChannel () {
88 return !!this.account || !!this.channel 116 return this.actorType === 'channel'
89 } 117 }
90 118
91 private getSizeNumber () { 119 private getSizeNumber () {
@@ -95,7 +123,7 @@ export class ActorAvatarComponent {
95 } 123 }
96 124
97 private getColorTheme () { 125 private getColorTheme () {
98 const initialLowercase = this.initial.toLowerCase() 126 const initialLowercase = this.getActorInitial().toLowerCase()
99 127
100 // Keep consistency with CSS 128 // Keep consistency with CSS
101 const themes = { 129 const themes = {
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html
index 52a402329..395cc8562 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html
@@ -1,7 +1,7 @@
1<div *ngIf="channel" class="channel"> 1<div *ngIf="channel" class="channel">
2 2
3 <div class="channel-avatar-row"> 3 <div class="channel-avatar-row">
4 <my-actor-avatar [channel]="channel" [internalHref]="getVideoChannelLink()" i18n-title title="See this video channel" size="75"></my-actor-avatar> 4 <my-actor-avatar [actor]="channel" actorType="channel" [internalHref]="getVideoChannelLink()" i18n-title title="See this video channel" size="75"></my-actor-avatar>
5 5
6 <h6> 6 <h6>
7 <a [routerLink]="getVideoChannelLink()" i18n-title title="See this video channel"> 7 <a [routerLink]="getVideoChannelLink()" i18n-title title="See this video channel">
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
index 0e4d5fb12..7d3498d4c 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
@@ -7,7 +7,7 @@ import { MiniatureDisplayOptions } from '../../shared-video-miniature'
7import { CustomMarkupComponent } from './shared' 7import { CustomMarkupComponent } from './shared'
8 8
9/* 9/*
10 * Markup component list videos depending on criterias 10 * Markup component list videos depending on criteria
11*/ 11*/
12 12
13@Component({ 13@Component({
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.html b/client/src/app/shared/shared-forms/advanced-input-filter.component.html
index 7031cb53b..e4f318385 100644
--- a/client/src/app/shared/shared-forms/advanced-input-filter.component.html
+++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.html
@@ -1,6 +1,7 @@
1<div class="input-group has-feedback has-clear"> 1<div class="input-group has-clear" ngbDropdown placement="bottom-left auto" container="body">
2 <div *ngIf="hasFilters()" class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body"> 2
3 <div class="input-group-text" ngbDropdownToggle> 3 <ng-container *ngIf="hasFilters()">
4 <div class="input-group-text c-hand" ngbDropdownToggle>
4 <span class="caret" aria-haspopup="menu" role="button"></span> 5 <span class="caret" aria-haspopup="menu" role="button"></span>
5 </div> 6 </div>
6 7
@@ -15,14 +16,14 @@
15 </button> 16 </button>
16 </ng-container> 17 </ng-container>
17 </div> 18 </div>
18 </div> 19 </ng-container>
19 20
20 <input 21 <input
21 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." 22 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
23 class="last-in-group"
22 [(ngModel)]="searchValue" 24 [(ngModel)]="searchValue"
23 (keyup)="onInputSearch($event)" 25 (keyup)="onInputSearch($event)"
24 > 26 >
25 27
26 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="onResetTableFilter()"></a> 28 <my-global-icon iconName="cross" role="button" class="form-control-clear" title="Clear filter" i18n-title (click)="onResetTableFilter()"></my-global-icon>
27 <span class="sr-only" i18n>Clear filters</span>
28</div> 29</div>
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
index ee1b3b508..ed85385fd 100644
--- a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
+++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
@@ -23,7 +23,6 @@ my-global-icon {
23 width: 18px; 23 width: 18px;
24 height: 18px; 24 height: 18px;
25 25
26
27 &.no-visible { 26 &.no-visible {
28 @include margin-right($size + $margin); 27 @include margin-right($size + $margin);
29 28
diff --git a/client/src/app/shared/shared-forms/dynamic-form-field.component.html b/client/src/app/shared/shared-forms/dynamic-form-field.component.html
index 2ef61ecfc..2dd6cf4ad 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
@@ -18,7 +18,7 @@
18 </select> 18 </select>
19 </div> 19 </div>
20 20
21 <my-input-toggle-hidden *ngIf="setting.type === 'input-password'" [formControlName]="setting.name" [inputId]="setting.name"></my-input-toggle-hidden> 21 <my-input-text *ngIf="setting.type === 'input-password'" [formError]="formErrors['settings.name']" [formControlName]="setting.name" [inputId]="setting.name"></my-input-text>
22 22
23 <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>
24 24
@@ -28,19 +28,19 @@
28 28
29 <my-markdown-textarea 29 <my-markdown-textarea
30 *ngIf="setting.type === 'markdown-text'" 30 *ngIf="setting.type === 'markdown-text'"
31 markdownType="text" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px" 31 markdownType="text" [id]="setting.name" [formControlName]="setting.name"
32 [classes]="{ 'input-error': formErrors['settings.name'] }" 32 [formError]="formErrors['settings.name']"
33 ></my-markdown-textarea> 33 ></my-markdown-textarea>
34 34
35 <my-markdown-textarea 35 <my-markdown-textarea
36 *ngIf="setting.type === 'markdown-enhanced'" 36 *ngIf="setting.type === 'markdown-enhanced'"
37 markdownType="enhanced" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px" 37 markdownType="enhanced" [id]="setting.name" [formControlName]="setting.name"
38 [classes]="{ 'input-error': formErrors['settings.name'] }" 38 [formError]="formErrors['settings.name']"
39 ></my-markdown-textarea> 39 ></my-markdown-textarea>
40 40
41 <div *ngIf="setting.type === 'html'" [innerHTML]="setting.html"></div> 41 <div *ngIf="setting.type === 'html'" [innerHTML]="setting.html"></div>
42 42
43 <div *ngIf="formErrors[setting.name]" class="form-error"> 43 <div *ngIf="hasDedicatedFormError() && formErrors[setting.name]" class="form-error">
44 {{ formErrors[setting.name] }} 44 {{ formErrors[setting.name] }}
45 </div> 45 </div>
46 46
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 31bf59edb..0737f372a 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
@@ -26,3 +26,8 @@ textarea {
26my-peertube-checkbox + .label-small-info { 26my-peertube-checkbox + .label-small-info {
27 margin-top: 5px; 27 margin-top: 5px;
28} 28}
29
30my-markdown-textarea {
31 display: block;
32 max-width: 500px;
33}
diff --git a/client/src/app/shared/shared-forms/dynamic-form-field.component.ts b/client/src/app/shared/shared-forms/dynamic-form-field.component.ts
index b63890797..e1a1f8034 100644
--- a/client/src/app/shared/shared-forms/dynamic-form-field.component.ts
+++ b/client/src/app/shared/shared-forms/dynamic-form-field.component.ts
@@ -12,4 +12,15 @@ export class DynamicFormFieldComponent {
12 @Input() form: FormGroup 12 @Input() form: FormGroup
13 @Input() formErrors: any 13 @Input() formErrors: any
14 @Input() setting: RegisterClientFormFieldOptions 14 @Input() setting: RegisterClientFormFieldOptions
15
16 hasDedicatedFormError () {
17 const dedicated = new Set<RegisterClientFormFieldOptions['type']>([
18 'input-checkbox',
19 'input',
20 'select',
21 'input-textarea'
22 ])
23
24 return dedicated.has(this.setting.type)
25 }
15} 26}
diff --git a/client/src/app/shared/shared-forms/index.ts b/client/src/app/shared/shared-forms/index.ts
index 727416a40..495785e7b 100644
--- a/client/src/app/shared/shared-forms/index.ts
+++ b/client/src/app/shared/shared-forms/index.ts
@@ -3,7 +3,7 @@ export * from './form-reactive'
3export * from './form-validator.service' 3export * from './form-validator.service'
4export * from './form-validator.service' 4export * from './form-validator.service'
5export * from './input-switch.component' 5export * from './input-switch.component'
6export * from './input-toggle-hidden.component' 6export * from './input-text.component'
7export * from './markdown-textarea.component' 7export * from './markdown-textarea.component'
8export * from './peertube-checkbox.component' 8export * from './peertube-checkbox.component'
9export * from './preview-upload.component' 9export * from './preview-upload.component'
diff --git a/client/src/app/shared/shared-forms/input-switch.component.html b/client/src/app/shared/shared-forms/input-switch.component.html
index ce1dee470..9a466055a 100644
--- a/client/src/app/shared/shared-forms/input-switch.component.html
+++ b/client/src/app/shared/shared-forms/input-switch.component.html
@@ -1,4 +1,4 @@
1<div (click)="update()"> 1<div (click)="update()">
2 <input type="checkbox" [checked]="checked"/> 2 <input type="checkbox" [checked]="checked"/>
3 <label class="ml-auto">Toggle</label> 3 <label class="ms-auto">Toggle</label>
4</div> 4</div>
diff --git a/client/src/app/shared/shared-forms/input-text.component.html b/client/src/app/shared/shared-forms/input-text.component.html
new file mode 100644
index 000000000..abb53a085
--- /dev/null
+++ b/client/src/app/shared/shared-forms/input-text.component.html
@@ -0,0 +1,23 @@
1<div class="input-group">
2 <input
3 [id]="inputId" [autocomplete]="autocomplete" [value]="value" [placeholder]="placeholder" [tabindex]="tabindex"
4 [(ngModel)]="value" (ngModelChange)="update()" [readonly]="readonly"
5 #input (click)="input.select()" (input)="update()" (change)="update()" [type]="inputType" class="form-control"
6 [ngClass]="{ 'input-error': formError }"
7 />
8
9 <button *ngIf="withToggle" (click)="toggle()" type="button" class="btn btn-outline-secondary" [title]="toggleTitle">
10 <my-global-icon *ngIf="show" iconName="eye-open"></my-global-icon>
11 <my-global-icon *ngIf="!show" iconName="eye-close"></my-global-icon>
12 </button>
13
14 <button
15 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button"
16 class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy"
17 >
18 <my-global-icon iconName="copy"></my-global-icon>
19 <span class="copy-text">Copy</span>
20 </button>
21</div>
22
23<div *ngIf="formError" class="form-error">{{ formError }}</div>
diff --git a/client/src/app/shared/shared-forms/input-toggle-hidden.component.scss b/client/src/app/shared/shared-forms/input-text.component.scss
index ef4236ebc..ae8bf5fec 100644
--- a/client/src/app/shared/shared-forms/input-toggle-hidden.component.scss
+++ b/client/src/app/shared/shared-forms/input-text.component.scss
@@ -5,11 +5,14 @@ input {
5 @include peertube-input-text(auto); 5 @include peertube-input-text(auto);
6 @include padding-left(15px !important); 6 @include padding-left(15px !important);
7 @include padding-right(15px !important); 7 @include padding-right(15px !important);
8}
8 9
9 // set again properties of peertube-input-text that are overriden by .input-group 10.btn {
10 font-size: 15px !important; 11 @include button-with-icon(18px);
11} 12}
12 13
13.eye-button { 14.copy-text {
14 line-height: 1 !important; 15 font-size: 14px;
16 margin-left: 5px;
17 vertical-align: top;
15} 18}
diff --git a/client/src/app/shared/shared-forms/input-toggle-hidden.component.ts b/client/src/app/shared/shared-forms/input-text.component.ts
index e03353fe1..d667ed663 100644
--- a/client/src/app/shared/shared-forms/input-toggle-hidden.component.ts
+++ b/client/src/app/shared/shared-forms/input-text.component.ts
@@ -3,18 +3,18 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4 4
5@Component({ 5@Component({
6 selector: 'my-input-toggle-hidden', 6 selector: 'my-input-text',
7 templateUrl: './input-toggle-hidden.component.html', 7 templateUrl: './input-text.component.html',
8 styleUrls: [ './input-toggle-hidden.component.scss' ], 8 styleUrls: [ './input-text.component.scss' ],
9 providers: [ 9 providers: [
10 { 10 {
11 provide: NG_VALUE_ACCESSOR, 11 provide: NG_VALUE_ACCESSOR,
12 useExisting: forwardRef(() => InputToggleHiddenComponent), 12 useExisting: forwardRef(() => InputTextComponent),
13 multi: true 13 multi: true
14 } 14 }
15 ] 15 ]
16}) 16})
17export class InputToggleHiddenComponent implements ControlValueAccessor { 17export class InputTextComponent implements ControlValueAccessor {
18 @Input() inputId = Math.random().toString(11).slice(2, 8) // id cannot be left empty or undefined 18 @Input() inputId = Math.random().toString(11).slice(2, 8) // id cannot be left empty or undefined
19 @Input() value = '' 19 @Input() value = ''
20 @Input() autocomplete = 'off' 20 @Input() autocomplete = 'off'
@@ -24,6 +24,7 @@ export class InputToggleHiddenComponent implements ControlValueAccessor {
24 @Input() withCopy = false 24 @Input() withCopy = false
25 @Input() readonly = false 25 @Input() readonly = false
26 @Input() show = false 26 @Input() show = false
27 @Input() formError: string
27 28
28 constructor (private notifier: Notifier) { } 29 constructor (private notifier: Notifier) { }
29 30
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
deleted file mode 100644
index dfe646d2f..000000000
--- a/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
+++ /dev/null
@@ -1,21 +0,0 @@
1<div class="input-group input-group-sm">
2 <input
3 [id]="inputId" [autocomplete]="autocomplete" [value]="value" [placeholder]="placeholder" [tabindex]="tabindex"
4 [(ngModel)]="value" (ngModelChange)="update()" [readonly]="readonly"
5 #input (click)="input.select()" (input)="update()" (change)="update()" [type]="inputType" class="form-control"
6 />
7
8 <div *ngIf="withToggle || withCopy" class="input-group-append">
9 <button *ngIf="withToggle" (click)="toggle()" type="button" class="btn btn-outline-secondary eye-button" [title]="toggleTitle">
10 <span class="glyphicon glyphicon-eye-{{ show ? 'open' : 'close' }}"></span>
11 </button>
12
13 <button
14 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button"
15 class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy"
16 >
17 <span class="glyphicon glyphicon-duplicate"></span>
18 Copy
19 </button>
20 </div>
21</div>
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.html b/client/src/app/shared/shared-forms/markdown-textarea.component.html
index 7c2a42791..5a9ff1a15 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.html
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.html
@@ -1,9 +1,9 @@
1<div class="root" [ngClass]="{ 'maximized': isMaximized }" [ngStyle]="{ 'max-width': textareaMaxWidth }"> 1<div class="root" [ngClass]="{ 'maximized': isMaximized }">
2
2 <textarea #textarea 3 <textarea #textarea
3 [(ngModel)]="content" (ngModelChange)="onModelChange()" 4 [(ngModel)]="content" (ngModelChange)="onModelChange()"
4 class="form-control" [ngClass]="classes" 5 class="form-control" [ngClass]="{ 'input-error': formError }"
5 [attr.disabled]="disabled || null" 6 [attr.disabled]="disabled || null"
6 [ngStyle]="{ height: textareaHeight }"
7 [id]="name" [name]="name"> 7 [id]="name" [name]="name">
8 </textarea> 8 </textarea>
9 9
@@ -25,14 +25,18 @@
25 </ng-template> 25 </ng-template>
26 </ng-container> 26 </ng-container>
27 27
28 <my-button 28 <my-global-icon
29 *ngIf="!isMaximized" [title]="maximizeInText" className="maximize-button" icon="fullscreen" (click)="onMaximizeClick()" [disabled]="disabled" 29 *ngIf="!isMaximized" role="button" [ngbTooltip]="maximizeInText"
30 ></my-button> 30 class="maximize-button" iconName="fullscreen" (click)="onMaximizeClick()" [ngClass]="{ disabled: disabled }"
31 ></my-global-icon>
31 32
32 <my-button 33 <my-global-icon
33 *ngIf="isMaximized" [title]="maximizeOutText" className="maximize-button" icon="exit-fullscreen" (click)="onMaximizeClick()" [disabled]="disabled" 34 *ngIf="isMaximized" role="button" [ngbTooltip]="maximizeOutText"
34 ></my-button> 35 class="maximize-button" iconName="exit-fullscreen" (click)="onMaximizeClick()" [ngClass]="{ disabled: disabled }"
36 ></my-global-icon>
35 </div> 37 </div>
36 38
37 <div [ngbNavOutlet]="nav"></div> 39 <div [ngbNavOutlet]="nav"></div>
40
41 <div *ngIf="!isMaximized && formError" class="form-error">{{ formError }}</div>
38</div> 42</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 5939bb999..c68c2c241 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.scss
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.scss
@@ -6,261 +6,169 @@ $nav-preview-tab-height: 30px;
6$base-padding: 15px; 6$base-padding: 15px;
7$input-border-radius: 3px; 7$input-border-radius: 3px;
8 8
9@mixin in-small-view { 9.root {
10 .root { 10 display: flex;
11 display: flex; 11 flex-direction: column;
12 flex-direction: column;
13 12
14 textarea { 13 textarea {
15 @include peertube-textarea(100%, 150px); 14 @include peertube-textarea(100%, 150px);
16 15
17 background-color: pvar(--markdownTextareaBackgroundColor); 16 background-color: pvar(--markdownTextareaBackgroundColor);
18 17
19 font-family: monospace; 18 font-family: monospace;
20 font-size: 13px; 19 font-size: 13px;
21 border-bottom: 0; 20 border-bottom: 0;
22 border-bottom-left-radius: unset; 21 border-bottom-left-radius: 0;
23 border-bottom-right-radius: unset; 22 border-bottom-right-radius: 0;
24 } 23 }
25 24
26 .nav-preview { 25 .nav-preview {
27 @include padding-left(10px); 26 padding: 10px;
28 @include padding-right(10px);
29 27
30 display: block; 28 border: 1px solid $input-border-color;
31 text-align: end; 29 border-top: 1px dashed $input-border-color;
32 padding-top: 10px;
33 padding-bottom: 10px;
34 border-top: 1px dashed $input-border-color;
35 border-left: 1px solid $input-border-color;
36 border-right: 1px solid $input-border-color;
37 border-bottom: 1px solid $input-border-color;
38 border-bottom-right-radius: $input-border-radius;
39
40 border-bottom-left-radius: $input-border-radius;
41 ::ng-deep {
42 .nav-link {
43 display: none !important;
44 }
45 30
46 .maximize-button { 31 border-bottom-right-radius: $input-border-radius;
47 padding: 0 7px; 32 border-bottom-left-radius: $input-border-radius;
48 cursor: pointer;
49
50 svg {
51 stroke: var(--mainForegroundColor);
52 opacity: 0.6;
53 }
54
55 &:hover,
56 &:active {
57 svg {
58 opacity: 1;
59 }
60 }
61 }
62 }
63 }
64 33
65 ::ng-deep { 34 display: flex;
66 .tab-content { 35 flex-grow: 1;
67 display: none; 36 border-bottom-left-radius: unset;
68 } 37 border-bottom-right-radius: unset;
69 } 38 border-bottom: 2px solid pvar(--mainColor);
70 }
71}
72 39
73@mixin nav-preview-medium { 40 .maximize-button {
74 display: flex; 41 @include margin-left(15px);
75 flex-grow: 1;
76 border-bottom-left-radius: unset;
77 border-bottom-right-radius: unset;
78 border-bottom: 2px solid pvar(--mainColor);
79 42
80 :first-child { 43 opacity: 0.6;
81 @include margin-left(auto); 44 cursor: default;
82 }
83 45
84 ::ng-deep { 46 &:not(.disabled) {
85 .nav-link { 47 cursor: pointer;
86 display: flex !important;
87 align-items: center;
88 height: $nav-preview-tab-height !important;
89 padding: 0 15px !important;
90 font-size: 85% !important;
91 opacity: .7;
92 }
93 48
94 .maximize-button { 49 &:hover,
95 @include margin-left(5px); 50 &:active {
51 opacity: 1;
52 }
53 }
96 } 54 }
97 } 55 }
98}
99 56
100@mixin content-preview-base { 57 .nav-pills {
101 display: block; 58 display: flex;
102 min-height: 75px; 59 align-items: center;
103 padding: $base-padding; 60 justify-content: flex-end;
104 overflow-y: auto;
105 font-size: 15px;
106 word-wrap: break-word;
107}
108 61
109@mixin maximized-base { 62 > * {
110 $nav-preview-vertical-padding: 40px; 63 font-size: 0.9em !important;
64 }
65 }
111 66
112 flex-direction: row; 67 .tab-content {
113 z-index: #{z(header) - 1}; 68 min-height: 75px;
114 position: fixed; 69 max-height: 210px;
115 top: $header-height; 70 padding: $base-padding;
116 left: $menu-width;
117 max-height: none !important;
118 max-width: none !important;
119 width: calc(100% - #{$menu-width});
120 height: calc(100vh - #{$header-height}) !important;
121 71
122 .nav-preview { 72 overflow-y: auto;
123 @include nav-preview-medium(); 73 word-wrap: break-word;
124 @include padding-right(0);
125 @include padding-left(0);
126 74
127 padding-top: math.div($nav-preview-vertical-padding, 2); 75 border: 1px solid $input-border-color;
128 padding-bottom: math.div($nav-preview-vertical-padding, 2);
129 position: absolute;
130 background-color: pvar(--mainBackgroundColor);
131 width: 100% !important;
132 border-top: 0; 76 border-top: 0;
133 border-left: 0;
134 border-right: 0;
135 77
136 :last-child { 78 border-bottom-left-radius: $input-border-radius;
137 @include margin-right(pvar(--horizontalMarginContent)); 79 border-bottom-right-radius: $input-border-radius;
138 }
139 } 80 }
140 81
141 ::ng-deep .tab-content { 82 &.maximized {
142 @include content-preview-base(); 83 z-index: #{z(header) - 1};
143 background-color: pvar(--mainBackgroundColor); 84 position: fixed;
144 scrollbar-color: pvar(--actionButtonColor) pvar(--mainBackgroundColor); 85 top: $header-height;
145 } 86 left: $menu-width;
146 87
147 textarea,
148 ::ng-deep .tab-content {
149 max-height: none !important; 88 max-height: none !important;
150 max-width: none !important; 89 max-width: none !important;
151 margin-top: #{$nav-preview-tab-height + $nav-preview-vertical-padding} !important; 90 width: calc(100% - #{$menu-width});
152 height: calc(100vh - #{$header-height + $nav-preview-tab-height + $nav-preview-vertical-padding}) !important; 91 height: calc(100vh - #{$header-height});
153 width: 50% !important;
154 border: 0 !important;
155 border-radius: unset !important;
156 }
157
158 :host-context(.expanded) {
159 .root.maximized {
160 left: 0;
161 width: 100%;
162 }
163 }
164}
165 92
166@mixin maximized-in-small-view { 93 display: grid;
167 .root.maximized { 94 grid-template-rows: auto 1fr;
168 @include maximized-base(); 95 grid-template-columns: 1fr 1fr;
169 96
170 textarea { 97 background-color: pvar(--mainBackgroundColor);
171 display: none;
172 }
173
174 ::ng-deep .tab-content {
175 width: 100% !important;
176 }
177 }
178}
179 98
180@mixin maximized-tabs-in-mobile-view {
181 // Ellipsis on tabs for mobile view
182 .root.maximized {
183 .nav-preview { 99 .nav-preview {
184 ::ng-deep .nav-link { 100 grid-row: 1;
185 @include ellipsis(); 101 grid-column: 1 / 3;
186 @include margin-right(10px !important);
187 102
188 display: block !important; 103 border: 0;
189 max-width: 45% !important; 104 border-bottom: 2px solid pvar(--mainColor);
190 padding: 5px 0 !important;
191 text-align: center;
192 105
193 &:not(.active) { 106 padding: 20px 0;
194 max-width: 15% !important; 107 width: 100% !important;
195 }
196 108
197 &.active { 109 .maximize-button {
198 padding: 5px 15px !important; 110 @include margin-right(15px);
199 }
200 } 111 }
201 } 112 }
202 }
203}
204 113
205@mixin in-medium-view { 114 textarea {
206 .root { 115 grid-column: 1;
207 .nav-preview { 116
208 @include nav-preview-medium(); 117 border: 0;
118 border-right: 1px dashed $input-border-color;
119
120 resize: none;
209 } 121 }
210 122
211 ::ng-deep .tab-content { 123 ::ng-deep .tab-content {
212 @include content-preview-base(); 124 grid-column: 2;
213 max-height: 210px;
214 border-bottom: 1px solid $input-border-color;
215 border-left: 1px solid $input-border-color;
216 border-right: 1px solid $input-border-color;
217 border-bottom-left-radius: $input-border-radius;
218 border-bottom-right-radius: $input-border-radius;
219 }
220 }
221}
222 125
223@mixin maximized-in-medium-view { 126 border: 0;
224 .root.maximized {
225 @include maximized-base();
226 127
227 textarea {
228 display: block; 128 display: block;
229 padding: $base-padding; 129 overflow-y: auto;
230 border-right: 1px dashed $input-border-color !important; 130 word-wrap: break-word;
231 resize: none;
232 scrollbar-color: pvar(--actionButtonColor) pvar(--markdownTextareaBackgroundColor);
233 131
234 &:focus { 132 scrollbar-color: pvar(--actionButtonColor) pvar(--mainBackgroundColor);
235 box-shadow: none;
236 }
237 } 133 }
238 }
239}
240 134
241@include in-small-view(); 135 textarea,
242@include maximized-in-small-view(); 136 ::ng-deep .tab-content {
137 grid-row: 2;
243 138
244@media only screen and (max-width: $mobile-view) { 139 height: 100% !important;
245 @include maximized-tabs-in-mobile-view(); 140 max-height: none !important;
246} 141 border-radius: 0;
247 142
248@media only screen and (max-width: #{$mobile-view + $menu-width}) { 143 padding: 15px;
249 :host-context(.main-col:not(.expanded)) { 144 }
250 @include maximized-tabs-in-mobile-view(); 145
251 } 146 @media screen and (max-width: $mobile-view) {
252} 147 grid-template-rows: auto 45% 1fr;
148 grid-template-columns: 1fr;
149
150 .nav-preview {
151 grid-column: 1;
152 }
253 153
254@media only screen and (min-width: $small-view) { 154 textarea {
255 @include maximized-in-medium-view(); 155 grid-row: 2;
156 grid-column: 1;
157 border: 0;
158 border-bottom: 1px dashed $input-border-color;;
159 }
256 160
257 :host-context(.expanded) { 161 ::ng-deep .tab-content {
258 @include in-medium-view(); 162 grid-column: 1;
163 grid-row: 3;
164 }
165 }
259 } 166 }
260} 167}
261 168
262@media only screen and (min-width: #{$small-view + $menu-width}) { 169:host-context(.main-col.expanded) {
263 :host-context(.main-col:not(.expanded)) { 170 .root.maximized {
264 @include in-medium-view(); 171 left: 0;
172 width: 100%;
265 } 173 }
266} 174}
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
index dcb5d20da..089991884 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
@@ -24,10 +24,7 @@ import { Video } from '@shared/models'
24export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { 24export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
25 @Input() content = '' 25 @Input() content = ''
26 26
27 @Input() classes: string[] | { [klass: string]: any[] | any } = [] 27 @Input() formError: string
28
29 @Input() textareaMaxWidth = '100%'
30 @Input() textareaHeight = '150px'
31 28
32 @Input() truncate: number 29 @Input() truncate: number
33 30
@@ -93,6 +90,8 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
93 } 90 }
94 91
95 onMaximizeClick () { 92 onMaximizeClick () {
93 if (this.disabled) return
94
96 this.isMaximized = !this.isMaximized 95 this.isMaximized = !this.isMaximized
97 96
98 // Make sure textarea have the focus 97 // Make sure textarea have the focus
diff --git a/client/src/app/shared/shared-forms/peertube-checkbox.component.html b/client/src/app/shared/shared-forms/peertube-checkbox.component.html
index c679e1403..cd06e75ba 100644
--- a/client/src/app/shared/shared-forms/peertube-checkbox.component.html
+++ b/client/src/app/shared/shared-forms/peertube-checkbox.component.html
@@ -1,6 +1,6 @@
1<div class="root flex-column"> 1<div class="root flex-column">
2 <div class="d-flex"> 2 <div class="d-flex">
3 <label class="form-group-checkbox"> 3 <label>
4 <input 4 <input
5 type="checkbox" 5 type="checkbox"
6 [(ngModel)]="checked" 6 [(ngModel)]="checked"
@@ -33,10 +33,10 @@
33 <div *ngIf="recommended" class="recommended" i18n>Recommended</div> 33 <div *ngIf="recommended" class="recommended" i18n>Recommended</div>
34 </div> 34 </div>
35 35
36 <div class="ml-4 d-flex flex-column"> 36 <div class="ms-4 d-flex flex-column">
37 <small class="wrapper mt-2 muted"> 37 <div class="wrapper form-group-description">
38 <ng-content select="description"></ng-content> 38 <ng-content select="description"></ng-content>
39 </small> 39 </div>
40 40
41 <span class="wrapper mt-3"> 41 <span class="wrapper mt-3">
42 <ng-content select="extra"></ng-content> 42 <ng-content select="extra"></ng-content>
diff --git a/client/src/app/shared/shared-forms/peertube-checkbox.component.scss b/client/src/app/shared/shared-forms/peertube-checkbox.component.scss
index 4e384e7e0..ac9127677 100644
--- a/client/src/app/shared/shared-forms/peertube-checkbox.component.scss
+++ b/client/src/app/shared/shared-forms/peertube-checkbox.component.scss
@@ -4,14 +4,9 @@
4.root { 4.root {
5 display: flex; 5 display: flex;
6 6
7 .form-group-checkbox { 7 label {
8 display: flex; 8 display: flex;
9 align-items: center; 9 font-size: $form-input-font-size;
10
11 .label-text {
12 font-weight: $font-regular;
13 margin: 0;
14 }
15 10
16 input { 11 input {
17 @include peertube-checkbox(1px); 12 @include peertube-checkbox(1px);
@@ -23,12 +18,11 @@
23 } 18 }
24 19
25 my-help { 20 my-help {
26 position: relative; 21 line-height: 17px;
27 top: 2px;
28 } 22 }
29 23
30 small { 24 .form-group-description {
31 font-size: 90%; 25 margin-top: 2px;
32 } 26 }
33 27
34 .wrapper:empty { 28 .wrapper:empty {
diff --git a/client/src/app/shared/shared-forms/reactive-file.component.ts b/client/src/app/shared/shared-forms/reactive-file.component.ts
index 50b7d4c3e..48055a51c 100644
--- a/client/src/app/shared/shared-forms/reactive-file.component.ts
+++ b/client/src/app/shared/shared-forms/reactive-file.component.ts
@@ -57,7 +57,7 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
57 57
58 const extension = '.' + file.name.split('.').pop() 58 const extension = '.' + file.name.split('.').pop()
59 if (this.extensions.includes(extension.toLowerCase()) === false) { 59 if (this.extensions.includes(extension.toLowerCase()) === false) {
60 const message = $localize`PeerTube cannot handle this kind of file. Accepted extensions are ${this.allowedExtensionsMessage}}.` 60 const message = $localize`PeerTube cannot handle this kind of file. Accepted extensions are ${this.allowedExtensionsMessage}.`
61 this.notifier.error(message) 61 this.notifier.error(message)
62 62
63 return 63 return
diff --git a/client/src/app/shared/shared-forms/select/select-channel.component.html b/client/src/app/shared/shared-forms/select/select-channel.component.html
index f83f17a16..b49fd36fa 100644
--- a/client/src/app/shared/shared-forms/select/select-channel.component.html
+++ b/client/src/app/shared/shared-forms/select/select-channel.component.html
@@ -7,7 +7,7 @@
7 [searchable]="searchable" 7 [searchable]="searchable"
8> 8>
9 <ng-option *ngFor="let channel of channels" [value]="channel.id"> 9 <ng-option *ngFor="let channel of channels" [value]="channel.id">
10 <img alt="" class="avatar mr-1" [src]="channel.avatarPath" /> 10 <img alt="" class="avatar me-1" [src]="channel.avatarPath" />
11 {{ channel.label }} 11 {{ channel.label }}
12 </ng-option> 12 </ng-option>
13</ng-select> 13</ng-select>
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts b/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts
index ebf7b77a6..2c3226f68 100644
--- a/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts
+++ b/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts
@@ -1,6 +1,7 @@
1import { Component, forwardRef, Input } from '@angular/core' 1import { Component, forwardRef, Input } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { prepareIcu } from '@app/helpers'
4import { SelectOptionsItem } from '../../../../types/select-options-item.model' 5import { SelectOptionsItem } from '../../../../types/select-options-item.model'
5import { ItemSelectCheckboxValue } from './select-checkbox.component' 6import { ItemSelectCheckboxValue } from './select-checkbox.component'
6 7
@@ -78,7 +79,12 @@ export class SelectCheckboxAllComponent implements ControlValueAccessor {
78 if (!outputItems) return true 79 if (!outputItems) return true
79 80
80 if (outputItems.length >= this.maxItems) { 81 if (outputItems.length >= this.maxItems) {
81 this.notifier.error($localize`You can't select more than ${this.maxItems} items`) 82 this.notifier.error(
83 prepareIcu($localize`You can't select more than {maxItems, plural, =1 {1 item} other {{maxItems} items}}`)(
84 { maxItems: this.maxItems },
85 $localize`You can't select more than ${this.maxItems} items`
86 )
87 )
82 88
83 return false 89 return false
84 } 90 }
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox.component.html b/client/src/app/shared/shared-forms/select/select-checkbox.component.html
index 03db2875b..2799ccdcc 100644
--- a/client/src/app/shared/shared-forms/select/select-checkbox.component.html
+++ b/client/src/app/shared/shared-forms/select/select-checkbox.component.html
@@ -22,7 +22,7 @@
22> 22>
23 23
24 <ng-template ng-optgroup-tmp let-item="item" let-item$="item$" let-index="index"> 24 <ng-template ng-optgroup-tmp let-item="item" let-item$="item$" let-index="index">
25 <div class="form-group-checkbox"> 25 <div class="checkbox-wrapper">
26 <input id="item-{{index}}" type="checkbox" [ngModel]="item$.selected"/> 26 <input id="item-{{index}}" type="checkbox" [ngModel]="item$.selected"/>
27 <span role="checkbox" [attr.aria-checked]="item$.selected"></span> 27 <span role="checkbox" [attr.aria-checked]="item$.selected"></span>
28 <span>{{ item.group }}</span> 28 <span>{{ item.group }}</span>
@@ -30,7 +30,7 @@
30 </ng-template> 30 </ng-template>
31 31
32 <ng-template ng-option-tmp let-item="item" let-item$="item$" let-index="index"> 32 <ng-template ng-option-tmp let-item="item" let-item$="item$" let-index="index">
33 <div class="form-group-checkbox"> 33 <div class="checkbox-wrapper">
34 <input id="item-{{index}}" type="checkbox" [ngModel]="item$.selected"/> 34 <input id="item-{{index}}" type="checkbox" [ngModel]="item$.selected"/>
35 <span role="checkbox" [attr.aria-checked]="item$.selected"></span> 35 <span role="checkbox" [attr.aria-checked]="item$.selected"></span>
36 <span>{{ item.label }}</span> 36 <span>{{ item.label }}</span>
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox.component.scss b/client/src/app/shared/shared-forms/select/select-checkbox.component.scss
index d47c4f9da..892f22dff 100644
--- a/client/src/app/shared/shared-forms/select/select-checkbox.component.scss
+++ b/client/src/app/shared/shared-forms/select/select-checkbox.component.scss
@@ -7,7 +7,7 @@ ng-select ::ng-deep {
7 align-items: center; 7 align-items: center;
8 } 8 }
9 9
10 .form-group-checkbox { 10 .checkbox-wrapper {
11 display: flex; 11 display: flex;
12 align-items: center; 12 align-items: center;
13 13
diff --git a/client/src/app/shared/shared-forms/shared-form.module.ts b/client/src/app/shared/shared-forms/shared-form.module.ts
index 60c2f66ae..81f076db6 100644
--- a/client/src/app/shared/shared-forms/shared-form.module.ts
+++ b/client/src/app/shared/shared-forms/shared-form.module.ts
@@ -9,7 +9,7 @@ import { AdvancedInputFilterComponent } from './advanced-input-filter.component'
9import { DynamicFormFieldComponent } from './dynamic-form-field.component' 9import { DynamicFormFieldComponent } from './dynamic-form-field.component'
10import { FormValidatorService } from './form-validator.service' 10import { FormValidatorService } from './form-validator.service'
11import { InputSwitchComponent } from './input-switch.component' 11import { InputSwitchComponent } from './input-switch.component'
12import { InputToggleHiddenComponent } from './input-toggle-hidden.component' 12import { InputTextComponent } from './input-text.component'
13import { MarkdownTextareaComponent } from './markdown-textarea.component' 13import { MarkdownTextareaComponent } from './markdown-textarea.component'
14import { PeertubeCheckboxComponent } from './peertube-checkbox.component' 14import { PeertubeCheckboxComponent } from './peertube-checkbox.component'
15import { PreviewUploadComponent } from './preview-upload.component' 15import { PreviewUploadComponent } from './preview-upload.component'
@@ -40,7 +40,7 @@ import { TimestampInputComponent } from './timestamp-input.component'
40 ], 40 ],
41 41
42 declarations: [ 42 declarations: [
43 InputToggleHiddenComponent, 43 InputTextComponent,
44 MarkdownTextareaComponent, 44 MarkdownTextareaComponent,
45 PeertubeCheckboxComponent, 45 PeertubeCheckboxComponent,
46 PreviewUploadComponent, 46 PreviewUploadComponent,
@@ -71,7 +71,7 @@ import { TimestampInputComponent } from './timestamp-input.component'
71 InputMaskModule, 71 InputMaskModule,
72 NgSelectModule, 72 NgSelectModule,
73 73
74 InputToggleHiddenComponent, 74 InputTextComponent,
75 MarkdownTextareaComponent, 75 MarkdownTextareaComponent,
76 PeertubeCheckboxComponent, 76 PeertubeCheckboxComponent,
77 PreviewUploadComponent, 77 PreviewUploadComponent,
diff --git a/client/src/app/shared/shared-forms/timestamp-input.component.scss b/client/src/app/shared/shared-forms/timestamp-input.component.scss
index 27d6fa173..e69a06947 100644
--- a/client/src/app/shared/shared-forms/timestamp-input.component.scss
+++ b/client/src/app/shared/shared-forms/timestamp-input.component.scss
@@ -4,7 +4,6 @@
4p-inputmask { 4p-inputmask {
5 ::ng-deep input { 5 ::ng-deep input {
6 width: 80px; 6 width: 80px;
7 font-size: 15px;
8 7
9 &:focus-within, 8 &:focus-within,
10 &:focus { 9 &:focus {
diff --git a/client/src/app/shared/shared-icons/global-icon.component.scss b/client/src/app/shared/shared-icons/global-icon.component.scss
index 6795d6628..10180829f 100644
--- a/client/src/app/shared/shared-icons/global-icon.component.scss
+++ b/client/src/app/shared/shared-icons/global-icon.component.scss
@@ -3,4 +3,9 @@
3 width: inherit; 3 width: inherit;
4 height: inherit; 4 height: inherit;
5 } 5 }
6
7 .feather-flag {
8 margin-left: -1px;
9 }
6} 10}
11
diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts
index ba23edde0..55eb45a75 100644
--- a/client/src/app/shared/shared-icons/global-icon.component.ts
+++ b/client/src/app/shared/shared-icons/global-icon.component.ts
@@ -21,6 +21,7 @@ const icons = {
21 local: require('!!raw-loader?!../../../assets/images/misc/local.svg').default, 21 local: require('!!raw-loader?!../../../assets/images/misc/local.svg').default,
22 22
23 // feather icons 23 // feather icons
24 copy: require('!!raw-loader?!../../../assets/images/feather/copy.svg').default,
24 flag: require('!!raw-loader?!../../../assets/images/feather/flag.svg').default, 25 flag: require('!!raw-loader?!../../../assets/images/feather/flag.svg').default,
25 playlists: require('!!raw-loader?!../../../assets/images/feather/list.svg').default, 26 playlists: require('!!raw-loader?!../../../assets/images/feather/list.svg').default,
26 syndication: require('!!raw-loader?!../../../assets/images/feather/syndication.svg').default, 27 syndication: require('!!raw-loader?!../../../assets/images/feather/syndication.svg').default,
@@ -28,7 +29,6 @@ const icons = {
28 alert: require('!!raw-loader?!../../../assets/images/feather/alert.svg').default, 29 alert: require('!!raw-loader?!../../../assets/images/feather/alert.svg').default,
29 globe: require('!!raw-loader?!../../../assets/images/feather/globe.svg').default, 30 globe: require('!!raw-loader?!../../../assets/images/feather/globe.svg').default,
30 home: require('!!raw-loader?!../../../assets/images/feather/home.svg').default, 31 home: require('!!raw-loader?!../../../assets/images/feather/home.svg').default,
31 'recently-added': require('!!raw-loader?!../../../assets/images/feather/recently-added.svg').default,
32 trending: require('!!raw-loader?!../../../assets/images/feather/trending.svg').default, 32 trending: require('!!raw-loader?!../../../assets/images/feather/trending.svg').default,
33 search: require('!!raw-loader?!../../../assets/images/feather/search.svg').default, 33 search: require('!!raw-loader?!../../../assets/images/feather/search.svg').default,
34 upload: require('!!raw-loader?!../../../assets/images/feather/upload.svg').default, 34 upload: require('!!raw-loader?!../../../assets/images/feather/upload.svg').default,
@@ -62,8 +62,9 @@ const icons = {
62 'exit-fullscreen': require('!!raw-loader?!../../../assets/images/feather/minimize.svg').default, 62 'exit-fullscreen': require('!!raw-loader?!../../../assets/images/feather/minimize.svg').default,
63 film: require('!!raw-loader?!../../../assets/images/feather/film.svg').default, 63 film: require('!!raw-loader?!../../../assets/images/feather/film.svg').default,
64 edit: require('!!raw-loader?!../../../assets/images/feather/edit-2.svg').default, 64 edit: require('!!raw-loader?!../../../assets/images/feather/edit-2.svg').default,
65 sensitive: require('!!raw-loader?!../../../assets/images/feather/eye.svg').default, 65 'external-link': require('!!raw-loader?!../../../assets/images/feather/external-link.svg').default,
66 unsensitive: require('!!raw-loader?!../../../assets/images/feather/eye-off.svg').default, 66 'eye-open': require('!!raw-loader?!../../../assets/images/feather/eye.svg').default,
67 'eye-close': require('!!raw-loader?!../../../assets/images/feather/eye-off.svg').default,
67 refresh: require('!!raw-loader?!../../../assets/images/feather/refresh-cw.svg').default, 68 refresh: require('!!raw-loader?!../../../assets/images/feather/refresh-cw.svg').default,
68 command: require('!!raw-loader?!../../../assets/images/feather/command.svg').default, 69 command: require('!!raw-loader?!../../../assets/images/feather/command.svg').default,
69 go: require('!!raw-loader?!../../../assets/images/feather/arrow-up-right.svg').default, 70 go: require('!!raw-loader?!../../../assets/images/feather/arrow-up-right.svg').default,
@@ -89,6 +90,7 @@ export type GlobalIconName = keyof typeof icons
89}) 90})
90export class GlobalIconComponent implements OnInit { 91export class GlobalIconComponent implements OnInit {
91 @Input() iconName: GlobalIconName 92 @Input() iconName: GlobalIconName
93 @Input() width: string
92 94
93 constructor ( 95 constructor (
94 private el: ElementRef, 96 private el: ElementRef,
@@ -104,6 +106,10 @@ export class GlobalIconComponent implements OnInit {
104 'filter:internal.common.svg-icons.get-content.params', 106 'filter:internal.common.svg-icons.get-content.params',
105 'filter:internal.common.svg-icons.get-content.result' 107 'filter:internal.common.svg-icons.get-content.result'
106 ) 108 )
109
110 if (this.width) {
111 nativeElement.style.width = this.width
112 }
107 } 113 }
108 114
109 private getSVGContent (options: { name: string }) { 115 private getSVGContent (options: { name: string }) {
diff --git a/client/src/app/shared/shared-instance/feature-boolean.component.html b/client/src/app/shared/shared-instance/feature-boolean.component.html
index ccb8a30cc..6de5d2075 100644
--- a/client/src/app/shared/shared-instance/feature-boolean.component.html
+++ b/client/src/app/shared/shared-instance/feature-boolean.component.html
@@ -1,3 +1,2 @@
1<span *ngIf="value === true" class="glyphicon glyphicon-ok" i18n-aria-label aria-label="yes"></span> 1<my-global-icon *ngIf="value === true" iconName="tick" i18n-aria-label aria-label="yes"></my-global-icon>
2<span *ngIf="value === false" class="glyphicon glyphicon-remove" i18n-aria-label aria-label="no"></span> 2<my-global-icon *ngIf="value === false" iconName="cross" i18n-aria-label aria-label="no"></my-global-icon>
3
diff --git a/client/src/app/shared/shared-instance/feature-boolean.component.scss b/client/src/app/shared/shared-instance/feature-boolean.component.scss
index 8683ecc55..29b8c3e02 100644
--- a/client/src/app/shared/shared-instance/feature-boolean.component.scss
+++ b/client/src/app/shared/shared-instance/feature-boolean.component.scss
@@ -1,10 +1,10 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.glyphicon-ok { 4my-global-icon[iconName=tick] {
5 color: $green; 5 color: $green;
6} 6}
7 7
8.glyphicon-remove { 8my-global-icon[iconName=cross] {
9 color: $red; 9 color: $red;
10} 10}
diff --git a/client/src/app/shared/shared-instance/instance-about-accordion.component.html b/client/src/app/shared/shared-instance/instance-about-accordion.component.html
index 73e511d1c..466d73ca4 100644
--- a/client/src/app/shared/shared-instance/instance-about-accordion.component.html
+++ b/client/src/app/shared/shared-instance/instance-about-accordion.component.html
@@ -1,6 +1,6 @@
1<h2 class="instance-name">{{ about?.instance.name }}</h2> 1<h2 *ngIf="displayInstanceName" class="instance-name">{{ about?.instance.name }}</h2>
2 2
3<div class="instance-short-description">{{ about?.instance.shortDescription }}</div> 3<div *ngIf="displayInstanceShortDescription" class="instance-short-description">{{ about?.instance.shortDescription }}</div>
4 4
5<ngb-accordion #accordion="ngbAccordion" [closeOthers]="true"> 5<ngb-accordion #accordion="ngbAccordion" [closeOthers]="true">
6 <ngb-panel *ngIf="panels.features" id="instance-features" i18n-title title="Features found on this instance"> 6 <ngb-panel *ngIf="panels.features" id="instance-features" i18n-title title="Features found on this instance">
@@ -32,7 +32,7 @@
32 </ng-template> 32 </ng-template>
33 </ngb-panel> 33 </ngb-panel>
34 34
35 <ngb-panel *ngIf="termsPanel" id="terms" i18n-title title="Terms"> 35 <ngb-panel *ngIf="termsPanel" id="terms" [title]="getTermsTitle()">
36 <ng-template ngbPanelContent> 36 <ng-template ngbPanelContent>
37 <div class="block" [innerHTML]="aboutHtml.terms"></div> 37 <div class="block" [innerHTML]="aboutHtml.terms"></div>
38 </ng-template> 38 </ng-template>
diff --git a/client/src/app/shared/shared-instance/instance-about-accordion.component.scss b/client/src/app/shared/shared-instance/instance-about-accordion.component.scss
index be6099a97..c8e288462 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
@@ -8,34 +8,9 @@
8.instance-short-description { 8.instance-short-description {
9 @include ellipsis-multiline(1rem, 3); 9 @include ellipsis-multiline(1rem, 3);
10 10
11 margin-top: 20px; 11 margin: 25px 0;
12 margin-bottom: 20px;
13} 12}
14 13
15.block { 14.block {
16 font-size: 15px;
17 margin-bottom: 15px; 15 margin-bottom: 15px;
18} 16}
19
20ngb-accordion ::ng-deep {
21 .card {
22 border-color: var(--mainBackgroundColor);
23
24 .card-header {
25 background-color: unset;
26 padding: 0;
27
28 + .collapse.show {
29 background-color: var(--submenuBackgroundColor);
30 }
31 }
32 }
33
34 .btn {
35 @include peertube-button;
36 @include grey-button;
37
38 border-radius: unset;
39 width: 100%;
40 }
41}
diff --git a/client/src/app/shared/shared-instance/instance-about-accordion.component.ts b/client/src/app/shared/shared-instance/instance-about-accordion.component.ts
index b9f57e2a4..e13703c03 100644
--- a/client/src/app/shared/shared-instance/instance-about-accordion.component.ts
+++ b/client/src/app/shared/shared-instance/instance-about-accordion.component.ts
@@ -15,6 +15,9 @@ export class InstanceAboutAccordionComponent implements OnInit {
15 15
16 @Output() init: EventEmitter<InstanceAboutAccordionComponent> = new EventEmitter<InstanceAboutAccordionComponent>() 16 @Output() init: EventEmitter<InstanceAboutAccordionComponent> = new EventEmitter<InstanceAboutAccordionComponent>()
17 17
18 @Input() displayInstanceName = true
19 @Input() displayInstanceShortDescription = true
20
18 @Input() pluginScope: PluginClientScope 21 @Input() pluginScope: PluginClientScope
19 @Input() pluginHook: ClientFilterHookName 22 @Input() pluginHook: ClientFilterHookName
20 23
@@ -66,6 +69,10 @@ export class InstanceAboutAccordionComponent implements OnInit {
66 return !!(this.aboutHtml?.administrator || this.about?.instance.maintenanceLifetime || this.about?.instance.businessModel) 69 return !!(this.aboutHtml?.administrator || this.about?.instance.maintenanceLifetime || this.about?.instance.businessModel)
67 } 70 }
68 71
72 getTermsTitle () {
73 return $localize`Terms of ${this.about.instance.name}`
74 }
75
69 get moderationPanel () { 76 get moderationPanel () {
70 return this.panels.moderation && !!this.aboutHtml.moderationInformation 77 return this.panels.moderation && !!this.aboutHtml.moderationInformation
71 } 78 }
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.html b/client/src/app/shared/shared-instance/instance-features-table.component.html
index 1fdef95ff..761243bfe 100644
--- a/client/src/app/shared/shared-instance/instance-features-table.component.html
+++ b/client/src/app/shared/shared-instance/instance-features-table.component.html
@@ -1,6 +1,6 @@
1<div *ngIf="serverConfig" class="feature-table"> 1<div *ngIf="serverConfig" class="feature-table">
2 2
3 <table class="table" *ngIf="serverConfig"> 3 <table *ngIf="serverConfig">
4 <caption i18n>Features found on this instance</caption> 4 <caption i18n>Features found on this instance</caption>
5 <tr> 5 <tr>
6 <th i18n class="label" scope="row">PeerTube version</th> 6 <th i18n class="label" scope="row">PeerTube version</th>
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.scss b/client/src/app/shared/shared-instance/instance-features-table.component.scss
index 56ca105f4..bfae0c112 100644
--- a/client/src/app/shared/shared-instance/instance-features-table.component.scss
+++ b/client/src/app/shared/shared-instance/instance-features-table.component.scss
@@ -4,6 +4,7 @@
4table { 4table {
5 font-size: 14px; 5 font-size: 14px;
6 color: pvar(--mainForegroundColor); 6 color: pvar(--mainForegroundColor);
7 width: 100%;
7 8
8 .label, 9 .label,
9 .sub-label { 10 .sub-label {
@@ -24,13 +25,14 @@ table {
24 } 25 }
25 } 26 }
26 27
28 th,
27 td { 29 td {
28 vertical-align: middle; 30 padding: 0.75rem;
31 border-top: 1px solid #dee2e6;
29 } 32 }
30 33
31 caption { 34 caption {
32 caption-side: top; 35 caption-side: top;
33 font-size: 15px;
34 font-weight: $font-semibold; 36 font-weight: $font-semibold;
35 color: pvar(--mainForegroundColor); 37 color: pvar(--mainForegroundColor);
36 } 38 }
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts
index 6335de450..e405c5790 100644
--- a/client/src/app/shared/shared-instance/instance-features-table.component.ts
+++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts
@@ -1,5 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
3import { prepareIcu } from '@app/helpers'
3import { ServerConfig } from '@shared/models' 4import { ServerConfig } from '@shared/models'
4import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service' 5import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service'
5 6
@@ -65,15 +66,20 @@ export class InstanceFeaturesTableComponent implements OnInit {
65 66
66 private getApproximateTime (seconds: number) { 67 private getApproximateTime (seconds: number) {
67 const hours = Math.floor(seconds / 3600) 68 const hours = Math.floor(seconds / 3600)
68 let pluralSuffix = ''
69 if (hours > 1) pluralSuffix = 's'
70 if (hours > 0) return `~ ${hours} hour${pluralSuffix}`
71 69
72 const minutes = Math.floor(seconds % 3600 / 60) 70 if (hours !== 0) {
71 return prepareIcu($localize`~ {hours, plural, =1 {1 hour} other {{hours} hours}}`)(
72 { hours },
73 $localize`~ ${hours} hours`
74 )
75 }
73 76
74 if (minutes === 1) return $localize`~ 1 minute` 77 const minutes = Math.floor(seconds % 3600 / 60)
75 78
76 return $localize`~ ${minutes} minutes` 79 return prepareIcu($localize`~ {minutes, plural, =1 {1 minute} other {{minutes} minutes}}`)(
80 { minutes },
81 $localize`~ ${minutes} minutes`
82 )
77 } 83 }
78 84
79 private buildQuotaHelpIndication () { 85 private buildQuotaHelpIndication () {
diff --git a/client/src/app/shared/shared-instance/instance-statistics.component.html b/client/src/app/shared/shared-instance/instance-statistics.component.html
index 2ca61fd94..68b209990 100644
--- a/client/src/app/shared/shared-instance/instance-statistics.component.html
+++ b/client/src/app/shared/shared-instance/instance-statistics.component.html
@@ -10,7 +10,7 @@
10 <p class="stat-value">{{ serverStats.totalUsers | number }}</p> 10 <p class="stat-value">{{ serverStats.totalUsers | number }}</p>
11 <p class="stat-label" i18n>users</p> 11 <p class="stat-label" i18n>users</p>
12 </div> 12 </div>
13 <i class="glyphicon glyphicon-user icon-bottom"></i> 13 <my-global-icon iconName="user"></my-global-icon>
14 </div> 14 </div>
15 </div> 15 </div>
16 16
@@ -20,7 +20,7 @@
20 <p class="stat-value">{{ serverStats.totalLocalVideos | number }}</p> 20 <p class="stat-value">{{ serverStats.totalLocalVideos | number }}</p>
21 <p class="stat-label" i18n>videos</p> 21 <p class="stat-label" i18n>videos</p>
22 </div> 22 </div>
23 <i class="glyphicon glyphicon-facetime-video"></i> 23 <my-global-icon iconName="film"></my-global-icon>
24 </div> 24 </div>
25 </div> 25 </div>
26 26
@@ -30,7 +30,7 @@
30 <p class="stat-value">{{ serverStats.totalLocalVideoViews | number }}</p> 30 <p class="stat-value">{{ serverStats.totalLocalVideoViews | number }}</p>
31 <p class="stat-label" i18n>views</p> 31 <p class="stat-label" i18n>views</p>
32 </div> 32 </div>
33 <i class="glyphicon glyphicon-eye-open"></i> 33 <my-global-icon iconName="eye-open"></my-global-icon>
34 </div> 34 </div>
35 </div> 35 </div>
36 36
@@ -40,7 +40,7 @@
40 <p class="stat-value">{{ serverStats.totalLocalVideoComments | number }}</p> 40 <p class="stat-value">{{ serverStats.totalLocalVideoComments | number }}</p>
41 <p class="stat-label" i18n>comments</p> 41 <p class="stat-label" i18n>comments</p>
42 </div> 42 </div>
43 <i class="glyphicon glyphicon-comment"></i> 43 <my-global-icon iconName="message-circle"></my-global-icon>
44 </div> 44 </div>
45 </div> 45 </div>
46 46
@@ -50,7 +50,7 @@
50 <p class="stat-value">{{ serverStats.totalLocalVideoFilesSize | bytes:1 }}</p> 50 <p class="stat-value">{{ serverStats.totalLocalVideoFilesSize | bytes:1 }}</p>
51 <p class="stat-label" i18n>hosted video</p> 51 <p class="stat-label" i18n>hosted video</p>
52 </div> 52 </div>
53 <i class="glyphicon glyphicon-hdd"></i> 53 <my-global-icon iconName="home"></my-global-icon>
54 </div> 54 </div>
55 </div> 55 </div>
56 </div> 56 </div>
@@ -64,7 +64,7 @@
64 <p class="stat-value">{{ serverStats.totalVideos | number }}</p> 64 <p class="stat-value">{{ serverStats.totalVideos | number }}</p>
65 <p class="stat-label" i18n>videos</p> 65 <p class="stat-label" i18n>videos</p>
66 </div> 66 </div>
67 <i class="glyphicon glyphicon-facetime-video"></i> 67 <my-global-icon iconName="film"></my-global-icon>
68 </div> 68 </div>
69 </div> 69 </div>
70 70
@@ -74,7 +74,7 @@
74 <p class="stat-value">{{ serverStats.totalVideoComments | number }}</p> 74 <p class="stat-value">{{ serverStats.totalVideoComments | number }}</p>
75 <p class="stat-label" i18n>comments</p> 75 <p class="stat-label" i18n>comments</p>
76 </div> 76 </div>
77 <i class="glyphicon glyphicon-comment"></i> 77 <my-global-icon iconName="message-circle"></my-global-icon>
78 </div> 78 </div>
79 </div> 79 </div>
80 80
@@ -84,7 +84,7 @@
84 <p class="stat-value">{{ serverStats.totalInstanceFollowers | number }}</p> 84 <p class="stat-value">{{ serverStats.totalInstanceFollowers | number }}</p>
85 <p class="stat-label" i18n>followers</p> 85 <p class="stat-label" i18n>followers</p>
86 </div> 86 </div>
87 <i class="glyphicon glyphicon-retweet"></i> 87 <my-global-icon iconName="share"></my-global-icon>
88 </div> 88 </div>
89 </div> 89 </div>
90 90
@@ -94,7 +94,7 @@
94 <p class="stat-value">{{ serverStats.totalInstanceFollowing | number }}</p> 94 <p class="stat-value">{{ serverStats.totalInstanceFollowing | number }}</p>
95 <p class="stat-label" i18n>following</p> 95 <p class="stat-label" i18n>following</p>
96 </div> 96 </div>
97 <i class="glyphicon glyphicon-retweet"></i> 97 <my-global-icon iconName="globe"></my-global-icon>
98 </div> 98 </div>
99 </div> 99 </div>
100 </div> 100 </div>
diff --git a/client/src/app/shared/shared-instance/instance-statistics.component.scss b/client/src/app/shared/shared-instance/instance-statistics.component.scss
index 5286ab03a..e1d489d28 100644
--- a/client/src/app/shared/shared-instance/instance-statistics.component.scss
+++ b/client/src/app/shared/shared-instance/instance-statistics.component.scss
@@ -1,3 +1,5 @@
1@use '_variables' as *;
2@use '_mixins' as *;
1 3
2h3 { 4h3 {
3 font-size: 1.25rem; 5 font-size: 1.25rem;
@@ -19,22 +21,19 @@ h3 {
19 margin: 0; 21 margin: 0;
20 } 22 }
21 23
22 .glyphicon {
23 opacity: 0.12;
24 position: absolute;
25 left: 16px;
26 top: -24px;
27
28 &.icon-bottom {
29 top: 4px;
30 }
31
32 &::before {
33 font-size: 8em;
34 }
35 }
36
37 .card-body { 24 .card-body {
38 z-index: 2; 25 z-index: 2;
39 } 26 }
40} 27}
28
29my-global-icon {
30 opacity: 0.12;
31 position: absolute;
32 left: 16px;
33 top: -24px;
34 width: 110px;
35
36 &.icon-bottom {
37 top: 4px;
38 }
39}
diff --git a/client/src/app/shared/shared-instance/shared-instance.module.ts b/client/src/app/shared/shared-instance/shared-instance.module.ts
index 13c681ab8..dfce88e11 100644
--- a/client/src/app/shared/shared-instance/shared-instance.module.ts
+++ b/client/src/app/shared/shared-instance/shared-instance.module.ts
@@ -1,6 +1,7 @@
1 1
2import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
3import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap' 3import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
4import { SharedGlobalIconModule } from '../shared-icons'
4import { SharedMainModule } from '../shared-main/shared-main.module' 5import { SharedMainModule } from '../shared-main/shared-main.module'
5import { FeatureBooleanComponent } from './feature-boolean.component' 6import { FeatureBooleanComponent } from './feature-boolean.component'
6import { InstanceAboutAccordionComponent } from './instance-about-accordion.component' 7import { InstanceAboutAccordionComponent } from './instance-about-accordion.component'
@@ -12,6 +13,7 @@ import { InstanceService } from './instance.service'
12@NgModule({ 13@NgModule({
13 imports: [ 14 imports: [
14 SharedMainModule, 15 SharedMainModule,
16 SharedGlobalIconModule,
15 NgbAccordionModule 17 NgbAccordionModule
16 ], 18 ],
17 19
diff --git a/client/src/app/shared/shared-main/angular/autofocus.directive.ts b/client/src/app/shared/shared-main/angular/autofocus.directive.ts
index 2da492ea1..051635f45 100644
--- a/client/src/app/shared/shared-main/angular/autofocus.directive.ts
+++ b/client/src/app/shared/shared-main/angular/autofocus.directive.ts
@@ -7,6 +7,8 @@ export class AutofocusDirective implements AfterViewInit {
7 constructor (private host: ElementRef) { } 7 constructor (private host: ElementRef) { }
8 8
9 ngAfterViewInit () { 9 ngAfterViewInit () {
10 this.host.nativeElement.focus() 10 const el = this.host.nativeElement as HTMLElement
11
12 el.focus({ preventScroll: true })
11 } 13 }
12} 14}
diff --git a/client/src/app/shared/shared-main/angular/from-now.pipe.ts b/client/src/app/shared/shared-main/angular/from-now.pipe.ts
index d62c1f88e..dc6a25e83 100644
--- a/client/src/app/shared/shared-main/angular/from-now.pipe.ts
+++ b/client/src/app/shared/shared-main/angular/from-now.pipe.ts
@@ -1,37 +1,51 @@
1import { Pipe, PipeTransform } from '@angular/core' 1import { Pipe, PipeTransform } from '@angular/core'
2import { prepareIcu } from '@app/helpers'
2 3
3// Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site 4// Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site
4@Pipe({ name: 'myFromNow' }) 5@Pipe({ name: 'myFromNow' })
5export class FromNowPipe implements PipeTransform { 6export class FromNowPipe implements PipeTransform {
7 private yearICU = prepareIcu($localize`{interval, plural, =1 {1 year ago} other {{interval} years ago}}`)
8 private monthICU = prepareIcu($localize`{interval, plural, =1 {1 month ago} other {{interval} months ago}}`)
9 private weekICU = prepareIcu($localize`{interval, plural, =1 {1 week ago} other {{interval} weeks ago}}`)
10 private dayICU = prepareIcu($localize`{interval, plural, =1 {1 day ago} other {{interval} days ago}}`)
11 private hourICU = prepareIcu($localize`{interval, plural, =1 {1 hour ago} other {{interval} hours ago}}`)
12
6 transform (arg: number | Date | string) { 13 transform (arg: number | Date | string) {
7 const argDate = new Date(arg) 14 const argDate = new Date(arg)
8 const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) 15 const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000)
9 16
10 let interval = Math.floor(seconds / 31536000) 17 let interval = Math.floor(seconds / 31536000)
11 if (interval > 1) return $localize`${interval} years ago` 18 if (interval >= 1) {
12 if (interval === 1) return $localize`1 year ago` 19 return this.yearICU({ interval }, $localize`${interval} year(s) ago`)
20 }
13 21
14 interval = Math.floor(seconds / 2419200) 22 interval = Math.floor(seconds / 2419200)
15 // 12 months = 360 days, but a year ~ 365 days 23 // 12 months = 360 days, but a year ~ 365 days
16 // Display "1 year ago" rather than "12 months ago" 24 // Display "1 year ago" rather than "12 months ago"
17 if (interval >= 12) return $localize`1 year ago` 25 if (interval >= 12) return $localize`1 year ago`
18 if (interval > 1) return $localize`${interval} months ago` 26
19 if (interval === 1) return $localize`1 month ago` 27 if (interval >= 1) {
28 return this.monthICU({ interval }, $localize`${interval} month(s) ago`)
29 }
20 30
21 interval = Math.floor(seconds / 604800) 31 interval = Math.floor(seconds / 604800)
22 // 4 weeks ~ 28 days, but our month is 30 days 32 // 4 weeks ~ 28 days, but our month is 30 days
23 // Display "1 month ago" rather than "4 weeks ago" 33 // Display "1 month ago" rather than "4 weeks ago"
24 if (interval >= 4) return $localize`1 month ago` 34 if (interval >= 4) return $localize`1 month ago`
25 if (interval > 1) return $localize`${interval} weeks ago` 35
26 if (interval === 1) return $localize`1 week ago` 36 if (interval >= 1) {
37 return this.weekICU({ interval }, $localize`${interval} week(s) ago`)
38 }
27 39
28 interval = Math.floor(seconds / 86400) 40 interval = Math.floor(seconds / 86400)
29 if (interval > 1) return $localize`${interval} days ago` 41 if (interval >= 1) {
30 if (interval === 1) return $localize`1 day ago` 42 return this.dayICU({ interval }, $localize`${interval} day(s) ago`)
43 }
31 44
32 interval = Math.floor(seconds / 3600) 45 interval = Math.floor(seconds / 3600)
33 if (interval > 1) return $localize`${interval} hours ago` 46 if (interval >= 1) {
34 if (interval === 1) return $localize`1 hour ago` 47 return this.hourICU({ interval }, $localize`${interval} hour(s) ago`)
48 }
35 49
36 interval = Math.floor(seconds / 60) 50 interval = Math.floor(seconds / 60)
37 if (interval >= 1) return $localize`${interval} min ago` 51 if (interval >= 1) return $localize`${interval} min ago`
diff --git a/client/src/app/shared/shared-main/buttons/action-dropdown.component.html b/client/src/app/shared/shared-main/buttons/action-dropdown.component.html
index 10dae68f0..017355bd0 100644
--- a/client/src/app/shared/shared-main/buttons/action-dropdown.component.html
+++ b/client/src/app/shared/shared-main/buttons/action-dropdown.component.html
@@ -39,7 +39,7 @@
39 </span> 39 </span>
40 40
41 <h6 41 <h6
42 *ngIf="!action.linkBuilder && action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }" 42 *ngIf="!action.linkBuilder && action.isHeader && areActionsDisplayed(actions, entry)" [ngClass]="{ 'with-icon': !!action.iconName }"
43 class="dropdown-header" tabindex="0" role="button" [title]="action.title || ''" (click)="action.handler(entry)" (keyup.enter)="action.handler(entry)" 43 class="dropdown-header" tabindex="0" role="button" [title]="action.title || ''" (click)="action.handler(entry)" (keyup.enter)="action.handler(entry)"
44 > 44 >
45 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container> 45 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
diff --git a/client/src/app/shared/shared-main/buttons/action-dropdown.component.ts b/client/src/app/shared/shared-main/buttons/action-dropdown.component.ts
index 67ac6e1aa..749773f8a 100644
--- a/client/src/app/shared/shared-main/buttons/action-dropdown.component.ts
+++ b/client/src/app/shared/shared-main/buttons/action-dropdown.component.ts
@@ -48,7 +48,7 @@ export class ActionDropdownComponent<T> {
48 return actions.some(a => { 48 return actions.some(a => {
49 if (Array.isArray(a)) return this.areActionsDisplayed(a, entry) 49 if (Array.isArray(a)) return this.areActionsDisplayed(a, entry)
50 50
51 return a.isDisplayed === undefined || a.isDisplayed(entry) 51 return a.isHeader !== true && (a.isDisplayed === undefined || a.isDisplayed(entry))
52 }) 52 })
53 } 53 }
54} 54}
diff --git a/client/src/app/shared/shared-main/buttons/button.component.html b/client/src/app/shared/shared-main/buttons/button.component.html
index d1a4215e6..3e3728623 100644
--- a/client/src/app/shared/shared-main/buttons/button.component.html
+++ b/client/src/app/shared/shared-main/buttons/button.component.html
@@ -1,8 +1,16 @@
1<span class="action-button" [ngClass]="getClasses()" [ngbTooltip]="title" tabindex="0"> 1<div *ngIf="!routerLink" class="action-button" [ngClass]="classes" [ngbTooltip]="title" tabindex="0">
2 <my-small-loader [loading]="loading"></my-small-loader> 2 <ng-container *ngTemplateOutlet="content"></ng-container>
3</div>
4
5<a *ngIf="routerLink" class="action-button" [ngClass]="classes" [ngbTooltip]="title" [routerLink]="routerLink">
6 <ng-container *ngTemplateOutlet="content"></ng-container>
7</a>
8
9<ng-template #content>
10 <my-loader size="sm" [loading]="loading"></my-loader>
3 <my-global-icon *ngIf="icon && !loading" [iconName]="icon"></my-global-icon> 11 <my-global-icon *ngIf="icon && !loading" [iconName]="icon"></my-global-icon>
4 12
5 <span *ngIf="label" class="button-label">{{ label }}</span> 13 <span *ngIf="label" class="button-label">{{ label }}</span>
6 14
7 <ng-content></ng-content> 15 <ng-content></ng-content>
8</span> 16</ng-template>
diff --git a/client/src/app/shared/shared-main/buttons/button.component.scss b/client/src/app/shared/shared-main/buttons/button.component.scss
index c53b8f2e5..7f0cdf1ed 100644
--- a/client/src/app/shared/shared-main/buttons/button.component.scss
+++ b/client/src/app/shared/shared-main/buttons/button.component.scss
@@ -9,17 +9,14 @@
9 .button-label { 9 .button-label {
10 display: none; 10 display: none;
11 } 11 }
12}
13 12
14:host { 13 my-global-icon {
15 outline: none; 14 margin: 0 !important;
16 display: inline-block; 15 }
17} 16}
18 17
19my-small-loader ::ng-deep .root { 18:host {
20 display: inline-block; 19 display: inline-block;
21 margin: 0 3px 0 0;
22 width: 20px;
23} 20}
24 21
25a[class$=-button], 22a[class$=-button],
@@ -30,35 +27,34 @@ span[class$=-button] {
30} 27}
31 28
32.action-button { 29.action-button {
33 @include peertube-button-link;
34 @include button-with-icon(21px);
35
36 width: 100%; // useful for ellipsis, allow to define a max-width on host component 30 width: 100%; // useful for ellipsis, allow to define a max-width on host component
37 31
38 &.icon-only { 32 &.has-icon {
39 my-global-icon { 33 @include button-with-icon(21px);
40 margin: 0; 34 }
41 } 35
36 &.icon-only my-global-icon {
37 margin: 0 !important;
42 } 38 }
43} 39}
44 40
45.orange-button { 41.orange-button,
42.grey-button {
46 @include peertube-button; 43 @include peertube-button;
47 @include orange-button;
48} 44}
49 45
50.orange-button-link { 46.orange-button-link,
47.grey-button-link {
51 @include peertube-button-link; 48 @include peertube-button-link;
52 @include orange-button;
53} 49}
54 50
55.grey-button { 51.orange-button,
56 @include peertube-button; 52.orange-button-link {
57 @include grey-button; 53 @include orange-button;
58} 54}
59 55
56.grey-button,
60.grey-button-link { 57.grey-button-link {
61 @include peertube-button-link;
62 @include grey-button; 58 @include grey-button;
63} 59}
64 60
diff --git a/client/src/app/shared/shared-main/buttons/button.component.ts b/client/src/app/shared/shared-main/buttons/button.component.ts
index 52936a4d4..10d67831f 100644
--- a/client/src/app/shared/shared-main/buttons/button.component.ts
+++ b/client/src/app/shared/shared-main/buttons/button.component.ts
@@ -1,4 +1,4 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input, OnChanges } from '@angular/core'
2import { GlobalIconName } from '@app/shared/shared-icons' 2import { GlobalIconName } from '@app/shared/shared-icons'
3 3
4@Component({ 4@Component({
@@ -7,20 +7,24 @@ import { GlobalIconName } from '@app/shared/shared-icons'
7 templateUrl: './button.component.html' 7 templateUrl: './button.component.html'
8}) 8})
9 9
10export class ButtonComponent { 10export class ButtonComponent implements OnChanges {
11 @Input() label = '' 11 @Input() label = ''
12 @Input() className = 'grey-button' 12 @Input() className = 'grey-button'
13 @Input() icon: GlobalIconName = undefined 13 @Input() icon: GlobalIconName = undefined
14 @Input() routerLink: string[] | string
14 @Input() title: string = undefined 15 @Input() title: string = undefined
15 @Input() loading = false 16 @Input() loading = false
16 @Input() disabled = false 17 @Input() disabled = false
17 @Input() responsiveLabel = false 18 @Input() responsiveLabel = false
18 19
19 getClasses () { 20 classes: { [id: string]: boolean } = {}
20 return { 21
22 ngOnChanges () {
23 this.classes = {
21 [this.className]: true, 24 [this.className]: true,
22 disabled: this.disabled, 25 disabled: this.disabled,
23 'icon-only': !this.label, 26 'icon-only': !this.label,
27 'has-icon': !!this.icon,
24 'responsive-label': this.responsiveLabel 28 'responsive-label': this.responsiveLabel
25 } 29 }
26 } 30 }
diff --git a/client/src/app/shared/shared-main/buttons/delete-button.component.html b/client/src/app/shared/shared-main/buttons/delete-button.component.html
deleted file mode 100644
index d7a6702a7..000000000
--- a/client/src/app/shared/shared-main/buttons/delete-button.component.html
+++ /dev/null
@@ -1,8 +0,0 @@
1<span
2 class="action-button action-button-delete grey-button"
3 [ngClass]="{ 'responsive-label': responsiveLabel }" [ngbTooltip]="title" role="button" tabindex="0"
4>
5 <my-global-icon iconName="delete" aria-hidden="true"></my-global-icon>
6
7 <span class="button-label" *ngIf="label">{{ label }}</span>
8</span>
diff --git a/client/src/app/shared/shared-main/buttons/delete-button.component.ts b/client/src/app/shared/shared-main/buttons/delete-button.component.ts
index 90735852c..1cab10803 100644
--- a/client/src/app/shared/shared-main/buttons/delete-button.component.ts
+++ b/client/src/app/shared/shared-main/buttons/delete-button.component.ts
@@ -2,17 +2,16 @@ import { Component, Input, OnInit } from '@angular/core'
2 2
3@Component({ 3@Component({
4 selector: 'my-delete-button', 4 selector: 'my-delete-button',
5 styleUrls: [ './button.component.scss' ], 5 template: `
6 templateUrl: './delete-button.component.html' 6 <my-button icon="delete" className="grey-button" [label]="label" [title]="title" [responsiveLabel]="responsiveLabel"></my-button>
7 `
7}) 8})
8
9export class DeleteButtonComponent implements OnInit { 9export class DeleteButtonComponent implements OnInit {
10 @Input() label: string 10 @Input() label: string
11 @Input() title: string 11 @Input() title: string
12 @Input() responsiveLabel = false 12 @Input() responsiveLabel = false
13 13
14 ngOnInit () { 14 ngOnInit () {
15 // <my-delete-button /> No label
16 if (this.label === undefined && !this.title) { 15 if (this.label === undefined && !this.title) {
17 this.title = $localize`Delete` 16 this.title = $localize`Delete`
18 } 17 }
diff --git a/client/src/app/shared/shared-main/buttons/edit-button.component.html b/client/src/app/shared/shared-main/buttons/edit-button.component.html
deleted file mode 100644
index 8beeee6c4..000000000
--- a/client/src/app/shared/shared-main/buttons/edit-button.component.html
+++ /dev/null
@@ -1,8 +0,0 @@
1<a
2 class="action-button action-button-edit grey-button"
3 [ngClass]="{ 'responsive-label': responsiveLabel }" [routerLink]="routerLink" [ngbTooltip]="title"
4>
5 <my-global-icon iconName="edit" aria-hidden="true"></my-global-icon>
6
7 <span class="button-label" *ngIf="label">{{ label }}</span>
8</a>
diff --git a/client/src/app/shared/shared-main/buttons/edit-button.component.ts b/client/src/app/shared/shared-main/buttons/edit-button.component.ts
index 24c8625ff..28aacbbff 100644
--- a/client/src/app/shared/shared-main/buttons/edit-button.component.ts
+++ b/client/src/app/shared/shared-main/buttons/edit-button.component.ts
@@ -2,8 +2,13 @@ import { Component, Input, OnInit } from '@angular/core'
2 2
3@Component({ 3@Component({
4 selector: 'my-edit-button', 4 selector: 'my-edit-button',
5 styleUrls: [ './button.component.scss' ], 5 template: `
6 templateUrl: './edit-button.component.html' 6 <my-button
7 icon="edit" className="grey-button-link"
8 [label]="label" [title]="title" [responsiveLabel]="responsiveLabel"
9 [routerLink]="routerLink"
10 ></my-button>
11 `
7}) 12})
8export class EditButtonComponent implements OnInit { 13export class EditButtonComponent implements OnInit {
9 @Input() label: string 14 @Input() label: string
@@ -20,10 +25,6 @@ export class EditButtonComponent implements OnInit {
20 // <my-edit-button label /> Use default label 25 // <my-edit-button label /> Use default label
21 if (this.label === '') { 26 if (this.label === '') {
22 this.label = $localize`Update` 27 this.label = $localize`Update`
23
24 if (!this.title) {
25 this.title = this.label
26 }
27 } 28 }
28 } 29 }
29} 30}
diff --git a/client/src/app/shared/shared-main/loaders/index.ts b/client/src/app/shared/shared-main/loaders/index.ts
index a061914d5..60483727c 100644
--- a/client/src/app/shared/shared-main/loaders/index.ts
+++ b/client/src/app/shared/shared-main/loaders/index.ts
@@ -1,2 +1 @@
1export * from './loader.component' export * from './loader.component'
2export * from './small-loader.component'
diff --git a/client/src/app/shared/shared-main/loaders/loader.component.html b/client/src/app/shared/shared-main/loaders/loader.component.html
deleted file mode 100644
index ca8ed063e..000000000
--- a/client/src/app/shared/shared-main/loaders/loader.component.html
+++ /dev/null
@@ -1,8 +0,0 @@
1<div *ngIf="loading">
2 <div class="loader">
3 <div></div>
4 <div></div>
5 <div></div>
6 <div></div>
7 </div>
8</div>
diff --git a/client/src/app/shared/shared-main/loaders/loader.component.scss b/client/src/app/shared/shared-main/loaders/loader.component.scss
deleted file mode 100644
index b88b0db6a..000000000
--- a/client/src/app/shared/shared-main/loaders/loader.component.scss
+++ /dev/null
@@ -1,45 +0,0 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4// Thanks to https://loading.io/css/ (CC0 License)
5
6.loader {
7 display: inline-block;
8 position: relative;
9 width: 50px;
10 height: 50px;
11}
12
13.loader div {
14 box-sizing: border-box;
15 display: block;
16 position: absolute;
17 width: 44px;
18 height: 44px;
19 margin: 6px;
20 border: 4px solid;
21 border-radius: 50%;
22 animation: loader 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
23 border-color: #999999 transparent transparent;
24}
25
26.loader div:nth-child(1) {
27 animation-delay: -0.45s;
28}
29
30.loader div:nth-child(2) {
31 animation-delay: -0.3s;
32}
33
34.loader div:nth-child(3) {
35 animation-delay: -0.15s;
36}
37
38@keyframes loader {
39 0% {
40 transform: rotate(0deg);
41 }
42 100% {
43 transform: rotate(360deg);
44 }
45}
diff --git a/client/src/app/shared/shared-main/loaders/loader.component.ts b/client/src/app/shared/shared-main/loaders/loader.component.ts
index e3b1eea3a..bd038f8b5 100644
--- a/client/src/app/shared/shared-main/loaders/loader.component.ts
+++ b/client/src/app/shared/shared-main/loaders/loader.component.ts
@@ -2,9 +2,27 @@ import { Component, Input } from '@angular/core'
2 2
3@Component({ 3@Component({
4 selector: 'my-loader', 4 selector: 'my-loader',
5 styleUrls: [ './loader.component.scss' ], 5 template: `<div *ngIf="loading" class="spinner-border" [ngStyle]="getStyle()" role="status"></div>`
6 templateUrl: './loader.component.html'
7}) 6})
8export class LoaderComponent { 7export class LoaderComponent {
9 @Input() loading: boolean 8 @Input() loading: boolean
9 @Input() size: 'sm' | 'xl'
10
11 private readonly sizes = {
12 sm: {
13 width: '1rem',
14 height: '1rem',
15 'border-width': '0.15rem'
16 },
17 xl: {
18 width: '3rem',
19 height: '3rem'
20 }
21 }
22
23 getStyle () {
24 if (!this.size) return undefined
25
26 return this.sizes[this.size]
27 }
10} 28}
diff --git a/client/src/app/shared/shared-main/loaders/small-loader.component.html b/client/src/app/shared/shared-main/loaders/small-loader.component.html
deleted file mode 100644
index 7886f8918..000000000
--- a/client/src/app/shared/shared-main/loaders/small-loader.component.html
+++ /dev/null
@@ -1,3 +0,0 @@
1<div class="root" *ngIf="loading">
2 <div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div>
3</div>
diff --git a/client/src/app/shared/shared-main/loaders/small-loader.component.ts b/client/src/app/shared/shared-main/loaders/small-loader.component.ts
deleted file mode 100644
index 191877f14..000000000
--- a/client/src/app/shared/shared-main/loaders/small-loader.component.ts
+++ /dev/null
@@ -1,11 +0,0 @@
1import { Component, Input } from '@angular/core'
2
3@Component({
4 selector: 'my-small-loader',
5 styleUrls: [ ],
6 templateUrl: './small-loader.component.html'
7})
8
9export class SmallLoaderComponent {
10 @Input() loading: boolean
11}
diff --git a/client/src/app/shared/shared-main/misc/channels-setup-message.component.html b/client/src/app/shared/shared-main/misc/channels-setup-message.component.html
index 3fe888a35..539df06bd 100644
--- a/client/src/app/shared/shared-main/misc/channels-setup-message.component.html
+++ b/client/src/app/shared/shared-main/misc/channels-setup-message.component.html
@@ -1,4 +1,4 @@
1<div *ngIf="hasChannelNotConfigured()" class="channels-setup-message alert alert-info"> 1<div *ngIf="hasChannelNotConfigured()" class="channels-setup-message alert pt-alert-primary">
2 <my-global-icon iconName="tip"></my-global-icon> 2 <my-global-icon iconName="tip"></my-global-icon>
3 3
4 <div> 4 <div>
diff --git a/client/src/app/shared/shared-main/misc/channels-setup-message.component.scss b/client/src/app/shared/shared-main/misc/channels-setup-message.component.scss
index 7dcba2ca5..2aa176e1b 100644
--- a/client/src/app/shared/shared-main/misc/channels-setup-message.component.scss
+++ b/client/src/app/shared/shared-main/misc/channels-setup-message.component.scss
@@ -5,28 +5,24 @@
5 display: flex; 5 display: flex;
6 align-items: center; 6 align-items: center;
7 justify-content: center; 7 justify-content: center;
8}
8 9
9 my-global-icon { 10my-global-icon {
10 width: 32px; 11 @include apply-svg-color(pvar(--mainColor));
11 align-self: flex-start;
12 12
13 ::ng-deep { 13 width: 32px;
14 svg { 14 align-self: flex-start;
15 fill: #0c5460;
16 }
17 }
18 15
19 + div { 16 + div {
20 margin-left: 10px; 17 margin-left: 10px;
21 text-align: center; 18 text-align: center;
19 }
20}
22 21
23 a.channels-settings-link { 22.channels-settings-link {
24 @include peertube-button-link; 23 @include peertube-button-link;
25 @include grey-button; 24 @include grey-button;
26 25
27 height: fit-content; 26 height: fit-content;
28 margin-top: 10px; 27 margin-top: 10px;
29 }
30 }
31 }
32} 28}
diff --git a/client/src/app/shared/shared-main/misc/list-overflow.component.html b/client/src/app/shared/shared-main/misc/list-overflow.component.html
index b2e0982f1..6f29eaefa 100644
--- a/client/src/app/shared/shared-main/misc/list-overflow.component.html
+++ b/client/src/app/shared/shared-main/misc/list-overflow.component.html
@@ -1,18 +1,22 @@
1<div #itemsParent class="d-flex align-items-center text-nowrap w-100 list-overflow-parent"> 1<div #itemsParent class="list-overflow-parent">
2 <span [id]="getId(id)" #itemsRendered *ngFor="let item of items; index as id"> 2 <span [id]="getId(id)" #itemsRendered *ngFor="let item of items; index as id">
3 <ng-container *ngTemplateOutlet="itemTemplate; context: {item: item}"></ng-container> 3 <ng-container *ngTemplateOutlet="itemTemplate; context: {item: item}"></ng-container>
4 </span> 4 </span>
5 5
6 <ng-container *ngIf="isMenuDisplayed()"> 6 <ng-container *ngIf="isMenuDisplayed()">
7 <button *ngIf="isInMobileView" class="btn btn-outline-secondary btn-sm list-overflow-menu" (click)="toggleModal()"> 7 <button *ngIf="isInMobileView" class="btn btn-outline-secondary btn-sm list-overflow-menu" (click)="toggleModal()">
8 <span class="glyphicon glyphicon-chevron-down"></span> 8 <span class="chevron-down"></span>
9 </button> 9 </button>
10 10
11 <div *ngIf="!isInMobileView" class="list-overflow-menu" ngbDropdown container="body" #dropdown="ngbDropdown" (mouseleave)="closeDropdownIfHovered(dropdown)" (mouseenter)="openDropdownOnHover(dropdown)"> 11 <div
12 *ngIf="!isInMobileView" class="list-overflow-menu"
13 ngbDropdown container="body" #dropdown="ngbDropdown"
14 (mouseleave)="closeDropdownIfHovered(dropdown)" (mouseenter)="openDropdownOnHover(dropdown)"
15 >
12 <button class="btn btn-outline-secondary btn-sm" [ngClass]="{ 'route-active': active }" 16 <button class="btn btn-outline-secondary btn-sm" [ngClass]="{ 'route-active': active }"
13 ngbDropdownAnchor (click)="dropdownAnchorClicked(dropdown)" role="button" 17 ngbDropdownAnchor (click)="dropdownAnchorClicked(dropdown)" role="button"
14 > 18 >
15 <span class="glyphicon glyphicon-chevron-down"></span> 19 <span class="chevron-down"></span>
16 </button> 20 </button>
17 21
18 <div ngbDropdownMenu> 22 <div ngbDropdownMenu>
diff --git a/client/src/app/shared/shared-main/misc/list-overflow.component.scss b/client/src/app/shared/shared-main/misc/list-overflow.component.scss
index 19c055fd3..b06418568 100644
--- a/client/src/app/shared/shared-main/misc/list-overflow.component.scss
+++ b/client/src/app/shared/shared-main/misc/list-overflow.component.scss
@@ -7,6 +7,9 @@
7 7
8.list-overflow-parent { 8.list-overflow-parent {
9 overflow: hidden; 9 overflow: hidden;
10 display: flex;
11 // For the menu icon
12 max-width: calc(100vw - 30px);
10} 13}
11 14
12.list-overflow-menu { 15.list-overflow-menu {
diff --git a/client/src/app/shared/shared-main/misc/list-overflow.component.ts b/client/src/app/shared/shared-main/misc/list-overflow.component.ts
index fbc481093..541991f74 100644
--- a/client/src/app/shared/shared-main/misc/list-overflow.component.ts
+++ b/client/src/app/shared/shared-main/misc/list-overflow.component.ts
@@ -15,6 +15,9 @@ import {
15} from '@angular/core' 15} from '@angular/core'
16import { ScreenService } from '@app/core' 16import { ScreenService } from '@app/core'
17import { NgbDropdown, NgbModal } from '@ng-bootstrap/ng-bootstrap' 17import { NgbDropdown, NgbModal } from '@ng-bootstrap/ng-bootstrap'
18import * as debug from 'debug'
19
20const logger = debug('peertube:main:ListOverflowItem')
18 21
19export interface ListOverflowItem { 22export interface ListOverflowItem {
20 label: string 23 label: string
@@ -37,7 +40,6 @@ export class ListOverflowComponent<T extends ListOverflowItem> implements AfterV
37 40
38 showItemsUntilIndexExcluded: number 41 showItemsUntilIndexExcluded: number
39 active = false 42 active = false
40 isInTouchScreen = false
41 isInMobileView = false 43 isInMobileView = false
42 44
43 private openedOnHover = false 45 private openedOnHover = false
@@ -58,13 +60,14 @@ export class ListOverflowComponent<T extends ListOverflowItem> implements AfterV
58 60
59 @HostListener('window:resize') 61 @HostListener('window:resize')
60 onWindowResize () { 62 onWindowResize () {
61 this.isInTouchScreen = !!this.screenService.isInTouchScreen()
62 this.isInMobileView = !!this.screenService.isInMobileView() 63 this.isInMobileView = !!this.screenService.isInMobileView()
63 64
64 const parentWidth = this.parent.nativeElement.getBoundingClientRect().width 65 const parentWidth = this.parent.nativeElement.getBoundingClientRect().width
65 let showItemsUntilIndexExcluded: number 66 let showItemsUntilIndexExcluded: number
66 let accWidth = 0 67 let accWidth = 0
67 68
69 logger('Parent width is %d', parentWidth)
70
68 for (const [ index, el ] of this.itemsRendered.toArray().entries()) { 71 for (const [ index, el ] of this.itemsRendered.toArray().entries()) {
69 accWidth += el.nativeElement.getBoundingClientRect().width 72 accWidth += el.nativeElement.getBoundingClientRect().width
70 if (showItemsUntilIndexExcluded === undefined) { 73 if (showItemsUntilIndexExcluded === undefined) {
@@ -76,6 +79,8 @@ export class ListOverflowComponent<T extends ListOverflowItem> implements AfterV
76 e.style.visibility = shouldBeVisible ? 'inherit' : 'hidden' 79 e.style.visibility = shouldBeVisible ? 'inherit' : 'hidden'
77 } 80 }
78 81
82 logger('Accumulated children width is %d so exclude index is %d', accWidth, showItemsUntilIndexExcluded)
83
79 this.showItemsUntilIndexExcluded = showItemsUntilIndexExcluded 84 this.showItemsUntilIndexExcluded = showItemsUntilIndexExcluded
80 this.cdr.markForCheck() 85 this.cdr.markForCheck()
81 } 86 }
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.html b/client/src/app/shared/shared-main/misc/simple-search-input.component.html
index 1e2f6c6a9..386d26116 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,17 +1,10 @@
1<div class="root"> 1<div class="root">
2 <div class="input-group has-feedback has-clear"> 2 <div class="input-group has-clear">
3 <input 3 <input #ref type="text" class="last-in-group"
4 #ref 4 [(ngModel)]="value" (keyup.enter)="sendSearch()" [hidden]="!inputShown" [name]="name" [placeholder]="placeholder"
5 type="text"
6 [(ngModel)]="value"
7 (keyup.enter)="sendSearch()"
8 [hidden]="!inputShown"
9 [name]="name"
10 [placeholder]="placeholder"
11 > 5 >
12 6
13 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="onResetFilter()"></a> 7 <my-global-icon iconName="cross" role="button" class="form-control-clear" title="Clear filter" i18n-title (click)="onResetFilter()"></my-global-icon>
14 <span class="sr-only" i18n>Clear filters</span>
15 </div> 8 </div>
16 9
17 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon> 10 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon>
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
index d5fcff760..ee0f7a8d2 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
@@ -5,7 +5,7 @@
5 display: flex; 5 display: flex;
6} 6}
7 7
8my-global-icon { 8.root > my-global-icon {
9 @include margin-left(10px); 9 @include margin-left(10px);
10 10
11 height: 28px; 11 height: 28px;
@@ -25,3 +25,7 @@ input {
25 box-shadow: 0 0 5px 0 #a5a5a5; 25 box-shadow: 0 0 5px 0 #a5a5a5;
26 } 26 }
27} 27}
28
29.input-group > my-global-icon {
30 width: 20px;
31}
diff --git a/client/src/app/shared/shared-main/misc/top-menu-dropdown.component.html b/client/src/app/shared/shared-main/misc/top-menu-dropdown.component.html
index d884e75b2..d96fdbdc6 100644
--- a/client/src/app/shared/shared-main/misc/top-menu-dropdown.component.html
+++ b/client/src/app/shared/shared-main/misc/top-menu-dropdown.component.html
@@ -1,7 +1,13 @@
1<div class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed, 'no-scroll': isModalOpened }"> 1<div class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed, 'no-scroll': isModalOpened }">
2 <ng-container *ngFor="let menuEntry of menuEntries; index as id"> 2 <ng-container *ngFor="let menuEntry of menuEntries; index as id">
3 3
4 <a *ngIf="menuEntry.routerLink && isDisplayed(menuEntry)" [routerLink]="menuEntry.routerLink" routerLinkActive="active" class="title-page title-page-settings" #routerLink (click)="onActiveLinkScrollToTop(routerLink)">{{ menuEntry.label }}</a> 4 <a
5 *ngIf="menuEntry.routerLink && isDisplayed(menuEntry)" class="sub-menu-entry"
6 [routerLink]="menuEntry.routerLink" routerLinkActive="active" #routerLink
7 (click)="onActiveLinkScrollToTop(routerLink)"
8 >
9 {{ menuEntry.label }}
10 </a>
5 11
6 <div *ngIf="!menuEntry.routerLink && isDisplayed(menuEntry)" ngbDropdown class="parent-entry" 12 <div *ngIf="!menuEntry.routerLink && isDisplayed(menuEntry)" ngbDropdown class="parent-entry"
7 #dropdown="ngbDropdown" autoClose="true" container="body"> 13 #dropdown="ngbDropdown" autoClose="true" container="body">
@@ -10,7 +16,7 @@
10 tabindex=0 16 tabindex=0
11 [ngClass]="{ active: !!suffixLabels[menuEntry.label] }" 17 [ngClass]="{ active: !!suffixLabels[menuEntry.label] }"
12 (click)="openModal(id)" (keydown.enter)="openModal(id)" 18 (click)="openModal(id)" (keydown.enter)="openModal(id)"
13 role="button" class="title-page title-page-settings"> 19 role="button" class="sub-menu-entry">
14 <ng-container i18n>{{ menuEntry.label }}</ng-container> 20 <ng-container i18n>{{ menuEntry.label }}</ng-container>
15 </span> 21 </span>
16 22
@@ -19,7 +25,7 @@
19 tabindex=0 25 tabindex=0
20 [ngClass]="{ active: !!suffixLabels[menuEntry.label] }" ngbDropdownAnchor 26 [ngClass]="{ active: !!suffixLabels[menuEntry.label] }" ngbDropdownAnchor
21 (click)="dropdownAnchorClicked(dropdown)" (keydown.enter)="dropdownAnchorClicked(dropdown)" 27 (click)="dropdownAnchorClicked(dropdown)" (keydown.enter)="dropdownAnchorClicked(dropdown)"
22 role="button" class="title-page title-page-settings" 28 role="button" class="sub-menu-entry"
23 > 29 >
24 <ng-container i18n>{{ menuEntry.label }}</ng-container> 30 <ng-container i18n>{{ menuEntry.label }}</ng-container>
25 </span> 31 </span>
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 5629640bc..89f43239f 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -34,7 +34,7 @@ import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditBu
34import { CustomPageService } from './custom-page' 34import { CustomPageService } from './custom-page'
35import { DateToggleComponent } from './date' 35import { DateToggleComponent } from './date'
36import { FeedComponent } from './feeds' 36import { FeedComponent } from './feeds'
37import { LoaderComponent, SmallLoaderComponent } from './loaders' 37import { LoaderComponent } from './loaders'
38import { 38import {
39 ChannelsSetupMessageComponent, 39 ChannelsSetupMessageComponent,
40 HelpComponent, 40 HelpComponent,
@@ -97,7 +97,6 @@ import { VideoChannelService } from './video-channel'
97 FeedComponent, 97 FeedComponent,
98 98
99 LoaderComponent, 99 LoaderComponent,
100 SmallLoaderComponent,
101 100
102 ChannelsSetupMessageComponent, 101 ChannelsSetupMessageComponent,
103 HelpComponent, 102 HelpComponent,
@@ -157,7 +156,6 @@ import { VideoChannelService } from './video-channel'
157 FeedComponent, 156 FeedComponent,
158 157
159 LoaderComponent, 158 LoaderComponent,
160 SmallLoaderComponent,
161 159
162 ChannelsSetupMessageComponent, 160 ChannelsSetupMessageComponent,
163 HelpComponent, 161 HelpComponent,
diff --git a/client/src/app/shared/shared-main/users/user-quota.component.html b/client/src/app/shared/shared-main/users/user-quota.component.html
index dd1fc20d0..0e0d38c2a 100644
--- a/client/src/app/shared/shared-main/users/user-quota.component.html
+++ b/client/src/app/shared/shared-main/users/user-quota.component.html
@@ -12,7 +12,7 @@
12 <div *ngIf="hasDailyQuota()" class="mt-3"> 12 <div *ngIf="hasDailyQuota()" class="mt-3">
13 <label class="user-quota-title" tabindex="0" i18n>Daily video quota</label> 13 <label class="user-quota-title" tabindex="0" i18n>Daily video quota</label>
14 <div class="progress" tabindex="0" [ngbTooltip]="titleVideoQuotaDaily()"> 14 <div class="progress" tabindex="0" [ngbTooltip]="titleVideoQuotaDaily()">
15 <div class="progress-bar secondary" role="progressbar" [style]="{ width: userVideoQuotaDailyPercentage + '%' }" 15 <div class="progress-bar" role="progressbar" [style]="{ width: userVideoQuotaDailyPercentage + '%' }"
16 [attr.aria-valuenow]="userVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.videoQuotaDaily"></div> 16 [attr.aria-valuenow]="userVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.videoQuotaDaily"></div>
17 <span>{{ userVideoQuotaUsedDaily | bytes: 1 }}</span> 17 <span>{{ userVideoQuotaUsedDaily | bytes: 1 }}</span>
18 <span>{{ userVideoQuotaDaily }}</span> 18 <span>{{ userVideoQuotaDaily }}</span>
diff --git a/client/src/app/shared/shared-main/users/user-quota.component.scss b/client/src/app/shared/shared-main/users/user-quota.component.scss
index 70571bde6..f3e86ce78 100644
--- a/client/src/app/shared/shared-main/users/user-quota.component.scss
+++ b/client/src/app/shared/shared-main/users/user-quota.component.scss
@@ -1,11 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4label {
5 font-weight: $font-regular;
6 font-size: 100%;
7}
8
9.user-quota { 4.user-quota {
10 label { 5 label {
11 @include margin-right(5px); 6 @include margin-right(5px);
diff --git a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts
index 00ebe5bc6..0f3afd116 100644
--- a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts
+++ b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts
@@ -18,7 +18,7 @@ export class VideoCaptionService {
18 private restExtractor: RestExtractor 18 private restExtractor: RestExtractor
19 ) {} 19 ) {}
20 20
21 listCaptions (videoId: number | string): Observable<ResultList<VideoCaption>> { 21 listCaptions (videoId: string): Observable<ResultList<VideoCaption>> {
22 return this.authHttp.get<ResultList<VideoCaption>>(`${VideoService.BASE_VIDEO_URL}/${videoId}/captions`) 22 return this.authHttp.get<ResultList<VideoCaption>>(`${VideoService.BASE_VIDEO_URL}/${videoId}/captions`)
23 .pipe( 23 .pipe(
24 switchMap(captionsResult => { 24 switchMap(captionsResult => {
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
index 32376bf62..62bd94349 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
@@ -27,6 +27,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
27 videosCount?: number 27 videosCount?: number
28 28
29 viewsPerDay?: ViewsPerDate[] 29 viewsPerDay?: ViewsPerDate[]
30 totalViews?: number
30 31
31 static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) { 32 static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) {
32 return Actor.GET_ACTOR_AVATAR_URL(actor, size) 33 return Actor.GET_ACTOR_AVATAR_URL(actor, size)
@@ -74,6 +75,10 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
74 this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date) })) 75 this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date) }))
75 } 76 }
76 77
78 if (hash.totalViews !== null && hash.totalViews !== undefined) {
79 this.totalViews = hash.totalViews
80 }
81
77 if (hash.ownerAccount) { 82 if (hash.ownerAccount) {
78 this.ownerAccount = hash.ownerAccount 83 this.ownerAccount = hash.ownerAccount
79 this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host) 84 this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index 022bb95ad..2e4ab87d7 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -1,6 +1,6 @@
1import { AuthUser } from '@app/core' 1import { AuthUser } from '@app/core'
2import { User } from '@app/core/users/user.model' 2import { User } from '@app/core/users/user.model'
3import { durationToString, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers' 3import { durationToString, prepareIcu, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
4import { Actor } from '@app/shared/shared-main/account/actor.model' 4import { Actor } from '@app/shared/shared-main/account/actor.model'
5import { buildVideoWatchPath } from '@shared/core-utils' 5import { buildVideoWatchPath } from '@shared/core-utils'
6import { peertubeTranslate } from '@shared/core-utils/i18n' 6import { peertubeTranslate } from '@shared/core-utils/i18n'
@@ -19,6 +19,9 @@ import {
19} from '@shared/models' 19} from '@shared/models'
20 20
21export class Video implements VideoServerModel { 21export class Video implements VideoServerModel {
22 private static readonly viewsICU = prepareIcu($localize`{views, plural, =0 {No view} =1 {1 view} other {{views} views}}`)
23 private static readonly viewersICU = prepareIcu($localize`{viewers, plural, =0 {No viewers} =1 {1 viewer} other {{viewers} viewers}}`)
24
22 byVideoChannel: string 25 byVideoChannel: string
23 byAccount: string 26 byAccount: string
24 27
@@ -269,12 +272,10 @@ export class Video implements VideoServerModel {
269 } 272 }
270 273
271 getExactNumberOfViews () { 274 getExactNumberOfViews () {
272 if (this.views < 1000) return ''
273
274 if (this.isLive) { 275 if (this.isLive) {
275 return $localize`${this.views} viewers` 276 return Video.viewersICU({ viewers: this.viewers }, $localize`${this.viewers} viewer(s)`)
276 } 277 }
277 278
278 return $localize`${this.views} views` 279 return Video.viewsICU({ views: this.views }, $localize`{${this.views} view(s)}`)
279 } 280 }
280} 281}
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 9efa1a24e..4fbc4f7f6 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -1,5 +1,5 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { from, Observable } from 'rxjs' 2import { from, Observable, of } from 'rxjs'
3import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators' 3import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators'
4import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' 4import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
@@ -24,6 +24,7 @@ import {
24 VideoTranscodingCreate, 24 VideoTranscodingCreate,
25 VideoUpdate 25 VideoUpdate
26} from '@shared/models' 26} from '@shared/models'
27import { VideoSource } from '@shared/models/videos/video-source'
27import { environment } from '../../../../environments/environment' 28import { environment } from '../../../../environments/environment'
28import { Account } from '../account/account.model' 29import { Account } from '../account/account.model'
29import { AccountService } from '../account/account.service' 30import { AccountService } from '../account/account.service'
@@ -323,19 +324,33 @@ export class VideoService {
323 ) 324 )
324 } 325 }
325 326
326 setVideoLike (id: number) { 327 getSource (videoId: number) {
328 return this.authHttp
329 .get<{ source: VideoSource }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source')
330 .pipe(
331 catchError(err => {
332 if (err.status === 404) {
333 return of(undefined)
334 }
335
336 this.restExtractor.handleError(err)
337 })
338 )
339 }
340
341 setVideoLike (id: string) {
327 return this.setVideoRate(id, 'like') 342 return this.setVideoRate(id, 'like')
328 } 343 }
329 344
330 setVideoDislike (id: number) { 345 setVideoDislike (id: string) {
331 return this.setVideoRate(id, 'dislike') 346 return this.setVideoRate(id, 'dislike')
332 } 347 }
333 348
334 unsetVideoLike (id: number) { 349 unsetVideoLike (id: string) {
335 return this.setVideoRate(id, 'none') 350 return this.setVideoRate(id, 'none')
336 } 351 }
337 352
338 getUserVideoRating (id: number) { 353 getUserVideoRating (id: string) {
339 const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating' 354 const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating'
340 355
341 return this.authHttp.get<UserVideoRate>(url) 356 return this.authHttp.get<UserVideoRate>(url)
@@ -451,7 +466,7 @@ export class VideoService {
451 } 466 }
452 } 467 }
453 468
454 private setVideoRate (id: number, rateType: UserVideoRateType) { 469 private setVideoRate (id: string, rateType: UserVideoRateType) {
455 const url = `${VideoService.BASE_VIDEO_URL}/${id}/rate` 470 const url = `${VideoService.BASE_VIDEO_URL}/${id}/rate`
456 const body: UserVideoRateUpdate = { 471 const body: UserVideoRateUpdate = {
457 rating: rateType 472 rating: rateType
diff --git a/client/src/app/shared/shared-moderation/account-block-badges.component.html b/client/src/app/shared/shared-moderation/account-block-badges.component.html
index feac707c2..fd3709676 100644
--- a/client/src/app/shared/shared-moderation/account-block-badges.component.html
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.html
@@ -1,4 +1,4 @@
1<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> 1<span *ngIf="account.mutedByUser" class="pt-badge badge-danger" i18n>Muted</span>
2<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span> 2<span *ngIf="account.mutedServerByUser" class="pt-badge badge-danger" i18n>Instance muted</span>
3<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span> 3<span *ngIf="account.mutedByInstance" class="pt-badge badge-danger" i18n>Muted by your instance</span>
4<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span> 4<span *ngIf="account.mutedServerByInstance" class="pt-badge badge-danger" i18n>Instance muted by your instance</span>
diff --git a/client/src/app/shared/shared-moderation/account-block-badges.component.scss b/client/src/app/shared/shared-moderation/account-block-badges.component.scss
index ccc3666aa..301d8305e 100644
--- a/client/src/app/shared/shared-moderation/account-block-badges.component.scss
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.scss
@@ -1,9 +1,8 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.badge { 4.pt-badge {
5 @include margin-right(10px); 5 @include margin-right(10px);
6 6
7 height: fit-content;
8 font-size: 12px; 7 font-size: 12px;
9} 8}
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.html b/client/src/app/shared/shared-moderation/account-blocklist.component.html
index a4f81d824..fef24cfdf 100644
--- a/client/src/app/shared/shared-moderation/account-blocklist.component.html
+++ b/client/src/app/shared/shared-moderation/account-blocklist.component.html
@@ -11,7 +11,7 @@
11> 11>
12 <ng-template pTemplate="caption"> 12 <ng-template pTemplate="caption">
13 <div class="caption"> 13 <div class="caption">
14 <div class="ml-auto"> 14 <div class="ms-auto">
15 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 15 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
16 </div> 16 </div>
17 </div> 17 </div>
@@ -33,7 +33,7 @@
33 <td> 33 <td>
34 <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> 34 <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
35 <div class="chip two-lines"> 35 <div class="chip two-lines">
36 <my-actor-avatar [account]="accountBlock.blockedAccount" size="32"></my-actor-avatar> 36 <my-actor-avatar [actor]="accountBlock.blockedAccount" actorType="account" size="32"></my-actor-avatar>
37 <div> 37 <div>
38 {{ accountBlock.blockedAccount.displayName }} 38 {{ accountBlock.blockedAccount.displayName }}
39 <span class="muted">{{ accountBlock.blockedAccount.nameWithHost }}</span> 39 <span class="muted">{{ accountBlock.blockedAccount.nameWithHost }}</span>
@@ -48,7 +48,7 @@
48 48
49 <ng-template pTemplate="emptymessage"> 49 <ng-template pTemplate="emptymessage">
50 <tr> 50 <tr>
51 <td colspan="6"> 51 <td colspan="3">
52 <div class="no-results"> 52 <div class="no-results">
53 <ng-container *ngIf="search" i18n>No account found matching current filters.</ng-container> 53 <ng-container *ngIf="search" i18n>No account found matching current filters.</ng-container>
54 <ng-container *ngIf="!search" i18n>No account found.</ng-container> 54 <ng-container *ngIf="!search" i18n>No account found.</ng-container>
diff --git a/client/src/app/shared/shared-moderation/report-modals/report.component.html b/client/src/app/shared/shared-moderation/report-modals/report.component.html
index 6c99180ef..8e0b0993c 100644
--- a/client/src/app/shared/shared-moderation/report-modals/report.component.html
+++ b/client/src/app/shared/shared-moderation/report-modals/report.component.html
@@ -8,11 +8,11 @@
8 <form novalidate [formGroup]="form" (ngSubmit)="report()"> 8 <form novalidate [formGroup]="form" (ngSubmit)="report()">
9 9
10 <div class="row"> 10 <div class="row">
11 <div class="col-5 form-group"> 11 <div class="col-5">
12 12
13 <label i18n for="reportPredefinedReasons">What is the issue?</label> 13 <label i18n for="reportPredefinedReasons">What is the issue?</label>
14 14
15 <div class="ml-2 mt-2 d-flex flex-column"> 15 <div class="ms-2 mt-2 d-flex flex-column">
16 <ng-container formGroupName="predefinedReasons"> 16 <ng-container formGroupName="predefinedReasons">
17 17
18 <div class="form-group" *ngFor="let reason of predefinedReasons"> 18 <div class="form-group" *ngFor="let reason of predefinedReasons">
@@ -29,7 +29,6 @@
29 29
30 </ng-container> 30 </ng-container>
31 </div> 31 </div>
32
33 </div> 32 </div>
34 33
35 <div class="col-7"> 34 <div class="col-7">
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.html b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
index afac108fc..51ca0b9d6 100644
--- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
@@ -8,38 +8,32 @@
8 <form novalidate [formGroup]="form" (ngSubmit)="report()"> 8 <form novalidate [formGroup]="form" (ngSubmit)="report()">
9 9
10 <div class="row"> 10 <div class="row">
11 <div class="col-5 form-group"> 11 <div class="col-12 col-md-5">
12 <label i18n for="reportPredefinedReasons">What is the issue?</label>
12 13
13 <label i18n for="reportPredefinedReasons">What is the issue?</label> 14 <div class="ms-2 mt-2 d-flex flex-column">
15 <ng-container formGroupName="predefinedReasons">
14 16
15 <div class="ml-2 mt-2 d-flex flex-column"> 17 <div class="form-group" *ngFor="let reason of predefinedReasons">
16 <ng-container formGroupName="predefinedReasons"> 18 <my-peertube-checkbox [inputName]="reason.id" [formControlName]="reason.id" [labelText]="reason.label">
19 <ng-template *ngIf="reason.help" ptTemplate="help">
20 <div [innerHTML]="reason.help"></div>
21 </ng-template>
17 22
18 <div class="form-group" *ngFor="let reason of predefinedReasons"> 23 <ng-container *ngIf="reason.description" ngProjectAs="description">
19 <my-peertube-checkbox [inputName]="reason.id" [formControlName]="reason.id" [labelText]="reason.label"> 24 <div [innerHTML]="reason.description"></div>
20 <ng-template *ngIf="reason.help" ptTemplate="help"> 25 </ng-container>
21 <div [innerHTML]="reason.help"></div> 26 </my-peertube-checkbox>
22 </ng-template> 27 </div>
23
24 <ng-container *ngIf="reason.description" ngProjectAs="description">
25 <div [innerHTML]="reason.description"></div>
26 </ng-container>
27 </my-peertube-checkbox>
28 </div>
29
30 </ng-container>
31 </div>
32 28
29 </ng-container>
30 </div>
33 </div> 31 </div>
34 32
35 <div class="col-7"> 33 <div class="col-12 col-md-7">
36 <div class="row justify-content-center"> 34 <my-embed [video]="video"></my-embed>
37 <div class="col-12 col-lg-9 mb-2">
38 <my-embed [video]="video"></my-embed>
39 </div>
40 </div>
41 35
42 <div class="mb-1 start-at" formGroupName="timestamp"> 36 <div class="mb-1 mt-3 start-at" formGroupName="timestamp">
43 <my-peertube-checkbox 37 <my-peertube-checkbox
44 formControlName="hasStart" 38 formControlName="hasStart"
45 i18n-labelText labelText="Start at" 39 i18n-labelText labelText="Start at"
diff --git a/client/src/app/shared/shared-moderation/server-blocklist.component.html b/client/src/app/shared/shared-moderation/server-blocklist.component.html
index 1a320e9a4..bc47bf26f 100644
--- a/client/src/app/shared/shared-moderation/server-blocklist.component.html
+++ b/client/src/app/shared/shared-moderation/server-blocklist.component.html
@@ -19,7 +19,7 @@
19 </a> 19 </a>
20 </div> 20 </div>
21 21
22 <div class="ml-auto"> 22 <div class="ms-auto">
23 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 23 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
24 </div> 24 </div>
25 </div> 25 </div>
@@ -41,7 +41,7 @@
41 <td> 41 <td>
42 <a [href]="'https://' + serverBlock.blockedServer.host" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer"> 42 <a [href]="'https://' + serverBlock.blockedServer.host" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
43 {{ serverBlock.blockedServer.host }} 43 {{ serverBlock.blockedServer.host }}
44 <span class="glyphicon glyphicon-new-window"></span> 44 <my-global-icon iconName="external-link"></my-global-icon>
45 </a> 45 </a>
46 </td> 46 </td>
47 <td>{{ serverBlock.createdAt | date: 'short' }}</td> 47 <td>{{ serverBlock.createdAt | date: 'short' }}</td>
@@ -50,7 +50,7 @@
50 50
51 <ng-template pTemplate="emptymessage"> 51 <ng-template pTemplate="emptymessage">
52 <tr> 52 <tr>
53 <td colspan="6"> 53 <td colspan="3">
54 <div class="no-results"> 54 <div class="no-results">
55 <ng-container *ngIf="search" i18n>No server found matching current filters.</ng-container> 55 <ng-container *ngIf="search" i18n>No server found matching current filters.</ng-container>
56 <ng-container *ngIf="!search" i18n>No server found.</ng-container> 56 <ng-container *ngIf="!search" i18n>No server found.</ng-container>
diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.scss b/client/src/app/shared/shared-moderation/user-ban-modal.component.scss
index 2c46c3d03..376fb1693 100644
--- a/client/src/app/shared/shared-moderation/user-ban-modal.component.scss
+++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.scss
@@ -2,7 +2,6 @@
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.description { 4.description {
5 font-size: 15px;
6 margin-bottom: 15px; 5 margin-bottom: 15px;
7} 6}
8 7
diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts
index 9edfac388..617408f2a 100644
--- a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts
+++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts
@@ -1,6 +1,7 @@
1import { forkJoin } from 'rxjs' 1import { forkJoin } from 'rxjs'
2import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 2import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { prepareIcu } from '@app/helpers'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -63,9 +64,16 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
63 forkJoin(observables) 64 forkJoin(observables)
64 .subscribe({ 65 .subscribe({
65 next: () => { 66 next: () => {
66 const message = Array.isArray(this.usersToBan) 67 let message: string
67 ? $localize`${this.usersToBan.length} users banned.` 68
68 : $localize`User ${this.usersToBan.username} banned.` 69 if (Array.isArray(this.usersToBan)) {
70 message = prepareIcu($localize`{count, plural, =1 {1 user banned.} other {{count} users banned.}}`)(
71 { count: this.usersToBan.length },
72 $localize`${this.usersToBan.length} users banned.`
73 )
74 } else {
75 message = $localize`User ${this.usersToBan.username} banned.`
76 }
69 77
70 this.notifier.success(message) 78 this.notifier.success(message)
71 79
@@ -79,7 +87,12 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
79 } 87 }
80 88
81 getModalTitle () { 89 getModalTitle () {
82 if (Array.isArray(this.usersToBan)) return $localize`Ban ${this.usersToBan.length} users` 90 if (Array.isArray(this.usersToBan)) {
91 return prepareIcu($localize`Ban {count, plural, =1 {1 user} other {{count} users}}`)(
92 { count: this.usersToBan.length },
93 $localize`Ban ${this.usersToBan.length} users`
94 )
95 }
83 96
84 return $localize`Ban "${this.usersToBan.username}"` 97 return $localize`Ban "${this.usersToBan.username}"`
85 } 98 }
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
index 787318c2c..c69a45c25 100644
--- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
@@ -100,7 +100,8 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
100 return 100 return
101 } 101 }
102 102
103 const message = $localize`If you remove user ${user.username}, you won't be able to create another with the same username!` 103 // eslint-disable-next-line max-len
104 const message = $localize`If you remove this user, you won't be able to create another user or channel with <strong>${user.username}</strong> username!`
104 const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`) 105 const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`)
105 if (res === false) return 106 if (res === false) return
106 107
diff --git a/client/src/app/shared/shared-moderation/video-block.component.scss b/client/src/app/shared/shared-moderation/video-block.component.scss
index 3061bbf15..7726eca11 100644
--- a/client/src/app/shared/shared-moderation/video-block.component.scss
+++ b/client/src/app/shared/shared-moderation/video-block.component.scss
@@ -6,6 +6,5 @@ textarea {
6} 6}
7 7
8.live-info { 8.live-info {
9 font-size: 15px;
10 margin: 40px 0 20px; 9 margin: 40px 0 20px;
11} 10}
diff --git a/client/src/app/shared/shared-moderation/video-block.component.ts b/client/src/app/shared/shared-moderation/video-block.component.ts
index 400913f02..f8b22a3f6 100644
--- a/client/src/app/shared/shared-moderation/video-block.component.ts
+++ b/client/src/app/shared/shared-moderation/video-block.component.ts
@@ -1,5 +1,6 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { prepareIcu } from '@app/helpers'
3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { Video } from '@app/shared/shared-main' 5import { Video } from '@app/shared/shared-main'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -80,9 +81,10 @@ export class VideoBlockComponent extends FormReactive implements OnInit {
80 this.videoBlocklistService.blockVideo(options) 81 this.videoBlocklistService.blockVideo(options)
81 .subscribe({ 82 .subscribe({
82 next: () => { 83 next: () => {
83 const message = this.isMultiple 84 const message = prepareIcu($localize`{count, plural, =1 {Blocked {videoName}.} other {Blocked {count} videos.}}`)(
84 ? $localize`Blocked ${this.videos.length} videos.` 85 { count: this.videos.length, videoName: this.getSingleVideo().name },
85 : $localize`Blocked ${this.getSingleVideo().name}` 86 $localize`Blocked ${this.videos.length} videos.`
87 )
86 88
87 this.notifier.success(message) 89 this.notifier.success(message)
88 this.hide() 90 this.hide()
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.html b/client/src/app/shared/shared-share-modal/video-share.component.html
index 67ca56516..b163d3581 100644
--- a/client/src/app/shared/shared-share-modal/video-share.component.html
+++ b/client/src/app/shared/shared-share-modal/video-share.component.html
@@ -8,7 +8,7 @@
8 <div class="modal-body"> 8 <div class="modal-body">
9 9
10 <div class="playlist" *ngIf="playlist"> 10 <div class="playlist" *ngIf="playlist">
11 <div class="title-page title-page-single" i18n *ngIf="video">Share the playlist</div> 11 <h5 i18n *ngIf="video">Share the playlist</h5>
12 12
13 <div *ngIf="isPrivatePlaylist()" class="alert-private alert alert-warning"> 13 <div *ngIf="isPrivatePlaylist()" class="alert-private alert alert-warning">
14 <div i18n>This playlist is private so you won't be able to share it with external users</div> 14 <div i18n>This playlist is private so you won't be able to share it with external users</div>
@@ -25,8 +25,7 @@
25 25
26 <ng-template ngbNavContent> 26 <ng-template ngbNavContent>
27 <div class="nav-content"> 27 <div class="nav-content">
28 28 <my-input-text [value]="getPlaylistUrl()" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-text>
29 <my-input-toggle-hidden [value]="getPlaylistUrl()" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-toggle-hidden>
30 </div> 29 </div>
31 </ng-template> 30 </ng-template>
32 </ng-container> 31 </ng-container>
@@ -46,10 +45,10 @@
46 45
47 <ng-template ngbNavContent> 46 <ng-template ngbNavContent>
48 <div class="nav-content"> 47 <div class="nav-content">
49 <my-input-toggle-hidden 48 <my-input-text
50 [value]="customizations.onlyEmbedUrl ? getPlaylistEmbedUrl() : getPlaylistIframeCode()" (change)="updateEmbedCode()" 49 [value]="customizations.onlyEmbedUrl ? getPlaylistEmbedUrl() : getPlaylistIframeCode()" (change)="updateEmbedCode()"
51 [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true" 50 [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"
52 ></my-input-toggle-hidden> 51 ></my-input-text>
53 52
54 <div i18n *ngIf="notSecure()" class="alert alert-warning"> 53 <div i18n *ngIf="notSecure()" class="alert alert-warning">
55 The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS websites). 54 The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS websites).
@@ -86,7 +85,7 @@
86 85
87 86
88 <div class="video" *ngIf="video"> 87 <div class="video" *ngIf="video">
89 <div class="title-page title-page-single" *ngIf="playlist" i18n>Share the video</div> 88 <h5 *ngIf="playlist" i18n>Share the video</h5>
90 89
91 <div *ngIf="isPrivateVideo()" class="alert-private alert alert-warning"> 90 <div *ngIf="isPrivateVideo()" class="alert-private alert alert-warning">
92 <div i18n>This video is private so you won't be able to share it with external users</div> 91 <div i18n>This video is private so you won't be able to share it with external users</div>
@@ -103,7 +102,7 @@
103 102
104 <ng-template ngbNavContent> 103 <ng-template ngbNavContent>
105 <div class="nav-content"> 104 <div class="nav-content">
106 <my-input-toggle-hidden [value]="getVideoUrl()" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-toggle-hidden> 105 <my-input-text [value]="getVideoUrl()" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-text>
107 </div> 106 </div>
108 </ng-template> 107 </ng-template>
109 </ng-container> 108 </ng-container>
@@ -123,10 +122,10 @@
123 122
124 <ng-template ngbNavContent> 123 <ng-template ngbNavContent>
125 <div class="nav-content"> 124 <div class="nav-content">
126 <my-input-toggle-hidden 125 <my-input-text
127 [value]="customizations.onlyEmbedUrl ? getVideoEmbedUrl() : getVideoIframeCode()" (ngModelChange)="updateEmbedCode()" 126 [value]="customizations.onlyEmbedUrl ? getVideoEmbedUrl() : getVideoIframeCode()" (ngModelChange)="updateEmbedCode()"
128 [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true" 127 [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"
129 ></my-input-toggle-hidden> 128 ></my-input-text>
130 129
131 <div i18n *ngIf="notSecure()" class="alert alert-warning"> 130 <div i18n *ngIf="notSecure()" class="alert alert-warning">
132 The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS websites). 131 The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS websites).
@@ -171,9 +170,8 @@
171 </div> 170 </div>
172 </div> 171 </div>
173 172
174 <div class="form-group"> 173 <div class="form-group" *ngIf="isInVideoEmbedTab()">
175 <my-peertube-checkbox 174 <my-peertube-checkbox
176 *ngIf="isInVideoEmbedTab()"
177 inputName="onlyEmbedUrl" [(ngModel)]="customizations.onlyEmbedUrl" 175 inputName="onlyEmbedUrl" [(ngModel)]="customizations.onlyEmbedUrl"
178 i18n-labelText labelText="Only display embed URL" 176 i18n-labelText labelText="Only display embed URL"
179 ></my-peertube-checkbox> 177 ></my-peertube-checkbox>
@@ -268,7 +266,7 @@
268 [attr.aria-expanded]="!isAdvancedCustomizationCollapsed" aria-controls="collapseBasic"> 266 [attr.aria-expanded]="!isAdvancedCustomizationCollapsed" aria-controls="collapseBasic">
269 267
270 <ng-container *ngIf="isAdvancedCustomizationCollapsed"> 268 <ng-container *ngIf="isAdvancedCustomizationCollapsed">
271 <span class="glyphicon glyphicon-menu-down"></span> 269 <span class="chevron-down"></span>
272 270
273 <ng-container i18n> 271 <ng-container i18n>
274 More customization 272 More customization
@@ -276,7 +274,7 @@
276 </ng-container> 274 </ng-container>
277 275
278 <ng-container *ngIf="!isAdvancedCustomizationCollapsed"> 276 <ng-container *ngIf="!isAdvancedCustomizationCollapsed">
279 <span class="glyphicon glyphicon-menu-up"></span> 277 <span class="chevron-up"></span>
280 278
281 <ng-container i18n> 279 <ng-container i18n>
282 Less customization 280 Less customization
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.scss b/client/src/app/shared/shared-share-modal/video-share.component.scss
index 44ebb13f4..6e80f8c76 100644
--- a/client/src/app/shared/shared-share-modal/video-share.component.scss
+++ b/client/src/app/shared/shared-share-modal/video-share.component.scss
@@ -1,14 +1,10 @@
1@use '_mixins' as *; 1@use '_mixins' as *;
2@use '_variables' as *; 2@use '_variables' as *;
3 3
4my-input-toggle-hidden { 4my-input-text {
5 width: 100%; 5 width: 100%;
6} 6}
7 7
8.title-page.title-page-single {
9 margin-top: 0;
10}
11
12.playlist { 8.playlist {
13 margin-bottom: 50px; 9 margin-bottom: 50px;
14} 10}
@@ -34,6 +30,10 @@ my-input-toggle-hidden {
34 margin-top: 20px; 30 margin-top: 20px;
35} 31}
36 32
33.alert-private {
34 margin-top: 0;
35}
36
37.filters { 37.filters {
38 margin-top: 30px; 38 margin-top: 30px;
39 39
@@ -46,20 +46,8 @@ my-input-toggle-hidden {
46 justify-content: center; 46 justify-content: center;
47 align-items: center; 47 align-items: center;
48 margin-top: 20px; 48 margin-top: 20px;
49 font-size: 16px;
50 font-weight: $font-semibold; 49 font-weight: $font-semibold;
51 cursor: pointer; 50 cursor: pointer;
52
53 .glyphicon {
54 @include margin-right(5px);
55 }
56 }
57
58 .form-group {
59 margin-bottom: 0;
60 height: 34px;
61 display: flex;
62 align-items: center;
63 } 51 }
64 52
65 .video-caption-block { 53 .video-caption-block {
@@ -88,3 +76,7 @@ my-input-toggle-hidden {
88 align-items: center; 76 align-items: center;
89 justify-content: space-between; 77 justify-content: space-between;
90} 78}
79
80h5 {
81 font-size: 1.15rem;
82}
diff --git a/client/src/app/shared/shared-tables/table-expander-icon.component.ts b/client/src/app/shared/shared-tables/table-expander-icon.component.ts
index 3756b475a..66bbfe6fb 100644
--- a/client/src/app/shared/shared-tables/table-expander-icon.component.ts
+++ b/client/src/app/shared/shared-tables/table-expander-icon.component.ts
@@ -4,7 +4,7 @@ import { Component, Input } from '@angular/core'
4 selector: 'my-table-expander-icon', 4 selector: 'my-table-expander-icon',
5 template: ` 5 template: `
6<span class="expander"> 6<span class="expander">
7 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> 7 <i [ngClass]="expanded ? 'chevron-down' : 'chevron-right'"></i>
8</span>` 8</span>`
9}) 9})
10export class TableExpanderIconComponent { 10export class TableExpanderIconComponent {
diff --git a/client/src/app/shared/shared-tables/video-cell.component.scss b/client/src/app/shared/shared-tables/video-cell.component.scss
index 7efb61502..5d26b02ef 100644
--- a/client/src/app/shared/shared-tables/video-cell.component.scss
+++ b/client/src/app/shared/shared-tables/video-cell.component.scss
@@ -59,13 +59,6 @@
59 color: pvar(--mainForegroundColor); 59 color: pvar(--mainForegroundColor);
60 line-height: 1rem; 60 line-height: 1rem;
61 61
62 div .glyphicon {
63 @include margin-left(0.1rem);
64
65 font-size: 80%;
66 color: #808080;
67 }
68
69 div + div { 62 div + div {
70 color: var(--greyForegroundColor); 63 color: var(--greyForegroundColor);
71 font-size: 11px; 64 font-size: 11px;
diff --git a/client/src/app/shared/shared-user-settings/user-interface-settings.component.html b/client/src/app/shared/shared-user-settings/user-interface-settings.component.html
index b739e881b..1e6e55e98 100644
--- a/client/src/app/shared/shared-user-settings/user-interface-settings.component.html
+++ b/client/src/app/shared/shared-user-settings/user-interface-settings.component.html
@@ -5,10 +5,10 @@
5 5
6 <div class="peertube-select-container"> 6 <div class="peertube-select-container">
7 <select formControlName="theme" id="theme" class="form-control"> 7 <select formControlName="theme" id="theme" class="form-control">
8 <option i18n value="instance-default">Instance default theme ({{ getDefaultThemeLabel() }})</option> 8 <option i18n value="instance-default">{{ instanceName }} default theme ({{ getDefaultInstanceThemeLabel() }})</option>
9 <option i18n value="default">{{ defaultThemeLabel }}</option> 9 <option i18n value="default">{{ getDefaultThemeLabel() }}</option>
10 10
11 <option *ngFor="let theme of availableThemes" [value]="theme">{{ capitalizeFirstLetter(theme) }}</option> 11 <option *ngFor="let theme of availableThemes" [value]="theme.id">{{ theme.label }}</option>
12 </select> 12 </select>
13 </div> 13 </div>
14 </div> 14 </div>
diff --git a/client/src/app/shared/shared-user-settings/user-interface-settings.component.scss b/client/src/app/shared/shared-user-settings/user-interface-settings.component.scss
index 2fc245ace..da8202594 100644
--- a/client/src/app/shared/shared-user-settings/user-interface-settings.component.scss
+++ b/client/src/app/shared/shared-user-settings/user-interface-settings.component.scss
@@ -1,11 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4label {
5 font-weight: $font-regular;
6 font-size: 100%;
7}
8
9input[type=submit] { 4input[type=submit] {
10 @include peertube-button; 5 @include peertube-button;
11 @include orange-button; 6 @include orange-button;
diff --git a/client/src/app/shared/shared-user-settings/user-interface-settings.component.ts b/client/src/app/shared/shared-user-settings/user-interface-settings.component.ts
index 932db498a..13e2e5424 100644
--- a/client/src/app/shared/shared-user-settings/user-interface-settings.component.ts
+++ b/client/src/app/shared/shared-user-settings/user-interface-settings.component.ts
@@ -1,9 +1,9 @@
1import { Subject, Subscription } from 'rxjs' 1import { Subject, Subscription } from 'rxjs'
2import { Component, Input, OnDestroy, OnInit } from '@angular/core' 2import { Component, Input, OnDestroy, OnInit } from '@angular/core'
3import { AuthService, Notifier, ServerService, UserService } from '@app/core' 3import { AuthService, Notifier, ServerService, ThemeService, UserService } from '@app/core'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { capitalizeFirstLetter } from '@root-helpers/string'
6import { HTMLServerConfig, User, UserUpdateMe } from '@shared/models' 5import { HTMLServerConfig, User, UserUpdateMe } from '@shared/models'
6import { SelectOptionsItem } from 'src/types'
7 7
8@Component({ 8@Component({
9 selector: 'my-user-interface-settings', 9 selector: 'my-user-interface-settings',
@@ -16,10 +16,9 @@ export class UserInterfaceSettingsComponent extends FormReactive implements OnIn
16 @Input() notifyOnUpdate = true 16 @Input() notifyOnUpdate = true
17 @Input() userInformationLoaded: Subject<any> 17 @Input() userInformationLoaded: Subject<any>
18 18
19 availableThemes: SelectOptionsItem[]
19 formValuesWatcher: Subscription 20 formValuesWatcher: Subscription
20 21
21 defaultThemeLabel = $localize`Light/Orange`
22
23 private serverConfig: HTMLServerConfig 22 private serverConfig: HTMLServerConfig
24 23
25 constructor ( 24 constructor (
@@ -27,19 +26,21 @@ export class UserInterfaceSettingsComponent extends FormReactive implements OnIn
27 private authService: AuthService, 26 private authService: AuthService,
28 private notifier: Notifier, 27 private notifier: Notifier,
29 private userService: UserService, 28 private userService: UserService,
29 private themeService: ThemeService,
30 private serverService: ServerService 30 private serverService: ServerService
31 ) { 31 ) {
32 super() 32 super()
33 } 33 }
34 34
35 get availableThemes () { 35 get instanceName () {
36 return this.serverConfig.theme.registered 36 return this.serverConfig.instance.name
37 .map(t => t.name)
38 } 37 }
39 38
40 ngOnInit () { 39 ngOnInit () {
41 this.serverConfig = this.serverService.getHTMLConfig() 40 this.serverConfig = this.serverService.getHTMLConfig()
42 41
42 this.availableThemes = this.themeService.buildAvailableThemes()
43
43 this.buildForm({ 44 this.buildForm({
44 theme: null 45 theme: null
45 }) 46 })
@@ -61,17 +62,19 @@ export class UserInterfaceSettingsComponent extends FormReactive implements OnIn
61 } 62 }
62 63
63 getDefaultThemeLabel () { 64 getDefaultThemeLabel () {
65 return this.themeService.getDefaultThemeLabel()
66 }
67
68 getDefaultInstanceThemeLabel () {
64 const theme = this.serverConfig.theme.default 69 const theme = this.serverConfig.theme.default
65 70
66 if (theme === 'default') return this.defaultThemeLabel 71 if (theme === 'default') {
72 return this.getDefaultThemeLabel()
73 }
67 74
68 return theme 75 return theme
69 } 76 }
70 77
71 capitalizeFirstLetter (str: string) {
72 return capitalizeFirstLetter(str)
73 }
74
75 updateInterfaceSettings () { 78 updateInterfaceSettings () {
76 const theme = this.form.value['theme'] 79 const theme = this.form.value['theme']
77 80
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.html b/client/src/app/shared/shared-user-settings/user-video-settings.component.html
index 446ade445..85b27a4ff 100644
--- a/client/src/app/shared/shared-user-settings/user-video-settings.component.html
+++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.html
@@ -1,5 +1,5 @@
1<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form"> 1<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
2 <div class="form-group form-group-select"> 2 <div class="form-group">
3 <div class="anchor" id="video-sensitive-content-policy"></div> <!-- video-sensitive-content-policy anchor --> 3 <div class="anchor" id="video-sensitive-content-policy"></div> <!-- video-sensitive-content-policy anchor -->
4 <label i18n for="nsfwPolicy">Default policy on videos containing sensitive content</label> 4 <label i18n for="nsfwPolicy">Default policy on videos containing sensitive content</label>
5 <my-help> 5 <my-help>
@@ -20,7 +20,7 @@
20 </div> 20 </div>
21 </div> 21 </div>
22 22
23 <div class="form-group form-group-select"> 23 <div class="form-group">
24 <div class="anchor" id="video-languages-subtitles"></div> <!-- video-languages-subtitles anchor --> 24 <div class="anchor" id="video-languages-subtitles"></div> <!-- video-languages-subtitles anchor -->
25 <label i18n for="videoLanguages">Only display videos in the following languages/subtitles</label> 25 <label i18n for="videoLanguages">Only display videos in the following languages/subtitles</label>
26 <my-help> 26 <my-help>
@@ -30,7 +30,7 @@
30 </my-help> 30 </my-help>
31 31
32 <div> 32 <div>
33 <my-select-languages formControlName="videoLanguages"></my-select-languages> 33 <my-select-languages [maxLanguages]="20" formControlName="videoLanguages"></my-select-languages>
34 </div> 34 </div>
35 </div> 35 </div>
36 36
@@ -42,7 +42,7 @@
42 i18n-labelText labelText="Help share videos being played" 42 i18n-labelText labelText="Help share videos being played"
43 > 43 >
44 <ng-container ngProjectAs="description"> 44 <ng-container ngProjectAs="description">
45 <span i18n>The <a routerLink="/about/peertube" fragment="privacy" target="_blank">sharing system</a> implies that some technical information about your system (such as a public IP address) can be sent to other peers, but greatly helps to reduce server load.</span> 45 <span i18n>The <a class="link-orange" routerLink="/about/peertube" fragment="privacy" target="_blank">sharing system</a> implies that some technical information about your system (such as a public IP address) can be sent to other peers, but greatly helps to reduce server load.</span>
46 </ng-container> 46 </ng-container>
47 </my-peertube-checkbox> 47 </my-peertube-checkbox>
48 </div> 48 </div>
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.scss b/client/src/app/shared/shared-user-settings/user-video-settings.component.scss
index c4f6020d4..163c899d3 100644
--- a/client/src/app/shared/shared-user-settings/user-video-settings.component.scss
+++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.scss
@@ -1,11 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4label {
5 font-weight: $font-regular;
6 font-size: 100%;
7}
8
9input[type=submit] { 4input[type=submit] {
10 @include peertube-button; 5 @include peertube-button;
11 @include orange-button; 6 @include orange-button;
@@ -15,8 +10,6 @@ input[type=submit] {
15 10
16.peertube-select-container { 11.peertube-select-container {
17 @include peertube-select-container(340px); 12 @include peertube-select-container(340px);
18
19 margin-bottom: 30px;
20} 13}
21 14
22my-select-languages { 15my-select-languages {
@@ -24,7 +17,3 @@ my-select-languages {
24 17
25 display: block; 18 display: block;
26} 19}
27
28.form-group-select {
29 margin-bottom: 30px;
30}
diff --git a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.html b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.html
index a00c3d1c7..656d1beb3 100644
--- a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.html
+++ b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.html
@@ -1,5 +1,5 @@
1<form novalidate [formGroup]="form" (ngSubmit)="formValidated()"> 1<form novalidate [formGroup]="form" (ngSubmit)="formValidated()">
2 <div class="form-group mb-2"> 2 <div class="form-group">
3 <input type="email" 3 <input type="email"
4 formControlName="text" 4 formControlName="text"
5 class="form-control" 5 class="form-control"
diff --git a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html
index a6d851315..0e09c2697 100644
--- a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html
+++ b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html
@@ -1,5 +1,5 @@
1<div 1<div
2 class="btn-group-subscribe btn-group" 2 class="btn-group-subscribe btn-group" role="group"
3 [ngClass]="{'subscribe-button': !isAllChannelsSubscribed, 'unsubscribe-button': isAllChannelsSubscribed, 'big': isBigButton }" 3 [ngClass]="{'subscribe-button': !isAllChannelsSubscribed, 'unsubscribe-button': isAllChannelsSubscribed, 'big': isBigButton }"
4> 4>
5 5
@@ -20,17 +20,11 @@
20 </ng-template> 20 </ng-template>
21 21
22 <ng-template #userLoggedIn> 22 <ng-template #userLoggedIn>
23 <button 23 <button *ngIf="!isAllChannelsSubscribed" type="button" class="btn" (click)="subscribe()">
24 *ngIf="!isAllChannelsSubscribed" type="button"
25 class="btn btn-sm" (click)="subscribe()"
26 >
27 <ng-template [ngTemplateOutlet]="userLoggedOut"></ng-template> 24 <ng-template [ngTemplateOutlet]="userLoggedOut"></ng-template>
28 </button> 25 </button>
29 26
30 <button 27 <button *ngIf="isAllChannelsSubscribed" type="button" class="btn" role="button" (click)="unsubscribe()">
31 *ngIf="isAllChannelsSubscribed" type="button"
32 class="btn btn-sm" role="button"
33 (click)="unsubscribe()">
34 <ng-container i18n>{account + "", select, undefined {Unsubscribe} other {Unsubscribe from all channels}}</ng-container> 28 <ng-container i18n>{account + "", select, undefined {Unsubscribe} other {Unsubscribe from all channels}}</ng-container>
35 </button> 29 </button>
36 </ng-template> 30 </ng-template>
@@ -43,7 +37,7 @@
43 class="btn-group" ngbDropdown autoClose="outside" placement="bottom-right bottom-left bottom auto" 37 class="btn-group" ngbDropdown autoClose="outside" placement="bottom-right bottom-left bottom auto"
44 role="group" aria-label="Multiple ways to subscribe to the current channel" i18n-aria-label 38 role="group" aria-label="Multiple ways to subscribe to the current channel" i18n-aria-label
45 > 39 >
46 <button class="btn btn-sm dropdown-toggle-split" ngbDropdownToggle aria-label="Open subscription dropdown" i18n-aria-label> 40 <button class="btn dropdown-toggle-split" ngbDropdownToggle aria-label="Open subscription dropdown" i18n-aria-label>
47 <ng-container 41 <ng-container
48 *ngIf="!isUserLoggedIn(); then userLoggedOut"> 42 *ngIf="!isUserLoggedIn(); then userLoggedOut">
49 </ng-container> 43 </ng-container>
diff --git a/client/src/app/shared/shared-user-subscription/subscribe-button.component.scss b/client/src/app/shared/shared-user-subscription/subscribe-button.component.scss
index da8eaf646..889596b62 100644
--- a/client/src/app/shared/shared-user-subscription/subscribe-button.component.scss
+++ b/client/src/app/shared/shared-user-subscription/subscribe-button.component.scss
@@ -3,14 +3,10 @@
3 3
4.btn-group-subscribe { 4.btn-group-subscribe {
5 @include peertube-button; 5 @include peertube-button;
6 @include disable-default-a-behaviour;
7 6
8 float: right; 7 button.dropdown-toggle {
9 padding: 0; 8 font-size: $button-font-size;
10 9 line-height: 1.2;
11 > .btn,
12 > .dropdown > .dropdown-toggle {
13 font-size: 15px;
14 } 10 }
15 11
16 &:not(.big) { 12 &:not(.big) {
@@ -38,7 +34,7 @@
38 34
39 // Unlogged 35 // Unlogged
40 > .dropdown > .dropdown-toggle span { 36 > .dropdown > .dropdown-toggle span {
41 @include padding-right(3px); 37 @include padding-right(5px);
42 } 38 }
43 39
44 // Logged 40 // Logged
@@ -65,9 +61,11 @@
65 @include padding-left(5px); 61 @include padding-left(5px);
66 } 62 }
67 } 63 }
64
68 &.unsubscribe-button { 65 &.unsubscribe-button {
69 .btn { 66 .btn {
70 @include grey-button; 67 @include grey-button;
68
71 font-weight: 600; 69 font-weight: 600;
72 } 70 }
73 } 71 }
diff --git a/client/src/app/shared/shared-video-comment/video-comment.service.ts b/client/src/app/shared/shared-video-comment/video-comment.service.ts
index 6f2ef50cb..8cd94643a 100644
--- a/client/src/app/shared/shared-video-comment/video-comment.service.ts
+++ b/client/src/app/shared/shared-video-comment/video-comment.service.ts
@@ -31,7 +31,7 @@ export class VideoCommentService {
31 private restService: RestService 31 private restService: RestService
32 ) {} 32 ) {}
33 33
34 addCommentThread (videoId: number | string, comment: VideoCommentCreate) { 34 addCommentThread (videoId: string, comment: VideoCommentCreate) {
35 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' 35 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads'
36 const normalizedComment = objectLineFeedToHtml(comment, 'text') 36 const normalizedComment = objectLineFeedToHtml(comment, 'text')
37 37
@@ -42,7 +42,7 @@ export class VideoCommentService {
42 ) 42 )
43 } 43 }
44 44
45 addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) { 45 addCommentReply (videoId: string, inReplyToCommentId: number, comment: VideoCommentCreate) {
46 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId 46 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId
47 const normalizedComment = objectLineFeedToHtml(comment, 'text') 47 const normalizedComment = objectLineFeedToHtml(comment, 'text')
48 48
@@ -75,7 +75,7 @@ export class VideoCommentService {
75 } 75 }
76 76
77 getVideoCommentThreads (parameters: { 77 getVideoCommentThreads (parameters: {
78 videoId: number | string 78 videoId: string
79 componentPagination: ComponentPaginationLight 79 componentPagination: ComponentPaginationLight
80 sort: string 80 sort: string
81 }): Observable<ThreadsResultList<VideoComment>> { 81 }): Observable<ThreadsResultList<VideoComment>> {
@@ -95,7 +95,7 @@ export class VideoCommentService {
95 } 95 }
96 96
97 getVideoThreadComments (parameters: { 97 getVideoThreadComments (parameters: {
98 videoId: number | string 98 videoId: string
99 threadId: number 99 threadId: number
100 }): Observable<VideoCommentThreadTree> { 100 }): Observable<VideoCommentThreadTree> {
101 const { videoId, threadId } = parameters 101 const { videoId, threadId } = parameters
diff --git a/client/src/app/shared/shared-video-live/live-documentation-link.component.html b/client/src/app/shared/shared-video-live/live-documentation-link.component.html
index acf8a71eb..27248645f 100644
--- a/client/src/app/shared/shared-video-live/live-documentation-link.component.html
+++ b/client/src/app/shared/shared-video-live/live-documentation-link.component.html
@@ -1,4 +1,4 @@
1<div i18n> 1<p i18n>
2 See <a href="https://docs.joinpeertube.org/use-create-upload-video?id=publish-a-live-in-peertube-gt-v3" target="_blank" rel="noopener noreferrer">the documentation</a> 2 See <a class="link-orange" href="https://docs.joinpeertube.org/use-create-upload-video?id=publish-a-live-in-peertube-gt-v3" target="_blank" rel="noopener noreferrer">the documentation</a>
3 to learn how to use the PeerTube live streaming feature. 3 to learn how to use the PeerTube live streaming feature.
4</div> 4</p>
diff --git a/client/src/app/shared/shared-video-live/live-stream-information.component.html b/client/src/app/shared/shared-video-live/live-stream-information.component.html
index 01e305938..99c7dbd4c 100644
--- a/client/src/app/shared/shared-video-live/live-stream-information.component.html
+++ b/client/src/app/shared/shared-video-live/live-stream-information.component.html
@@ -7,27 +7,27 @@
7 7
8 <div class="modal-body" *ngIf="live"> 8 <div class="modal-body" *ngIf="live">
9 <div> 9 <div>
10 <div class="badge badge-info" *ngIf="live.permanentLive" i18n>Permanent/Recurring live</div> 10 <div class="pt-badge badge-blue" *ngIf="live.permanentLive" i18n>Permanent/Recurring live</div>
11 <div class="badge badge-info" *ngIf="live.saveReplay" i18n>Replay will be saved</div> 11 <div class="pt-badge badge-blue" *ngIf="live.saveReplay" i18n>Replay will be saved</div>
12 </div> 12 </div>
13 13
14 <div class="alert alert-info"> 14 <div class="alert pt-alert-primary">
15 <my-live-documentation-link></my-live-documentation-link> 15 <my-live-documentation-link></my-live-documentation-link>
16 </div> 16 </div>
17 17
18 <div *ngIf="live.rtmpUrl" class="form-group"> 18 <div *ngIf="live.rtmpUrl" class="form-group">
19 <label for="liveVideoRTMPUrl" i18n>Live RTMP Url</label> 19 <label for="liveVideoRTMPUrl" i18n>Live RTMP Url</label>
20 <my-input-toggle-hidden inputId="liveVideoRTMPUrl" [value]="live.rtmpUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-toggle-hidden> 20 <my-input-text inputId="liveVideoRTMPUrl" [value]="live.rtmpUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-text>
21 </div> 21 </div>
22 22
23 <div *ngIf="live.rtmpsUrl" class="form-group"> 23 <div *ngIf="live.rtmpsUrl" class="form-group">
24 <label for="liveVideoRTMPSUrl" i18n>Live RTMPS Url</label> 24 <label for="liveVideoRTMPSUrl" i18n>Live RTMPS Url</label>
25 <my-input-toggle-hidden inputId="liveVideoRTMPSUrl" [value]="live.rtmpsUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-toggle-hidden> 25 <my-input-text inputId="liveVideoRTMPSUrl" [value]="live.rtmpsUrl" [withToggle]="false" [withCopy]="true" [show]="true" [readonly]="true"></my-input-text>
26 </div> 26 </div>
27 27
28 <div class="form-group"> 28 <div class="form-group">
29 <label for="liveVideoStreamKey" i18n>Live stream key</label> 29 <label for="liveVideoStreamKey" i18n>Live stream key</label>
30 <my-input-toggle-hidden inputId="liveVideoStreamKey" [value]="live.streamKey" [withCopy]="true" [readonly]="true"></my-input-toggle-hidden> 30 <my-input-text inputId="liveVideoStreamKey" [value]="live.streamKey" [withCopy]="true" [readonly]="true"></my-input-text>
31 31
32 <div class="form-group-description" i18n>⚠️ Never share your stream key with anyone.</div> 32 <div class="form-group-description" i18n>⚠️ Never share your stream key with anyone.</div>
33 </div> 33 </div>
@@ -36,8 +36,8 @@
36 <label i18n>Latest live sessions</label> 36 <label i18n>Latest live sessions</label>
37 37
38 <div class="journal-session" *ngFor="let session of latestLiveSessions"> 38 <div class="journal-session" *ngFor="let session of latestLiveSessions">
39 <span i18n class="badge badge-success" *ngIf="!getErrorLabel(session)">Success</span> 39 <span i18n class="pt-badge badge-success" *ngIf="!getErrorLabel(session)">Success</span>
40 <span class="badge badge-danger" *ngIf="getErrorLabel(session)">{{ getErrorLabel(session) }}</span> 40 <span class="pt-badge badge-danger" *ngIf="getErrorLabel(session)">{{ getErrorLabel(session) }}</span>
41 41
42 <span i18n>Started on {{ session.startDate | date:'medium' }}</span> 42 <span i18n>Started on {{ session.startDate | date:'medium' }}</span>
43 <span i18n *ngIf="session.endDate">Ended on {{ session.endDate | date:'medium' }}</span> 43 <span i18n *ngIf="session.endDate">Ended on {{ session.endDate | date:'medium' }}</span>
diff --git a/client/src/app/shared/shared-video-live/live-stream-information.component.scss b/client/src/app/shared/shared-video-live/live-stream-information.component.scss
index 9c8ad12bd..a0cb30897 100644
--- a/client/src/app/shared/shared-video-live/live-stream-information.component.scss
+++ b/client/src/app/shared/shared-video-live/live-stream-information.component.scss
@@ -9,19 +9,20 @@ p-autocomplete {
9 margin: 20px 0; 9 margin: 20px 0;
10} 10}
11 11
12.alert-info { 12.pt-alert-primary {
13 margin: 1rem 0; 13 margin: 1rem 0;
14} 14}
15 15
16.badge { 16.pt-badge {
17 @include margin-right(5px);
18
17 font-size: 13px; 19 font-size: 13px;
18 margin-right: 5px;
19} 20}
20 21
21.journal-session { 22.journal-session {
22 margin-bottom: 5px; 23 margin-bottom: 5px;
23 24
24 span:not(.badge, :last-child)::after { 25 span:not(.pt-badge, :last-child)::after {
25 margin: 3px; 26 margin: 3px;
26 content: '•'; 27 content: '•';
27 } 28 }
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.html b/client/src/app/shared/shared-video-miniature/video-download.component.html
index b50544057..1c7458b4b 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.html
@@ -28,15 +28,10 @@
28 28
29 <ng-template ngbNavContent> 29 <ng-template ngbNavContent>
30 <div class="nav-content"> 30 <div class="nav-content">
31 <div class="input-group input-group-sm"> 31 <my-input-text
32 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> 32 *ngIf="!isConfidentialVideo()"
33 33 [show]="true" [readonly]="true" [withCopy]="true" [withToggle]="false" [value]="getLink()"
34 <div class="input-group-append" *ngIf="!isConfidentialVideo()"> 34 ></my-input-text>
35 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
36 <span class="glyphicon glyphicon-duplicate"></span>
37 </button>
38 </div>
39 </div>
40 </div> 35 </div>
41 </ng-template> 36 </ng-template>
42 </ng-container> 37 </ng-container>
@@ -53,14 +48,10 @@
53 48
54 <ng-template ngbNavContent> 49 <ng-template ngbNavContent>
55 <div class="nav-content"> 50 <div class="nav-content">
56 <div class="input-group input-group-sm"> 51 <my-input-text
57 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> 52 *ngIf="!isConfidentialVideo()"
58 <div class="input-group-append" *ngIf="!isConfidentialVideo()"> 53 [show]="true" [readonly]="true" [withCopy]="true" [withToggle]="false" [value]="getLink()"
59 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> 54 ></my-input-text>
60 <span class="glyphicon glyphicon-duplicate"></span>
61 </button>
62 </div>
63 </div>
64 </div> 55 </div>
65 </ng-template> 56 </ng-template>
66 </ng-container> 57 </ng-container>
@@ -129,7 +120,7 @@
129 [attr.aria-expanded]="!isAdvancedCustomizationCollapsed" aria-controls="collapseBasic" 120 [attr.aria-expanded]="!isAdvancedCustomizationCollapsed" aria-controls="collapseBasic"
130 > 121 >
131 <ng-container *ngIf="isAdvancedCustomizationCollapsed"> 122 <ng-container *ngIf="isAdvancedCustomizationCollapsed">
132 <span class="glyphicon glyphicon-menu-down"></span> 123 <span class="chevron-down"></span>
133 124
134 <ng-container i18n> 125 <ng-container i18n>
135 Advanced 126 Advanced
@@ -137,7 +128,7 @@
137 </ng-container> 128 </ng-container>
138 129
139 <ng-container *ngIf="!isAdvancedCustomizationCollapsed"> 130 <ng-container *ngIf="!isAdvancedCustomizationCollapsed">
140 <span class="glyphicon glyphicon-menu-up"></span> 131 <span class="chevron-up"></span>
141 132
142 <ng-container i18n> 133 <ng-container i18n>
143 Simple 134 Simple
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.scss b/client/src/app/shared/shared-video-miniature/video-download.component.scss
index bd42f4813..407bdadf2 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
@@ -10,17 +10,12 @@
10 justify-content: center; 10 justify-content: center;
11 align-items: center; 11 align-items: center;
12 margin-top: 20px; 12 margin-top: 20px;
13 font-size: 16px;
14 font-weight: 600; 13 font-weight: 600;
15 cursor: pointer; 14 cursor: pointer;
16 15
17 .nav-tabs { 16 .nav-tabs {
18 margin-top: 10x; 17 margin-top: 10x;
19 } 18 }
20
21 .glyphicon {
22 @include margin-right(5px);
23 }
24} 19}
25 20
26.peertube-select-container.title-select { 21.peertube-select-container.title-select {
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 5328f5170..bbda39c2d 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
@@ -193,10 +193,6 @@ export class VideoDownloadComponent {
193 return this.video.privacy.id === VideoPrivacy.PRIVATE || this.video.privacy.id === VideoPrivacy.INTERNAL 193 return this.video.privacy.id === VideoPrivacy.PRIVATE || this.video.privacy.id === VideoPrivacy.INTERNAL
194 } 194 }
195 195
196 activateCopiedMessage () {
197 this.notifier.success($localize`Copied`)
198 }
199
200 switchToType (type: DownloadType) { 196 switchToType (type: DownloadType) {
201 this.type = type 197 this.type = type
202 } 198 }
diff --git a/client/src/app/shared/shared-video-miniature/video-filters-header.component.html b/client/src/app/shared/shared-video-miniature/video-filters-header.component.html
index a07b8b5ee..fe7a59bdb 100644
--- a/client/src/app/shared/shared-video-miniature/video-filters-header.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-filters-header.component.html
@@ -44,6 +44,7 @@
44 [searchable]="false" 44 [searchable]="false"
45 > 45 >
46 <ng-option i18n value="-publishedAt">Sort by <strong>"Recently Added"</strong></ng-option> 46 <ng-option i18n value="-publishedAt">Sort by <strong>"Recently Added"</strong></ng-option>
47 <ng-option i18n value="-originallyPublishedAt">Sort by <strong>"Original Publication Date"</strong></ng-option>
47 48
48 <ng-option i18n *ngIf="isTrendingSortEnabled('most-viewed')" value="-trending">Sort by <strong>"Recent Views"</strong></ng-option> 49 <ng-option i18n *ngIf="isTrendingSortEnabled('most-viewed')" value="-trending">Sort by <strong>"Recent Views"</strong></ng-option>
49 <ng-option i18n *ngIf="isTrendingSortEnabled('hot')" value="-hot">Sort by <strong>"Hot"</strong></ng-option> 50 <ng-option i18n *ngIf="isTrendingSortEnabled('hot')" value="-hot">Sort by <strong>"Hot"</strong></ng-option>
diff --git a/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss b/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss
index 8cb1ff5b8..a4e51982c 100644
--- a/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss
@@ -3,7 +3,6 @@
3 3
4.root { 4.root {
5 margin-bottom: 45px; 5 margin-bottom: 45px;
6 font-size: 15px;
7} 6}
8 7
9.first-row { 8.first-row {
@@ -49,7 +48,6 @@
49 48
50 border-radius: 24px; 49 border-radius: 24px;
51 padding: 4px 15px; 50 padding: 4px 15px;
52 font-size: 16px;
53 margin-bottom: 15px; 51 margin-bottom: 15px;
54 cursor: pointer; 52 cursor: pointer;
55} 53}
@@ -101,7 +99,7 @@
101} 99}
102 100
103.sort { 101.sort {
104 min-width: 200px; 102 min-width: 250px;
105 max-width: 300px; 103 max-width: 300px;
106 height: min-content; 104 height: min-content;
107 105
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 3cf128de0..e8d2ca1c4 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
@@ -12,13 +12,13 @@
12 <div class="d-flex video-miniature-meta"> 12 <div class="d-flex video-miniature-meta">
13 <my-actor-avatar 13 <my-actor-avatar
14 *ngIf="displayOptions.avatar && displayOwnerVideoChannel() && !displayAsRow" [title]="channelLinkTitle" 14 *ngIf="displayOptions.avatar && displayOwnerVideoChannel() && !displayAsRow" [title]="channelLinkTitle"
15 [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]" 15 [actor]="video.channel" actorType="channel" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]"
16 size="32" 16 size="32"
17 ></my-actor-avatar> 17 ></my-actor-avatar>
18 18
19 <my-actor-avatar 19 <my-actor-avatar
20 *ngIf="displayOptions.avatar && displayOwnerAccount() && !displayAsRow" [title]="channelLinkTitle" 20 *ngIf="displayOptions.avatar && displayOwnerAccount() && !displayAsRow" [title]="channelLinkTitle"
21 [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]" 21 [actor]="video.account" actorType="channel" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]"
22 size="32" 22 size="32"
23 ></my-actor-avatar> 23 ></my-actor-avatar>
24 24
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 80b418c87..a397efdca 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
@@ -4,6 +4,10 @@
4 4
5$more-button-width: 40px; 5$more-button-width: 40px;
6 6
7.video-miniature {
8 font-size: 14px;
9}
10
7.video-miniature-name { 11.video-miniature-name {
8 @include miniature-name; 12 @include miniature-name;
9} 13}
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
index 42c472579..534a78b3f 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
@@ -175,7 +175,7 @@ export class VideoMiniatureComponent implements OnInit {
175 175
176 if (video.scheduledUpdate) { 176 if (video.scheduledUpdate) {
177 const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId) 177 const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId)
178 return $localize`Publication scheduled on ` + updateAt 178 return $localize`Publication scheduled on ${updateAt}`
179 } 179 }
180 180
181 if (video.state.id === VideoState.TRANSCODING_FAILED) { 181 if (video.state.id === VideoState.TRANSCODING_FAILED) {
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.html b/client/src/app/shared/shared-video-miniature/videos-list.component.html
index 2b554517f..c220f61f1 100644
--- a/client/src/app/shared/shared-video-miniature/videos-list.component.html
+++ b/client/src/app/shared/shared-video-miniature/videos-list.component.html
@@ -12,15 +12,15 @@
12 12
13 <div class="action-block"> 13 <div class="action-block">
14 <ng-container *ngFor="let action of headerActions"> 14 <ng-container *ngFor="let action of headerActions">
15 <a *ngIf="action.routerLink" class="ml-2" [routerLink]="action.routerLink" routerLinkActive="active"> 15 <a *ngIf="action.routerLink" class="ms-2" [routerLink]="action.routerLink" routerLinkActive="active">
16 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container> 16 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container>
17 </a> 17 </a>
18 18
19 <a *ngIf="!action.routerLink && !action.href && action.click" class="ml-2" (click)="action.click($event)" (key.enter)="action.click($event)"> 19 <a *ngIf="!action.routerLink && !action.href && action.click" class="ms-2" (click)="action.click($event)" (key.enter)="action.click($event)">
20 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container> 20 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container>
21 </a> 21 </a>
22 22
23 <a *ngIf="!action.routerLink && action.href && action.click" class="ml-2" (click)="action.click($event)" (key.enter)="action.click($event)" [href]="action.href"> 23 <a *ngIf="!action.routerLink && action.href && action.click" class="ms-2" (click)="action.click($event)" (key.enter)="action.click($event)" [href]="action.href">
24 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container> 24 <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container>
25 </a> 25 </a>
26 26
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.scss b/client/src/app/shared/shared-video-miniature/videos-list.component.scss
index 209201a5c..fb9dcafb8 100644
--- a/client/src/app/shared/shared-video-miniature/videos-list.component.scss
+++ b/client/src/app/shared/shared-video-miniature/videos-list.component.scss
@@ -54,9 +54,9 @@ $margin-top: 30px;
54} 54}
55 55
56.date-title { 56.date-title {
57 font-size: 16px;
58 font-weight: $font-semibold; 57 font-weight: $font-semibold;
59 margin-bottom: 20px; 58 margin-bottom: 20px;
59 font-size: 1rem;
60 60
61 // Make the element span a full grid row within .videos grid 61 // Make the element span a full grid row within .videos grid
62 grid-column: 1 / -1; 62 grid-column: 1 / -1;
@@ -99,11 +99,5 @@ $margin-top: 30px;
99 align-items: center; 99 align-items: center;
100 height: auto; 100 height: auto;
101 margin-bottom: 10px; 101 margin-bottom: 10px;
102
103 .title-page {
104 @include margin-right(0);
105
106 margin-bottom: 10px;
107 }
108 } 102 }
109} 103}
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html
index bd5d37196..6d787796a 100644
--- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html
@@ -30,12 +30,10 @@
30 </div> 30 </div>
31 31
32 <div class="optional-rows" *ngIf="playlist.optionalRowDisplayed"> 32 <div class="optional-rows" *ngIf="playlist.optionalRowDisplayed">
33 <div class="labels"> 33 <div class="header-label" i18n>Start at</div>
34 <div i18n>Start at</div> 34 <div class="header-label" i18n>Stop at</div>
35 <div i18n>Stop at</div>
36 </div>
37 35
38 <div *ngFor="let element of buildOptionalRowElements(playlist)"> 36 <ng-container *ngFor="let element of buildOptionalRowElements(playlist)">
39 <my-peertube-checkbox 37 <my-peertube-checkbox
40 [inputName]="getOptionalInputName(playlist, element)" 38 [inputName]="getOptionalInputName(playlist, element)"
41 [ngModel]="element.enabled" [onPushWorkaround]="true" 39 [ngModel]="element.enabled" [onPushWorkaround]="true"
@@ -55,7 +53,7 @@
55 (inputBlur)="onElementTimestampUpdate(playlist, element)" 53 (inputBlur)="onElementTimestampUpdate(playlist, element)"
56 #stopAt 54 #stopAt
57 ></my-timestamp-input> 55 ></my-timestamp-input>
58 </div> 56 </ng-container>
59 </div> 57 </div>
60 </div> 58 </div>
61 </div> 59 </div>
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss
index 7db469d7c..de2f1032b 100644
--- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss
+++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss
@@ -1,10 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4$optional-rows-checkbox-width: 34px;
5$timestamp-width: 50px;
6$timestamp-margin-right: 10px;
7
8.header, 4.header,
9.dropdown-item, 5.dropdown-item,
10.input-container { 6.input-container {
@@ -52,12 +48,12 @@ $timestamp-margin-right: 10px;
52 } 48 }
53} 49}
54 50
55.primary-row, 51.primary-row {
56.optional-rows > div {
57 display: flex; 52 display: flex;
58 53
59 my-peertube-checkbox { 54 my-peertube-checkbox {
60 @include margin-right(10px); 55 @include margin-right(10px);
56
61 align-self: center; 57 align-self: center;
62 } 58 }
63 59
@@ -84,41 +80,30 @@ $timestamp-margin-right: 10px;
84 height: 19px; 80 height: 19px;
85 } 81 }
86 } 82 }
87
88 my-timestamp-input {
89 @include margin-right($timestamp-margin-right);
90
91 ::ng-deep .ui-inputtext {
92 padding: 0;
93 width: $timestamp-width;
94 }
95 }
96} 83}
97 84
98.optional-rows { 85.optional-rows {
99 > div { 86 display: grid;
100 padding: 8px 5px 5px 10px; 87 grid-template-columns: 35px 80px 80px;
101 } 88 row-gap: 3px;
89 column-gap: 10px;
90 align-items: center;
102 91
103 my-peertube-checkbox { 92 my-peertube-checkbox {
104 @include margin-right(0 !important); 93 @include margin-left(auto);
105
106 display: block;
107 width: $optional-rows-checkbox-width;
108 } 94 }
109 95
110 .labels { 96 .header-label {
111 @include margin-left($optional-rows-checkbox-width);
112
113 font-size: 13px; 97 font-size: 13px;
114 color: pvar(--greyForegroundColor); 98 color: pvar(--greyForegroundColor);
115 padding-top: 5px; 99 padding-left: 2px;
116 padding-bottom: 0;
117 100
118 div { 101 &:nth-child(1) {
119 @include margin-right($timestamp-margin-right); 102 grid-column: 2;
103 }
120 104
121 width: $timestamp-width; 105 &:nth-child(2) {
106 grid-column: 3;
122 } 107 }
123 } 108 }
124} 109}
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
index 2400a4c25..f58d5f7f6 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
@@ -21,7 +21,7 @@
21 [attr.title]="playlistElement.video.name" 21 [attr.title]="playlistElement.video.name"
22 >{{ playlistElement.video.name }}</a> 22 >{{ playlistElement.video.name }}</a>
23 23
24 <span *ngIf="isVideoPrivate()" class="badge badge-yellow">Private</span> 24 <span *ngIf="isVideoPrivate()" class="pt-badge badge-yellow">Private</span>
25 </div> 25 </div>
26 26
27 <span class="video-miniature-created-at-views"> 27 <span class="video-miniature-created-at-views">
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
index fbf67e892..e6b01d33d 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
@@ -75,41 +75,6 @@ my-video-thumbnail,
75 left: -2px; 75 left: -2px;
76 } 76 }
77 } 77 }
78
79 .video-info {
80 display: flex;
81 flex-direction: column;
82 align-self: flex-start;
83 min-width: 0;
84
85 .video-info-header {
86 display: flex;
87 align-items: baseline;
88
89 a {
90 width: auto;
91 padding-right: 5px;
92 }
93
94 .badge {
95 @include peertube-badge;
96 margin-right: 5px;
97 }
98 }
99
100 .video-info-account,
101 .video-info-timestamp {
102 color: pvar(--greyForegroundColor);
103 }
104 }
105 }
106
107 .video-info-name {
108 @include ellipsis;
109
110 font-size: 18px;
111 font-weight: $font-semibold;
112 display: inline-block;
113 } 78 }
114 79
115 .more, 80 .more,
@@ -140,6 +105,45 @@ my-video-thumbnail,
140 } 105 }
141} 106}
142 107
108.video-info-name {
109 @include ellipsis;
110
111 font-size: 18px;
112 font-weight: $font-semibold;
113 display: inline-block;
114}
115
116.video-info {
117 display: flex;
118 flex-direction: column;
119 align-self: flex-start;
120 min-width: 0;
121
122 .video-info-header {
123 display: flex;
124 align-items: baseline;
125
126 a {
127 width: auto;
128 padding-right: 5px;
129 }
130
131 .pt-badge {
132 @include margin-right(5px);
133 }
134 }
135
136 .video-info-account,
137 .video-info-timestamp {
138 color: pvar(--greyForegroundColor);
139 }
140}
141
142.video-info-account,
143.video-miniature-created-at-views {
144 font-size: 14px;
145}
146
143.dropdown-menu { 147.dropdown-menu {
144 148
145 .dropdown-item { 149 .dropdown-item {
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 3956d9282..d43afad28 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
@@ -53,7 +53,7 @@
53 53
54 .privacy-date { 54 .privacy-date {
55 margin-top: 5px; 55 margin-top: 5px;
56 font-size: 13px; 56 font-size: 14px;
57 57
58 .privacy { 58 .privacy {
59 font-weight: $font-semibold; 59 font-weight: $font-semibold;