Videos horizontal menu

Chocobozzz 2024-11-07 15:45:46 +01:00
parent e162a9a117
commit 55b2dc86e1
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
39 changed files with 311 additions and 332 deletions

View File

@ -1,11 +1,10 @@
<h1 class="visually-hidden" i18n>Videos</h1>
<my-videos-list <my-videos-list
#videosList #videosList
*ngIf="account" *ngIf="account"
[title]="title"
displayTitle="false"
[getVideosObservableFunction]="getVideosObservableFunction" [getVideosObservableFunction]="getVideosObservableFunction"
[getSyndicationItemsFunction]="getSyndicationItemsFunction" [getSyndicationItemsFunction]="getSyndicationItemsFunction"

View File

@ -21,7 +21,6 @@ export class AccountVideosComponent implements OnInit, OnDestroy, DisableForReus
getVideosObservableFunction = this.getVideosObservable.bind(this) getVideosObservableFunction = this.getVideosObservable.bind(this)
getSyndicationItemsFunction = this.getSyndicationItems.bind(this) getSyndicationItemsFunction = this.getSyndicationItems.bind(this)
title = $localize`Videos`
defaultSort = '-publishedAt' as VideoSortField defaultSort = '-publishedAt' as VideoSortField
account: Account account: Account

View File

@ -1,6 +1,6 @@
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
import { CustomMarkupContainerComponent } from '../shared/shared-custom-markup/custom-markup-container.component'
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service' import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
import { CustomMarkupContainerComponent } from '../shared/shared-custom-markup/custom-markup-container.component'
@Component({ @Component({
templateUrl: './home.component.html', templateUrl: './home.component.html',

View File

@ -1,11 +1,10 @@
<h1 class="visually-hidden" i18n>Videos</h1>
<my-videos-list <my-videos-list
#videosList #videosList
*ngIf="videoChannel" *ngIf="videoChannel"
[title]="title"
displayTitle="false"
[getVideosObservableFunction]="getVideosObservableFunction" [getVideosObservableFunction]="getVideosObservableFunction"
[getSyndicationItemsFunction]="getSyndicationItemsFunction" [getSyndicationItemsFunction]="getSyndicationItemsFunction"

View File

@ -22,7 +22,6 @@ export class VideoChannelVideosComponent implements OnInit, AfterViewInit, OnDes
getVideosObservableFunction = this.getVideosObservable.bind(this) getVideosObservableFunction = this.getVideosObservable.bind(this)
getSyndicationItemsFunction = this.getSyndicationItems.bind(this) getSyndicationItemsFunction = this.getSyndicationItems.bind(this)
title = $localize`Videos`
defaultSort = '-publishedAt' as VideoSortField defaultSort = '-publishedAt' as VideoSortField
displayOptions: MiniatureDisplayOptions = { displayOptions: MiniatureDisplayOptions = {

View File

@ -1,13 +1,14 @@
import { UrlSegment } from '@angular/router' import { inject } from '@angular/core'
import { LoginGuard } from '@app/core' import { Routes } from '@angular/router'
import { OverviewService, VideosListCommonPageComponent } from './video-list' import { LoginGuard, RedirectService } from '@app/core'
import { VideoOverviewComponent } from './video-list/overview/video-overview.component' import { AbuseService } from '@app/shared/shared-moderation/abuse.service'
import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
import { BlocklistService } from '@app/shared/shared-moderation/blocklist.service' import { BlocklistService } from '@app/shared/shared-moderation/blocklist.service'
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service' import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
import { UserSubscriptionService } from '@app/shared/shared-user-subscription/user-subscription.service' import { UserSubscriptionService } from '@app/shared/shared-user-subscription/user-subscription.service'
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service' import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
import { AbuseService } from '@app/shared/shared-moderation/abuse.service' import { OverviewService, VideosListAllComponent } from './video-list'
import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
export default [ export default [
{ {
@ -31,26 +32,36 @@ export default [
} }
}, },
// ---------------------------------------------------------------------------
// Old URL redirections
// ---------------------------------------------------------------------------
{ {
// Old URL redirection
path: 'most-liked', path: 'most-liked',
redirectTo: 'trending?sort=most-liked' redirectTo: 'browse?scope=federated&sort=-likes'
}, },
{ {
matcher: (url: UrlSegment[]) => { path: 'trending',
if (url.length === 1 && [ 'recently-added', 'trending', 'local' ].includes(url[0].path)) { redirectTo: () => {
return { const redirectService = inject(RedirectService)
consumed: url,
posParams: {
page: new UrlSegment(url[0].path, {})
}
}
}
return null return 'browse?scope=federated&sort=-' + redirectService.getDefaultTrendingAlgorithm()
}, }
},
{
path: 'recently-added',
redirectTo: 'browse?scope=federated&sort=-publishedAt'
},
{
path: 'local',
redirectTo: 'browse?scope=local&sort=-publishedAt'
},
component: VideosListCommonPageComponent, // ---------------------------------------------------------------------------
{
path: 'browse',
component: VideosListAllComponent,
data: { data: {
reuse: { reuse: {
enabled: true, enabled: true,
@ -75,4 +86,4 @@ export default [
} }
] ]
} }
] ] satisfies Routes

View File

@ -1,2 +1,2 @@
export * from './overview' export * from './overview'
export * from './videos-list-common-page.component' export * from './videos-list-all.component'

View File

@ -1,9 +1,9 @@
<h1 class="visually-hidden" i18n>Videos from your subscriptions</h1>
<my-videos-list <my-videos-list
[getVideosObservableFunction]="getVideosObservableFunction" [getVideosObservableFunction]="getVideosObservableFunction"
[getSyndicationItemsFunction]="getSyndicationItemsFunction" [getSyndicationItemsFunction]="getSyndicationItemsFunction"
[title]="titlePage"
[defaultSort]="defaultSort" [defaultSort]="defaultSort"
displayFilters="false" displayFilters="false"

View File

@ -29,8 +29,6 @@ export class VideoUserSubscriptionsComponent implements DisableForReuseHook {
} }
] ]
titlePage = $localize`Videos from your subscriptions`
disabled = false disabled = false
private feedToken: string private feedToken: string

View File

@ -1,10 +1,8 @@
<h1 class="visually-hidden" i18n>Browse videos</h1>
<my-videos-list <my-videos-list
[getVideosObservableFunction]="getVideosObservableFunction" [getVideosObservableFunction]="getVideosObservableFunction"
[getSyndicationItemsFunction]="getSyndicationItemsFunction" [getSyndicationItemsFunction]="getSyndicationItemsFunction"
[baseRouteBuilderFunction]="baseRouteBuilderFunction"
[title]="title"
[titleTooltip]="titleTooltip"
[defaultSort]="defaultSort" [defaultSort]="defaultSort"
[defaultScope]="defaultScope" [defaultScope]="defaultScope"

View File

@ -1,30 +1,23 @@
import { Component, OnDestroy, OnInit } from '@angular/core' import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { ComponentPaginationLight, DisableForReuseHook, MetaService, RedirectService, ServerService } from '@app/core' import { ComponentPaginationLight, DisableForReuseHook, MetaService, ServerService } from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service' import { HooksService } from '@app/core/plugins/hooks.service'
import { VideoService } from '@app/shared/shared-main/video/video.service' import { VideoService } from '@app/shared/shared-main/video/video.service'
import { VideoFilterScope, VideoFilters } from '@app/shared/shared-video-miniature/video-filters.model' import { VideoFilterScope, VideoFilters } from '@app/shared/shared-video-miniature/video-filters.model'
import { ClientFilterHookName, VideoSortField } from '@peertube/peertube-models' import { VideoSortField } from '@peertube/peertube-models'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { VideosListComponent } from '../../shared/shared-video-miniature/videos-list.component' import { VideosListComponent } from '../../shared/shared-video-miniature/videos-list.component'
export type VideosListCommonPageRouteData = {
sort: VideoSortField
scope: VideoFilterScope
hookParams: ClientFilterHookName
hookResult: ClientFilterHookName
}
@Component({ @Component({
templateUrl: './videos-list-common-page.component.html', templateUrl: './videos-list-all.component.html',
standalone: true, standalone: true,
imports: [ VideosListComponent ] imports: [
VideosListComponent
]
}) })
export class VideosListCommonPageComponent implements OnInit, OnDestroy, DisableForReuseHook { export class VideosListAllComponent implements OnInit, OnDestroy, DisableForReuseHook {
getVideosObservableFunction = this.getVideosObservable.bind(this) getVideosObservableFunction = this.getVideosObservable.bind(this)
getSyndicationItemsFunction = this.getSyndicationItems.bind(this) getSyndicationItemsFunction = this.getSyndicationItems.bind(this)
baseRouteBuilderFunction = this.baseRouteBuilder.bind(this)
title: string title: string
titleTooltip: string titleTooltip: string
@ -34,16 +27,12 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable
defaultSort: VideoSortField defaultSort: VideoSortField
defaultScope: VideoFilterScope defaultScope: VideoFilterScope
hookParams: ClientFilterHookName
hookResult: ClientFilterHookName
loadUserVideoPreferences = true loadUserVideoPreferences = true
displayFilters = true displayFilters = true
disabled = false disabled = false
private trendingDays: number
private routeSub: Subscription private routeSub: Subscription
constructor ( constructor (
@ -51,17 +40,15 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable
private route: ActivatedRoute, private route: ActivatedRoute,
private videoService: VideoService, private videoService: VideoService,
private hooks: HooksService, private hooks: HooksService,
private meta: MetaService, private meta: MetaService
private redirectService: RedirectService
) { ) {
} }
ngOnInit () { ngOnInit () {
this.trendingDays = this.server.getHTMLConfig().trending.videos.intervalDays this.defaultSort = '-publishedAt'
this.defaultScope = 'federated'
this.routeSub = this.route.params.subscribe(params => { this.routeSub = this.route.params.subscribe(() => this.update())
this.update(params['page'])
})
} }
ngOnDestroy () { ngOnDestroy () {
@ -80,8 +67,8 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable
this.videoService.getVideos.bind(this.videoService), this.videoService.getVideos.bind(this.videoService),
params, params,
'common', 'common',
this.hookParams, 'filter:api.browse-videos.videos.list.params',
this.hookResult 'filter:api.browse-videos.videos.list.result'
) )
} }
@ -96,18 +83,6 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable
this.updateGroupByDate(filters.sort) this.updateGroupByDate(filters.sort)
} }
baseRouteBuilder (filters: VideoFilters) {
const sanitizedSort = this.getSanitizedSort(filters.sort)
let suffix: string
if (filters.scope === 'local') suffix = 'local'
else if (sanitizedSort === 'publishedAt') suffix = 'recently-added'
else suffix = 'trending'
return [ '/videos', suffix ]
}
disableForReuse () { disableForReuse () {
this.disabled = true this.disabled = true
} }
@ -116,80 +91,19 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable
this.disabled = false this.disabled = false
} }
update (page: string) { update () {
const data = this.getData(page)
this.hookParams = data.hookParams
this.hookResult = data.hookResult
this.defaultSort = data.sort
this.defaultScope = data.scope
this.buildTitle() this.buildTitle()
this.updateGroupByDate(this.defaultSort) this.updateGroupByDate(this.defaultSort)
this.meta.setTitle(this.title) this.meta.setTitle(this.title)
} }
private getData (page: string) {
if (page === 'trending') return this.generateTrendingData(this.route.snapshot)
if (page === 'local') return this.generateLocalData()
return this.generateRecentlyAddedData()
}
private generateRecentlyAddedData (): VideosListCommonPageRouteData {
return {
sort: '-publishedAt',
scope: 'federated',
hookParams: 'filter:api.recently-added-videos.videos.list.params',
hookResult: 'filter:api.recently-added-videos.videos.list.result'
}
}
private generateLocalData (): VideosListCommonPageRouteData {
return {
sort: '-publishedAt' as VideoSortField,
scope: 'local',
hookParams: 'filter:api.local-videos.videos.list.params',
hookResult: 'filter:api.local-videos.videos.list.result'
}
}
private generateTrendingData (route: ActivatedRouteSnapshot): VideosListCommonPageRouteData {
const sort = route.queryParams['sort'] ?? this.parseTrendingAlgorithm(this.redirectService.getDefaultTrendingAlgorithm())
return {
sort,
scope: 'federated',
hookParams: 'filter:api.trending-videos.videos.list.params',
hookResult: 'filter:api.trending-videos.videos.list.result'
}
}
private parseTrendingAlgorithm (algorithm: string): VideoSortField {
switch (algorithm) {
case 'most-viewed':
return '-trending'
case 'most-liked':
return '-likes'
// We'll automatically apply "best" sort if using "hot" sort with a logged user
case 'best':
return '-hot'
default:
return '-' + algorithm as VideoSortField
}
}
private updateGroupByDate (sort: VideoSortField) { private updateGroupByDate (sort: VideoSortField) {
this.groupByDate = sort === '-publishedAt' || sort === 'publishedAt' this.groupByDate = sort === '-publishedAt' || sort === 'publishedAt'
} }
private buildTitle (scope: VideoFilterScope = this.defaultScope, sort: VideoSortField = this.defaultSort) { private buildTitle (scope: VideoFilterScope = this.defaultScope, sort: VideoSortField = this.defaultSort) {
const trendingDays = this.server.getHTMLConfig().trending.videos.intervalDays
const sanitizedSort = this.getSanitizedSort(sort) const sanitizedSort = this.getSanitizedSort(sort)
if (scope === 'local') { if (scope === 'local') {
@ -223,12 +137,12 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable
} }
if (sanitizedSort === 'trending') { if (sanitizedSort === 'trending') {
if (this.trendingDays === 1) { if (trendingDays === 1) {
this.titleTooltip = $localize`Videos with the most views during the last 24 hours` this.titleTooltip = $localize`Videos with the most views during the last 24 hours`
return return
} }
this.titleTooltip = $localize`Videos with the most views during the last ${this.trendingDays} days` this.titleTooltip = $localize`Videos with the most views during the last ${trendingDays} days`
} }
return return

View File

@ -5,6 +5,7 @@ import { EmptyComponent } from './empty.component'
import { HomepageRedirectComponent } from './homepage-redirect.component' import { HomepageRedirectComponent } from './homepage-redirect.component'
import { USER_USERNAME_REGEX_CHARACTERS } from './shared/form-validators/user-validators' import { USER_USERNAME_REGEX_CHARACTERS } from './shared/form-validators/user-validators'
import { ActorRedirectGuard } from './shared/shared-main/router/actor-redirect-guard.service' import { ActorRedirectGuard } from './shared/shared-main/router/actor-redirect-guard.service'
import { VideosParentComponent } from './videos-parent.component'
const routes: Routes = [ const routes: Routes = [
{ {
@ -12,11 +13,6 @@ const routes: Routes = [
loadChildren: () => import('./+admin/routes'), loadChildren: () => import('./+admin/routes'),
canActivateChild: [ MetaGuard ] canActivateChild: [ MetaGuard ]
}, },
{
path: 'home',
loadChildren: () => import('./+home/routes'),
canActivateChild: [ MetaGuard ]
},
{ {
path: 'my-account', path: 'my-account',
loadChildren: () => import('./+my-account/routes'), loadChildren: () => import('./+my-account/routes'),
@ -128,11 +124,33 @@ const routes: Routes = [
preload: 5000 preload: 5000
} }
}, },
// /home and other /videos routes
{ {
path: 'videos', matcher: (url): UrlMatchResult => {
loadChildren: () => import('./+videos/routes'), if (url.length < 1) return null
canActivateChild: [ MetaGuard ]
const matchResult = url[0].path === 'home' || url[0].path === 'videos'
if (!matchResult) return null
// So the children can detect the appropriate route
return { consumed: [] }
},
component: VideosParentComponent,
children: [
{
path: 'home',
loadChildren: () => import('./+home/routes'),
canActivateChild: [ MetaGuard ]
},
{
path: 'videos',
loadChildren: () => import('./+videos/routes'),
canActivateChild: [ MetaGuard ]
}
]
}, },
{ {
path: 'video-playlists/watch', path: 'video-playlists/watch',
redirectTo: 'videos/watch/playlist' redirectTo: 'videos/watch/playlist'

View File

@ -1,11 +1,12 @@
import debug from 'debug'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { NavigationCancel, NavigationEnd, Router } from '@angular/router' import { NavigationCancel, NavigationEnd, Router } from '@angular/router'
import { VideoSortField } from '@peertube/peertube-models'
import { logger } from '@root-helpers/logger' import { logger } from '@root-helpers/logger'
import { PluginsManager } from '@root-helpers/plugins-manager'
import debug from 'debug'
import { environment } from 'src/environments/environment'
import { ServerService } from '../server' import { ServerService } from '../server'
import { SessionStorageService } from '../wrappers/storage.service' import { SessionStorageService } from '../wrappers/storage.service'
import { PluginsManager } from '@root-helpers/plugins-manager'
import { environment } from 'src/environments/environment'
const debugLogger = debug('peertube:router:RedirectService') const debugLogger = debug('peertube:router:RedirectService')
@ -70,7 +71,22 @@ export class RedirectService {
} }
getDefaultTrendingAlgorithm () { getDefaultTrendingAlgorithm () {
return this.defaultTrendingAlgorithm const algorithm = this.defaultTrendingAlgorithm
switch (algorithm) {
case 'most-viewed':
return '-trending'
case 'most-liked':
return '-likes'
// We'll automatically apply "best" sort if using "hot" sort with a logged user
case 'best':
return '-hot'
default:
return '-' + algorithm as VideoSortField
}
} }
redirectToLatestSessionRoute () { redirectToLatestSessionRoute () {

View File

@ -4,11 +4,11 @@
<span class="instance-name">{{ instanceName }}</span> <span class="instance-name">{{ instanceName }}</span>
</a> </a>
<div class="d-flex align-items-center" [hidden]="!loaded"> <div class="d-flex align-items-center" [hidden]="!loaded || (loggedIn && !user.account)">
<my-search-typeahead class="w-100 me-5"></my-search-typeahead> <my-search-typeahead class="w-100 me-5"></my-search-typeahead>
@if (!loaded) { @if (!loggedIn) {
<my-button class="me-3" icon="cog" (click)="openQuickSettings()"></my-button> <my-button theme="tertiary" rounded="true" class="me-3" icon="cog" (click)="openQuickSettings()"></my-button>
<a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="create-account-button peertube-button-link text-truncate d-block"> <a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="create-account-button peertube-button-link text-truncate d-block">
<my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
@ -25,7 +25,7 @@
container="body" (openChange)="onDropdownOpenChange($event)" container="body" (openChange)="onDropdownOpenChange($event)"
> >
<button class="button-unstyle" ngbDropdownToggle> <button class="button-unstyle" ngbDropdownToggle>
<!-- <my-actor-avatar [actor]="user.account" actorType="account" size="34" class="me-2"></my-actor-avatar> --> <my-actor-avatar [actor]="user.account" actorType="account" size="34" class="me-2"></my-actor-avatar>
<div class="logged-in-info text-start"> <div class="logged-in-info text-start">
<div class="display-name ellipsis">{{ user.account?.displayName }}</div> <div class="display-name ellipsis">{{ user.account?.displayName }}</div>

View File

@ -6,12 +6,12 @@
line-height: 42px; line-height: 42px;
@include peertube-input-text(270px, 14px); @include peertube-input-text(270px, 14px);
@include padding-left(42px); // For the search icon
@include padding-right(20px);
& { & {
padding-top: 6px; padding-top: 6px;
padding-bottom: 6px; padding-bottom: 6px;
padding-inline-start: 42px; // For the search icon
padding-inline-end: 20px; // For the search icon
} }
&::placeholder { &::placeholder {

View File

@ -1,12 +1,12 @@
<a <a
tabindex="-1" class="d-flex flex-auto align-center p-2" [class.focus-visible]="active" tabindex="-1" class="d-flex flex-auto align-items-center p-2" [class.focus-visible]="active"
[title]="getTitle()" [title]="getTitle()"
[attr.aria-describedby]="describedby" [attr.aria-describedby]="describedby"
> >
<my-global-icon class="me-2" iconName="search"></my-global-icon> <my-global-icon class="me-2" iconName="search"></my-global-icon>
<div <div
class="flex-auto overflow-hidden no-wrap d-flex align-center" class="flex-auto overflow-hidden no-wrap d-flex align-items-center"
[attr.aria-label]="result.text" [attr.aria-label]="result.text"
> >
{{ result.text }} {{ result.text }}

View File

@ -0,0 +1,3 @@
<div class="margin-content">
<my-horizontal-menu [menuEntries]="menuEntries"></my-horizontal-menu>
</div>

View File

@ -0,0 +1,22 @@
import { Component } from '@angular/core'
import { RouterOutlet } from '@angular/router'
import { HorizontalMenuComponent } from '@app/shared/shared-main/menu/horizontal-menu.component'
import { ListOverflowItem } from '@app/shared/shared-main/menu/list-overflow.component'
@Component({
selector: 'my-home-menu',
templateUrl: './home-menu.component.html',
standalone: true,
imports: [
HorizontalMenuComponent,
RouterOutlet
]
})
export class HomeMenuComponent {
menuEntries: ListOverflowItem[] = [
{ label: $localize`Home`, routerLink: '/home' },
{ label: $localize`Discover`, routerLink: '/videos/overview' },
{ label: $localize`Subscriptions`, routerLink: '/videos/subscriptions' },
{ label: $localize`Browse videos`, routerLink: '/videos/browse' }
]
}

View File

@ -14,11 +14,11 @@
<nav> <nav>
<ng-container *ngFor="let menuSection of menuSections" > <ng-container *ngFor="let menuSection of menuSections" >
<ul [ngClass]="[ menuSection.key, 'menu-block' ]"> <ul class="ul-unstyle" [ngClass]="[ menuSection.key, 'menu-block' ]">
<li i18n> <li i18n>
<div class="block-title ellipsis" [ngClass]="{ 'visually-hidden': collapsed }">{{ menuSection.title }}</div> <div class="block-title ellipsis" [ngClass]="{ 'visually-hidden': collapsed }">{{ menuSection.title }}</div>
<ul> <ul class="ul-unstyle">
<li *ngFor="let link of menuSection.links"> <li *ngFor="let link of menuSection.links">
@if (link.isPrimaryButton === true) { @if (link.isPrimaryButton === true) {
<my-button class="d-block menu-button" theme="primary" [icon]="link.icon" [ariaLabel]="link.label" [ptRouterLink]="link.path"> <my-button class="d-block menu-button" theme="primary" [icon]="link.icon" [ariaLabel]="link.label" [ptRouterLink]="link.path">
@ -44,7 +44,7 @@
<div [ngClass]="{ 'visually-hidden': collapsed }" class="description">{{ shortDescription }}</div> <div [ngClass]="{ 'visually-hidden': collapsed }" class="description">{{ shortDescription }}</div>
<my-button class="mt-2 d-block" theme="secondary" icon="help" i18n-ariaLabel aria-label="More info" i18n> <my-button class="mt-2 d-block" theme="secondary" icon="help" i18n-ariaLabel aria-label="More info" i18n ptRouterLink="/about">
@if (!collapsed) { @if (!collapsed) {
More info More info
} }

View File

@ -2,15 +2,6 @@
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
ul {
margin: 0;
padding: 0;
li {
list-style: none;
}
}
.menu-wrapper { .menu-wrapper {
--menuXPadding: 1.5rem; --menuXPadding: 1.5rem;

View File

@ -0,0 +1,7 @@
<div class="root">
<ng-template #linkTemplate let-item="item">
<a [routerLink]="item.routerLink" routerLinkActive="active" class="x-menu-entry">{{ item.label }}</a>
</ng-template>
<my-list-overflow [items]="menuEntries" [itemTemplate]="linkTemplate"></my-list-overflow>
</div>

View File

@ -0,0 +1,40 @@
@use '_variables' as *;
@use '_mixins' as *;
.root {
width: 100%;
border-bottom: 1px solid pvar(--secondary-text-emphasis);
}
.x-menu-entry {
color: pvar(--secondary);
display: inline-block;
font-weight: $font-bold;
white-space: nowrap;
@include font-size(22px);
@include disable-default-a-behaviour;
@include margin-right(2rem);
&.active {
color: pvar(--fg);
position: relative;
&::after {
content: '';
display: block;
height: 4px;
background-color: pvar(--primary);
border-radius: 2px;
position: absolute;
bottom: -2px;
width: 100%;
}
}
&:hover,
&:active,
&:focus {
color: pvar(--fg);
}
}

View File

@ -0,0 +1,21 @@
import { CommonModule } from '@angular/common'
import { Component, Input } from '@angular/core'
import { RouterModule } from '@angular/router'
import { GlobalIconComponent } from '../../shared-icons/global-icon.component'
import { ListOverflowComponent, ListOverflowItem } from './list-overflow.component'
@Component({
selector: 'my-horizontal-menu',
templateUrl: './horizontal-menu.component.html',
styleUrls: [ './horizontal-menu.component.scss' ],
standalone: true,
imports: [
CommonModule,
RouterModule,
GlobalIconComponent,
ListOverflowComponent
]
})
export class HorizontalMenuComponent {
@Input() menuEntries: ListOverflowItem[] = []
}

View File

@ -1,39 +1,42 @@
<div #itemsParent class="list-overflow-parent"> <div #itemsParent class="list-overflow-parent" [ngClass]="{ 'opacity-0': !initialized }">
<span [id]="getId(id)" #itemsRendered *ngFor="let item of items; index as id">
<ng-container *ngTemplateOutlet="itemTemplate; context: {item: item}"></ng-container>
</span>
<ng-container *ngIf="isMenuDisplayed()"> <ul class="ul-unstyle d-flex">
<button *ngIf="isInMobileView" class="btn btn-outline-secondary btn-sm list-overflow-menu" (click)="toggleModal()"> <li class="d-inline-block" *ngFor="let item of items; index as id" [id]="getId(id)" #itemsRendered>
<span class="chevron-down"></span> <ng-container *ngTemplateOutlet="itemTemplate; context: {item: item}"></ng-container>
</button> </li>
</ul>
<div @if (isMenuDisplayed()) {
*ngIf="!isInMobileView" class="list-overflow-menu" @if (isInMobileView) {
ngbDropdown container="body" #dropdown="ngbDropdown" <button type="button" class="peertube-button tertiary-button list-overflow-menu" (click)="toggleModal()">
(mouseleave)="closeDropdownIfHovered(dropdown)" (mouseenter)="openDropdownOnHover(dropdown)"
>
<button class="btn btn-outline-secondary btn-sm" [ngClass]="{ 'route-active': active }"
ngbDropdownAnchor (click)="dropdownAnchorClicked(dropdown)" role="button"
>
<span class="chevron-down"></span> <span class="chevron-down"></span>
</button> </button>
} @else {
<div class="list-overflow-menu" ngbDropdown container="body" #dropdown="ngbDropdown">
<button class="peertube-button tertiary-button p-0" ngbDropdownToggle type="button">
<span class="chevron-down"></span>
</button>
<div ngbDropdownMenu> <ul class="ul-unstyle" ngbDropdownMenu>
<a *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length" <li *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length">
[routerLink]="item.routerLink" routerLinkActive="active" class="dropdown-item"> <a [routerLink]="item.routerLink" routerLinkActive="active" class="dropdown-item">
{{ item.label }} {{ item.label }}
</a> </a>
</li>
</ul>
</div> </div>
</div> }
</ng-container> }
</div > </div >
<ng-template #modal let-close="close" let-dismiss="dismiss"> <ng-template #modal let-close="close" let-dismiss="dismiss">
<div class="modal-body"> <div class="modal-body">
<a *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length" <ul class="ul-unstyle">
[routerLink]="item.routerLink" routerLinkActive="active" (click)="dismissOtherModals()"> <li *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length">
{{ item.label }} <a [routerLink]="item.routerLink" routerLinkActive="active" (click)="dismissOtherModals()">
</a> {{ item.label }}
</a>
</li>
</ul>
</div> </div>
</ng-template> </ng-template>

View File

@ -6,21 +6,20 @@
} }
.list-overflow-parent { .list-overflow-parent {
overflow: hidden;
display: flex; display: flex;
align-items: center;
// For the menu icon // For the menu icon
position: relative;
max-width: calc(100vw - 30px); max-width: calc(100vw - 30px);
} }
.list-overflow-menu { .list-overflow-menu {
position: absolute; position: absolute;
right: 25px; right: 0;
} }
button { button {
width: 30px;
border: 0;
&::after { &::after {
display: none; display: none;
} }
@ -36,13 +35,6 @@ button {
} }
} }
::ng-deep .dropdown-menu {
margin-top: 0 !important;
position: static;
right: auto;
bottom: auto;
}
.modal-body { .modal-body {
a { a {
color: currentColor; color: currentColor;

View File

@ -1,5 +1,4 @@
import { lowerFirst, uniqueId } from 'lodash-es' import { NgClass, NgFor, NgIf, NgTemplateOutlet, SlicePipe } from '@angular/common'
import { take } from 'rxjs/operators'
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
@ -13,11 +12,11 @@ import {
ViewChild, ViewChild,
ViewChildren ViewChildren
} from '@angular/core' } from '@angular/core'
import { RouterLink, RouterLinkActive } from '@angular/router'
import { ScreenService } from '@app/core' import { ScreenService } from '@app/core'
import { NgbDropdown, NgbModal, NgbDropdownAnchor, NgbDropdownMenu } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdown, NgbDropdownMenu, NgbDropdownToggle, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import debug from 'debug' import debug from 'debug'
import { RouterLinkActive, RouterLink } from '@angular/router' import { lowerFirst, uniqueId } from 'lodash-es'
import { NgFor, NgTemplateOutlet, NgIf, NgClass, SlicePipe } from '@angular/common'
const debugLogger = debug('peertube:main:ListOverflowItem') const debugLogger = debug('peertube:main:ListOverflowItem')
@ -37,7 +36,7 @@ export interface ListOverflowItem {
NgTemplateOutlet, NgTemplateOutlet,
NgIf, NgIf,
NgbDropdown, NgbDropdown,
NgbDropdownAnchor, NgbDropdownToggle,
NgClass, NgClass,
NgbDropdownMenu, NgbDropdownMenu,
RouterLinkActive, RouterLinkActive,
@ -54,10 +53,8 @@ export class ListOverflowComponent<T extends ListOverflowItem> implements AfterV
@ViewChildren('itemsRendered') itemsRendered: QueryList<ElementRef> @ViewChildren('itemsRendered') itemsRendered: QueryList<ElementRef>
showItemsUntilIndexExcluded: number showItemsUntilIndexExcluded: number
active = false
isInMobileView = false isInMobileView = false
initialized = false
private openedOnHover = false
constructor ( constructor (
private cdr: ChangeDetectorRef, private cdr: ChangeDetectorRef,
@ -66,7 +63,10 @@ export class ListOverflowComponent<T extends ListOverflowItem> implements AfterV
) {} ) {}
ngAfterViewInit () { ngAfterViewInit () {
setTimeout(() => this.onWindowResize(), 0) setTimeout(() => {
this.onWindowResize()
this.initialized = true
}, 0)
} }
isMenuDisplayed () { isMenuDisplayed () {
@ -100,32 +100,6 @@ export class ListOverflowComponent<T extends ListOverflowItem> implements AfterV
this.cdr.markForCheck() this.cdr.markForCheck()
} }
openDropdownOnHover (dropdown: NgbDropdown) {
this.openedOnHover = true
dropdown.open()
// Menu was closed
dropdown.openChange
.pipe(take(1))
.subscribe(() => this.openedOnHover = false)
}
dropdownAnchorClicked (dropdown: NgbDropdown) {
if (this.openedOnHover) {
this.openedOnHover = false
return
}
return dropdown.toggle()
}
closeDropdownIfHovered (dropdown: NgbDropdown) {
if (this.openedOnHover === false) return
dropdown.close()
this.openedOnHover = false
}
toggleModal () { toggleModal () {
this.modalService.open(this.modal, { centered: true }) this.modalService.open(this.modal, { centered: true })
} }

View File

@ -1,4 +1,4 @@
<ul class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed, 'no-scroll': isModalOpened }"> <ul class="sub-menu ul-unstyle" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed, 'no-scroll': isModalOpened }">
<ng-container *ngFor="let menuEntry of menuEntries; index as id"> <ng-container *ngFor="let menuEntry of menuEntries; index as id">
@if (isDisplayed(menuEntry)) { @if (isDisplayed(menuEntry)) {
@if (menuEntry.routerLink) { @if (menuEntry.routerLink) {
@ -21,7 +21,7 @@
<li ngbDropdown #dropdown="ngbDropdown" autoClose="true" container="body"> <li ngbDropdown #dropdown="ngbDropdown" autoClose="true" container="body">
<button ngbDropdownToggle class="sub-menu-entry" [ngClass]="{ active: !!suffixLabels[menuEntry.label] }">{{ menuEntry.label }}</button> <button ngbDropdownToggle class="sub-menu-entry" [ngClass]="{ active: !!suffixLabels[menuEntry.label] }">{{ menuEntry.label }}</button>
<ul ngbDropdownMenu> <ul class="ul-unstyle" ngbDropdownMenu>
<li *ngFor="let menuChild of menuEntry.children"> <li *ngFor="let menuChild of menuEntry.children">
<a <a
*ngIf="isDisplayed(menuChild)" ngbDropdownItem *ngIf="isDisplayed(menuChild)" ngbDropdownItem

View File

@ -1,12 +1,6 @@
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
ul {
list-style: none;
margin: 0;
padding: 0;
}
.sub-menu ::ng-deep .dropdown-toggle::after { .sub-menu ::ng-deep .dropdown-toggle::after {
position: relative; position: relative;
top: 2px; top: 2px;

View File

@ -7,6 +7,7 @@
.peertube-button { .peertube-button {
// Prevent weird border radius blur on chrome // Prevent weird border radius blur on chrome
z-index: unset !important; z-index: unset !important;
padding: 6px 10px;
} }
.dropdown-toggle::after { .dropdown-toggle::after {

View File

@ -1,15 +1,5 @@
<div class="margin-content"> <div class="margin-content">
<div class="videos-header mb-4"> <div class="videos-header mb-4">
<h1 *ngIf="displayTitle" class="title mb-1" placement="bottom" [ngbTooltip]="titleTooltip" container="body">
{{ title }}
</h1>
<div [ngClass]="{ 'no-title': !displayTitle, invisible: !syndicationItems || syndicationItems.length === 0 }" class="title-subscription">
<ng-container i18n>Subscribe to RSS feed "{{ title }}"</ng-container>
<my-feed [syndicationItems]="syndicationItems"></my-feed>
</div>
<div *ngIf="headerActions.length !== 0" class="action-block mt-3"> <div *ngIf="headerActions.length !== 0" class="action-block mt-3">
<ng-container *ngFor="let action of headerActions"> <ng-container *ngFor="let action of headerActions">
<a *ngIf="action.routerLink" class="ms-2" [routerLink]="action.routerLink" routerLinkActive="active"> <a *ngIf="action.routerLink" class="ms-2" [routerLink]="action.routerLink" routerLinkActive="active">
@ -25,8 +15,11 @@
</a> </a>
<ng-template #actionContent let-action> <ng-template #actionContent let-action>
<my-button *ngIf="!action.justIcon" [icon]="action.iconName" [label]="action.label"></my-button> @if (action.justIcon) {
<my-button *ngIf="action.justIcon" [icon]="action.iconName" [ngbTooltip]="action.label"></my-button> <my-button [icon]="action.iconName" [ngbTooltip]="action.label"></my-button>
} @else {
<my-button [icon]="action.iconName" [label]="action.label"></my-button>
}
</ng-template> </ng-template>
</ng-container> </ng-container>
</div> </div>

View File

@ -10,42 +10,10 @@ $margin-top: 2rem;
display: grid; display: grid;
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;
.title,
.title-subscription {
grid-column: 1;
}
.title {
font-size: 18px;
color: pvar(--mainForegroundColor);
display: inline-block;
font-weight: $font-semibold;
}
.title-subscription {
grid-row: 2;
font-size: 14px;
color: pvar(--greyForegroundColor);
&.no-title {
margin-top: 10px;
}
}
.action-block { .action-block {
grid-column: 3; grid-column: 3;
grid-row: 1/3; grid-row: 1/3;
} }
my-feed {
display: inline-block;
width: 16px;
color: pvar(--mainColor);
position: relative;
top: -2px;
@include margin-left(5px);
}
} }
.date-title { .date-title {

View File

@ -19,8 +19,8 @@ import { logger } from '@root-helpers/logger'
import debug from 'debug' import debug from 'debug'
import { Observable, Subject, Subscription, forkJoin, fromEvent, of } from 'rxjs' import { Observable, Subject, Subscription, forkJoin, fromEvent, of } from 'rxjs'
import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators' import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators'
import { InfiniteScrollerDirective } from '../shared-main/common/infinite-scroller.directive'
import { ButtonComponent } from '../shared-main/buttons/button.component' import { ButtonComponent } from '../shared-main/buttons/button.component'
import { InfiniteScrollerDirective } from '../shared-main/common/infinite-scroller.directive'
import { FeedComponent } from '../shared-main/feeds/feed.component' import { FeedComponent } from '../shared-main/feeds/feed.component'
import { Syndication } from '../shared-main/feeds/syndication.model' import { Syndication } from '../shared-main/feeds/syndication.model'
import { Video } from '../shared-main/video/video.model' import { Video } from '../shared-main/video/video.model'
@ -73,11 +73,6 @@ enum GroupDate {
export class VideosListComponent implements OnInit, OnChanges, OnDestroy { export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
@Input() getVideosObservableFunction: (pagination: ComponentPaginationLight, filters: VideoFilters) => Observable<ResultList<Video>> @Input() getVideosObservableFunction: (pagination: ComponentPaginationLight, filters: VideoFilters) => Observable<ResultList<Video>>
@Input() getSyndicationItemsFunction: (filters: VideoFilters) => Promise<Syndication[]> | Syndication[] @Input() getSyndicationItemsFunction: (filters: VideoFilters) => Promise<Syndication[]> | Syndication[]
@Input() baseRouteBuilderFunction: (filters: VideoFilters) => string[]
@Input() title: string
@Input() titleTooltip: string
@Input({ transform: booleanAttribute }) displayTitle = true
@Input() defaultSort: VideoSortField @Input() defaultSort: VideoSortField
@Input() defaultScope: VideoFilterScope = 'federated' @Input() defaultScope: VideoFilterScope = 'federated'
@ -426,20 +421,6 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
debugLogger('Will inject %O in URL query', queryParams) debugLogger('Will inject %O in URL query', queryParams)
const baseRoute = this.baseRouteBuilderFunction
? this.baseRouteBuilderFunction(this.filters)
: []
const pathname = window.location.pathname
const baseRouteChanged = baseRoute.length !== 0 &&
pathname !== '/' && // Exclude special '/' case, we'll be redirected without component change
baseRoute.length !== 0 && pathname !== baseRoute.join('/')
if (baseRouteChanged || Object.keys(baseQuery).length !== 0 || customizedByUser) {
this.peertubeRouter.silentNavigate(baseRoute, queryParams)
}
this.filtersChanged.emit(this.filters) this.filtersChanged.emit(this.filters)
} }

View File

@ -0,0 +1,3 @@
<my-home-menu></my-home-menu>
<router-outlet></router-outlet>

View File

@ -0,0 +1,23 @@
import { Component } from '@angular/core'
import { RouterOutlet } from '@angular/router'
import { HomeMenuComponent } from '@app/menu/home-menu.component'
import { DisableForReuseHook } from './core'
@Component({
templateUrl: './videos-parent.component.html',
standalone: true,
imports: [
HomeMenuComponent,
RouterOutlet
]
})
export class VideosParentComponent implements DisableForReuseHook {
disableForReuse () {
// empty
}
enabledForReuse () {
// empty
}
}

View File

@ -1,8 +1 @@
<svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis-vertical"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
<path d="M2 8.875C2.48325 8.875 2.875 8.48325 2.875 8C2.875 7.51675 2.48325 7.125 2 7.125C1.51675 7.125 1.125 7.51675 1.125 8C1.125 8.48325 1.51675 8.875 2 8.875Z" fill="currentColor"/>
<path d="M2 8.875C2.48325 8.875 2.875 8.48325 2.875 8C2.875 7.51675 2.48325 7.125 2 7.125C1.51675 7.125 1.125 7.51675 1.125 8C1.125 8.48325 1.51675 8.875 2 8.875Z" fill="currentColor" stroke="currentColor" stroke-linejoin="round"/>
<path d="M2 2.75C2.48325 2.75 2.875 2.35825 2.875 1.875C2.875 1.39175 2.48325 1 2 1C1.51675 1 1.125 1.39175 1.125 1.875C1.125 2.35825 1.51675 2.75 2 2.75Z" fill="currentColor"/>
<path d="M2 2.75C2.48325 2.75 2.875 2.35825 2.875 1.875C2.875 1.39175 2.48325 1 2 1C1.51675 1 1.125 1.39175 1.125 1.875C1.125 2.35825 1.51675 2.75 2 2.75Z" fill="currentColor" stroke="currentColor" stroke-linejoin="round"/>
<path d="M2 15C2.48325 15 2.875 14.6082 2.875 14.125C2.875 13.6418 2.48325 13.25 2 13.25C1.51675 13.25 1.125 13.6418 1.125 14.125C1.125 14.6082 1.51675 15 2 15Z" fill="currentColor"/>
<path d="M2 15C2.48325 15 2.875 14.6082 2.875 14.125C2.875 13.6418 2.48325 13.25 2 13.25C1.51675 13.25 1.125 13.6418 1.125 14.125C1.125 14.6082 1.51675 15 2 15Z" fill="currentColor" stroke="currentColor" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 320 B

View File

@ -79,7 +79,6 @@ body {
--secondary-text-emphasis: #553D3D; --secondary-text-emphasis: #553D3D;
font-family: $main-fonts; font-family: $main-fonts;
font-weight: $font-regular; font-weight: $font-regular;
color: pvar(--fg); color: pvar(--fg);
@ -100,6 +99,11 @@ body {
background-color: pvar(--mainHoverColor); background-color: pvar(--mainHoverColor);
} }
// Force to override bootstrap utilities
body [hidden] {
display: none !important;
}
noscript, noscript,
#incompatible-browser { #incompatible-browser {
display: block; display: block;

View File

@ -149,3 +149,14 @@
.transform-rotate-180 { .transform-rotate-180 {
transform: rotate(180deg); transform: rotate(180deg);
} }
// ---------------------------------------------------------------------------
.ul-unstyle {
margin: 0;
padding: 0;
li {
list-style: none;
}
}

View File

@ -16,7 +16,7 @@
@include margin-right(55px); @include margin-right(55px);
&.active { &.active {
border-bottom-color: pvar(--mainColor); border-bottom-color: pvar(--primary);
} }
&:hover, &:hover,

View File

@ -18,6 +18,10 @@ export const clientFilterHookObject = {
'filter:api.recently-added-videos.videos.list.params': true, 'filter:api.recently-added-videos.videos.list.params': true,
'filter:api.recently-added-videos.videos.list.result': true, 'filter:api.recently-added-videos.videos.list.result': true,
// Filter params/result of the function that fetch videos of the browse videos page
'filter:api.browse-videos.videos.list.params': true,
'filter:api.browse-videos.videos.list.result': true,
// Filter params/result of the function that fetch videos of the user subscription page // Filter params/result of the function that fetch videos of the user subscription page
'filter:api.user-subscriptions-videos.videos.list.params': true, 'filter:api.user-subscriptions-videos.videos.list.params': true,
'filter:api.user-subscriptions-videos.videos.list.result': true, 'filter:api.user-subscriptions-videos.videos.list.result': true,