diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index 1279f19b0..ff972d05a 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -8,25 +8,8 @@ class="peertube-container" [ngClass]="{ 'user-logged-in': isUserLoggedIn(), 'user-not-logged-in': !isUserLoggedIn(), 'hotkeys-modal-opened': hotkeysModalOpened }" > -
- -
- - - - - {{ instanceName }} - -
- -
- -
+
+
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index 735917bcc..7fbe0208a 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss @@ -36,72 +36,6 @@ position: fixed; top: 0; width: 100%; - background-color: pvar(--mainBackgroundColor); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16); - display: flex; -} - -.top-left-block { - z-index: 1; - height: $header-height; - display: flex; - align-items: center; - min-width: 0; - - .icon { - @include icon(24px); - } - - .icon-menu { - mask-image: url('../assets/images/misc/menu.svg'); - -webkit-mask-image: url('../assets/images/misc/menu.svg'); - - background-color: pvar(--mainForegroundColor); - margin: 0 18px 0 20px; - - @media screen and (max-width: $mobile-view) { - margin: 0 10px; - } - } -} - -.root-header-right { - height: $header-height; - display: flex; - align-items: center; - justify-content: flex-end; - white-space: nowrap; - flex: 1; -} - -.peertube-title { - font-size: 20px; - font-weight: $font-bold; - color: inherit !important; - display: flex; - align-items: center; - overflow: hidden; - padding: 0 0 0 10px; - - @include disable-default-a-behaviour; - - .instance-name { - width: 100%; - - @include ellipsis; - - @media screen and (max-width: $mobile-view) { - display: none; - } - } - - .icon.icon-logo { - display: inline-block; - width: 23px; - height: 24px; - - @include margin-right(0.5rem); - } } .broadcast-message { diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 52f9d420c..6fede6e00 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -106,10 +106,6 @@ export class AppComponent implements OnInit, AfterViewInit { this.ngbConfig.animation = false } - get instanceName () { - return this.serverConfig.instance.name - } - ngOnInit () { document.getElementById('incompatible-browser').className += ' browser-ok' @@ -160,22 +156,6 @@ export class AppComponent implements OnInit, AfterViewInit { // --------------------------------------------------------------------------- - getDefaultRoute () { - return this.redirectService.getDefaultRoute().split('?')[0] - } - - getDefaultRouteQuery () { - return this.router.parseUrl(this.redirectService.getDefaultRoute()).queryParams - } - - // --------------------------------------------------------------------------- - - getToggleTitle () { - if (this.menu.isDisplayed()) return $localize`Close the left menu` - - return $localize`Open the left menu` - } - isUserLoggedIn () { return this.authService.isLoggedIn() } diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html index 4e327769d..b5af6dbee 100644 --- a/client/src/app/header/header.component.html +++ b/client/src/app/header/header.component.html @@ -1,6 +1,104 @@ - +
+ + + {{ instanceName }} + - - - Publish - +
+ + + @if (!isLoggedIn) { + + + + + + } @else { + @defer (when isLoggedIn) { + + } + + + +
+ + +
+ + Public profile + + + + + + + + + Videos: + {{ videoLanguages.join(', ') }} + + + + + + Sensitive: + {{ nsfwPolicy }} + + + + + + + + + +
+
+ } +
+
+ + + diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss index f2b5d0d4c..be07182a6 100644 --- a/client/src/app/header/header.component.scss +++ b/client/src/app/header/header.component.scss @@ -1,22 +1,89 @@ +@use 'sass:math'; @use '_variables' as *; @use '_mixins' as *; -.publish-button { - @include button-with-icon(21px, 3px, -1px); - @include margin-right(25px); +.peertube-title { + font-size: 24px; + font-weight: $font-bold; + color: inherit !important; + display: flex; + align-items: center; + overflow: hidden; - @media screen and (max-width: $mobile-view) { - padding-right: 10px; - padding-left: 10px; + @include padding-left(18px); + @include disable-default-a-behaviour; - @include margin-right(10px); + .instance-name { + width: 100%; - .icon.icon-upload { - @include margin-right(0); - } + @include ellipsis; - .publish-button-label { + @media screen and (max-width: $mobile-view) { display: none; } } + + .icon.icon-logo { + display: inline-block; + width: 34px; + height: 34px; + + @include margin-right(10px); + } +} + +.dropdown { + z-index: #{z('menu') + 1} !important; +} + +.dropdown-item { + cursor: pointer; + display: flex; + align-items: center; + + @include dropdown-with-icon-item; + + &:hover { + .hover-display-toggle { + display: none; + } + + .hover-display-toggle[hidden] { + display: inherit !important; + } + } +} + +.logged-in-more { + flex: 1; + border-radius: 25px; + transition: all .1s ease-in-out; + cursor: pointer; + max-width: 250px; + + > .dropdown-toggle { + display: flex; + align-items: center; + + &::after { + // Disable bootstrap toggle + border: 0; + } + } + + .dropdown-toggle-indicator { + position: relative; + display: none; + width: 17px; + + span { + position: absolute; + top: -8px; + color: #808080; + } + } +} + +.display-name { + font-weight: $font-semibold; } diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts index 4a6bc71ca..98821a083 100644 --- a/client/src/app/header/header.component.ts +++ b/client/src/app/header/header.component.ts @@ -1,6 +1,32 @@ -import { Component } from '@angular/core' +import { CommonModule, ViewportScroller } from '@angular/common' +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { Router, RouterLink, RouterLinkActive } from '@angular/router' +import { + AuthService, + AuthStatus, + AuthUser, + HooksService, + HotkeysService, + MenuService, + RedirectService, + ScreenService, + ServerService, + UserService +} from '@app/core' +import { scrollToTop } from '@app/helpers' +import { LanguageChooserComponent } from '@app/menu/language-chooser.component' +import { NotificationDropdownComponent } from '@app/menu/notification-dropdown.component' +import { QuickSettingsModalComponent } from '@app/menu/quick-settings-modal.component' +import { ActorAvatarComponent } from '@app/shared/shared-actor-image/actor-avatar.component' +import { InputSwitchComponent } from '@app/shared/shared-forms/input-switch.component' +import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service' +import { LoginLinkComponent } from '@app/shared/shared-main/users/login-link.component' +import { SignupLabelComponent } from '@app/shared/shared-main/users/signup-label.component' +import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' +import { ServerConfig, VideoConstant } from '@peertube/peertube-models' +import { Subscription, first, forkJoin } from 'rxjs' import { GlobalIconComponent } from '../shared/shared-icons/global-icon.component' -import { RouterLink } from '@angular/router' +import { ButtonComponent } from '../shared/shared-main/buttons/button.component' import { SearchTypeaheadComponent } from './search-typeahead.component' @Component({ @@ -8,7 +34,264 @@ import { SearchTypeaheadComponent } from './search-typeahead.component' templateUrl: './header.component.html', styleUrls: [ './header.component.scss' ], standalone: true, - imports: [ SearchTypeaheadComponent, RouterLink, GlobalIconComponent ] + imports: [ + CommonModule, + NotificationDropdownComponent, + ActorAvatarComponent, + InputSwitchComponent, + SignupLabelComponent, + LoginLinkComponent, + LanguageChooserComponent, + QuickSettingsModalComponent, + GlobalIconComponent, + RouterLink, + RouterLinkActive, + NgbDropdownModule, + SearchTypeaheadComponent, + RouterLink, + GlobalIconComponent, + ButtonComponent + ] }) -export class HeaderComponent {} +export class HeaderComponent implements OnInit, OnDestroy { + @ViewChild('languageChooserModal', { static: true }) languageChooserModal: LanguageChooserComponent + @ViewChild('quickSettingsModal', { static: true }) quickSettingsModal: QuickSettingsModalComponent + @ViewChild('dropdown') dropdown: NgbDropdown + + user: AuthUser + isLoggedIn: boolean + + hotkeysHelpVisible = false + + videoLanguages: string[] = [] + nsfwPolicy: string + + currentInterfaceLanguage: string + + private languages: VideoConstant[] = [] + + private serverConfig: ServerConfig + + private languagesSub: Subscription + private quickSettingsModalSub: Subscription + private hotkeysSub: Subscription + private authSub: Subscription + + constructor ( + private viewportScroller: ViewportScroller, + private authService: AuthService, + private userService: UserService, + private serverService: ServerService, + private redirectService: RedirectService, + private hotkeysService: HotkeysService, + private screenService: ScreenService, + private menuService: MenuService, + private modalService: PeertubeModalService, + private router: Router, + private hooks: HooksService + ) { } + + get isInMobileView () { + return this.screenService.isInMobileView() + } + + get language () { + return this.languageChooserModal.getCurrentLanguage() + } + + get requiresApproval () { + return this.serverConfig.signup.requiresApproval + } + + get instanceName () { + return this.serverConfig.instance.name + } + + ngOnInit () { + this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() + + this.isLoggedIn = this.authService.isLoggedIn() + this.updateUserState() + + this.authSub = this.authService.loginChangedSource.subscribe(status => { + if (status === AuthStatus.LoggedIn) { + this.isLoggedIn = true + } else if (status === AuthStatus.LoggedOut) { + this.isLoggedIn = false + } + + this.updateUserState() + }) + + this.hotkeysSub = this.hotkeysService.cheatSheetToggle + .subscribe(isOpen => this.hotkeysHelpVisible = isOpen) + + this.languagesSub = forkJoin([ + this.serverService.getVideoLanguages(), + this.authService.userInformationLoaded.pipe(first()) + ]).subscribe(([ languages ]) => { + this.languages = languages + + this.buildUserLanguages() + }) + + this.serverService.getConfig() + .subscribe(config => this.serverConfig = config) + + this.quickSettingsModalSub = this.modalService.openQuickSettingsSubject + .subscribe(() => this.openQuickSettings()) + } + + ngOnDestroy () { + if (this.quickSettingsModalSub) this.quickSettingsModalSub.unsubscribe() + if (this.languagesSub) this.languagesSub.unsubscribe() + if (this.hotkeysSub) this.hotkeysSub.unsubscribe() + if (this.authSub) this.authSub.unsubscribe() + } + + // --------------------------------------------------------------------------- + + getDefaultRoute () { + return this.redirectService.getDefaultRoute().split('?')[0] + } + + getDefaultRouteQuery () { + return this.router.parseUrl(this.redirectService.getDefaultRoute()).queryParams + } + + // --------------------------------------------------------------------------- + + isRegistrationAllowed () { + if (!this.serverConfig) return false + + return this.serverConfig.signup.allowed && + this.serverConfig.signup.allowedForCurrentIP + } + + logout (event: Event) { + event.preventDefault() + + this.authService.logout() + // Redirect to home page + this.redirectService.redirectToHomepage() + } + + openLanguageChooser () { + this.languageChooserModal.show() + } + + openQuickSettings () { + this.quickSettingsModal.show() + } + + toggleUseP2P () { + if (!this.user) return + this.user.p2pEnabled = !this.user.p2pEnabled + + this.userService.updateMyProfile({ p2pEnabled: this.user.p2pEnabled }) + .subscribe(() => this.authService.refreshUserInformation()) + } + + // FIXME: needed? + onDropdownOpenChange (opened: boolean) { + if (this.screenService.isInMobileView()) return + + // Close dropdown when window scroll to avoid dropdown quick jump for re-position + const onWindowScroll = () => { + this.dropdown?.close() + window.removeEventListener('scroll', onWindowScroll) + } + + if (opened) { + window.addEventListener('scroll', onWindowScroll) + document.querySelector('nav').scrollTo(0, 0) // Reset menu scroll to easy lock + // eslint-disable-next-line @typescript-eslint/unbound-method + document.querySelector('nav').addEventListener('scroll', this.onMenuScrollEvent) + } else { + // eslint-disable-next-line @typescript-eslint/unbound-method + document.querySelector('nav').removeEventListener('scroll', this.onMenuScrollEvent) + } + } + + // Lock menu scroll when menu scroll to avoid fleeing / detached dropdown + // FIXME: needed? + onMenuScrollEvent () { + document.querySelector('nav').scrollTo(0, 0) + } + + // FIXME: needed? + onActiveLinkScrollToAnchor (link: HTMLAnchorElement) { + const linkURL = link.getAttribute('href') + const linkHash = link.getAttribute('fragment') + + // On same url without fragment restore top scroll position + if (!linkHash && this.router.url.includes(linkURL)) { + scrollToTop('smooth') + } + + // On same url with fragment restore anchor scroll position + if (linkHash && this.router.url === linkURL) { + this.viewportScroller.scrollToAnchor(linkHash) + } + + if (this.screenService.isInSmallView()) { + this.menuService.toggleMenu() + } + } + + openHotkeysCheatSheet () { + this.hotkeysService.cheatSheetToggle.next(!this.hotkeysHelpVisible) + } + + private buildUserLanguages () { + if (!this.user) { + this.videoLanguages = [] + return + } + + if (!this.user.videoLanguages) { + this.videoLanguages = [ $localize`any language` ] + return + } + + this.videoLanguages = this.user.videoLanguages + .map(locale => this.langForLocale(locale)) + .map(value => value === undefined ? '?' : value) + } + + private langForLocale (localeId: string) { + if (localeId === '_unknown') return $localize`Unknown` + + return this.languages.find(lang => lang.id === localeId).label + } + + private computeNSFWPolicy () { + if (!this.user) { + this.nsfwPolicy = null + return + } + + switch (this.user.nsfwPolicy) { + case 'do_not_list': + this.nsfwPolicy = $localize`hide` + break + + case 'blur': + this.nsfwPolicy = $localize`blur` + break + + case 'display': + this.nsfwPolicy = $localize`display` + break + } + } + + private updateUserState () { + this.user = this.isLoggedIn + ? this.authService.getUser() + : undefined + + this.computeNSFWPolicy() + } +} diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html index 1b5c28d66..605fe0e9e 100644 --- a/client/src/app/header/search-typeahead.component.html +++ b/client/src/app/header/search-typeahead.component.html @@ -1,4 +1,4 @@ -
+
- diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss index 5fc711813..7459564b3 100644 --- a/client/src/app/header/search-typeahead.component.scss +++ b/client/src/app/header/search-typeahead.component.scss @@ -3,33 +3,47 @@ #search-video { text-overflow: ellipsis; + line-height: 42px; - @include peertube-input-text($search-input-width, 14px); - @include padding-left(10px); - @include padding-right(40px); // For the search icon + @include peertube-input-text(270px, 14px); + @include padding-left(42px); // For the search icon + @include padding-right(20px); + + & { + padding-top: 6px; + padding-bottom: 6px; + } &::placeholder { color: pvar(--inputPlaceholderColor); } + + @media screen and (max-width: $small-view) { + width: 200px; + } + + @media screen and (max-width: $mobile-view) { + width: 150px; + } } .search-button { - display: inline-flex; - align-self: center; position: absolute; + top: 0; + bottom: 0; - @include right(5px); + @include left(18px); &:hover { opacity: 0.8; } my-global-icon { - @include icon(18px); + height: 18px; + width: 18px; - & { - display: inline-flex; - } + position: relative; + top: -2px; } } @@ -80,30 +94,6 @@ #typeahead-container { font-size: 14px; - margin: 0 10px; - - input { - border: 1px solid pvar(--mainBackgroundColor) !important; - box-shadow: rgba(0, 0, 0, 0.1) 0 1px 20px 0; - flex-grow: 1; - transition: box-shadow .3s ease, width .2s ease; - } - - @media screen and (max-width: $small-view) { - input { - width: 200px; - } - } - - @media screen and (max-width: $mobile-view) { - input { - width: 150px; - } - } - - span { - right: 10px; - } > div:last-child { // we have to switch the display and not the opacity, diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index b16fdad20..2b2231c5f 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html @@ -1,141 +1,22 @@