Support quick filters

Chocobozzz 2024-11-21 09:12:50 +01:00
parent 77f7691ddb
commit c6821b689d
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
18 changed files with 220 additions and 71 deletions

View File

@ -26,8 +26,8 @@ export class AdminSettingsComponent implements OnInit {
private buildMenu () {
this.menuEntries = []
this.buildFederationItems()
this.buildConfigurationItems()
this.buildFederationItems()
this.buildPluginItems()
this.buildRunnerItems()
this.buildSystemItems()

View File

@ -48,7 +48,7 @@ export default [
redirectTo: () => {
const redirectService = inject(RedirectService)
return 'browse?scope=federated&sort=-' + redirectService.getDefaultTrendingAlgorithm()
return 'browse?scope=federated&sort=-' + redirectService.getDefaultTrendingSort()
}
},
{

View File

@ -9,7 +9,7 @@
<span class="me-2 fg-100">Quick access:</span>
@for (quickAccess of quickAccessLinks; track quickAccess.label) {
<a class="fg-300 text-decoration-underline me-2" [routerLink]="quickAccess.routerLink" [queryParams]="quickAccess.queryParams">{{ quickAccess.label }}</a>
<a class="me-2" [routerLink]="quickAccess.routerLink" [queryParams]="quickAccess.queryParams">{{ quickAccess.label }}</a>
}
</div>
@ -22,7 +22,7 @@
>
<div class="section videos" *ngFor="let object of objects">
<div class="section-header d-flex justify-content-between">
<div class="section-header d-flex justify-content-between align-items-start">
<h1 class="section-title">
<my-actor-avatar *ngIf="object.channel" size="40px" actorType="channel" [actor]="object.channel"></my-actor-avatar>
@ -32,7 +32,7 @@
<span i18n class="fg-100 fs-7">{{ object.type }}</span>
</h1>
<my-button theme="secondary" [routerLink]="object.routerLink" [queryParams]="object.queryParams">{{ object.buttonLabel }}</my-button>
<my-button theme="primary" [routerLink]="object.routerLink" [queryParams]="object.queryParams">{{ object.buttonLabel }}</my-button>
</div>
<div class="video-wrapper" *ngFor="let video of object.videos">

View File

@ -29,9 +29,21 @@
white-space: nowrap;
}
.quick-access-links:not(.see-all-quick-links) {
overflow: hidden;
text-overflow: ellipsis;
.quick-access-links {
a {
color: pvar(--fg-300);
text-decoration: underline;
&:hover {
opacity: 0.8;
}
}
&:not(.see-all-quick-links) {
overflow: hidden;
text-overflow: ellipsis;
}
}
.see-all-quick-links {

View File

@ -15,7 +15,7 @@ export class RedirectService {
private static SESSION_STORAGE_LATEST_SESSION_URL_KEY = 'redirect-latest-session-url'
// Default route could change according to the instance configuration
static INIT_DEFAULT_ROUTE = '/videos/trending'
static INIT_DEFAULT_ROUTE = '/videos/browse'
static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed'
private previousUrl: string
@ -70,7 +70,7 @@ export class RedirectService {
return this.defaultRoute
}
getDefaultTrendingAlgorithm () {
getDefaultTrendingSort () {
const algorithm = this.defaultTrendingAlgorithm
switch (algorithm) {

View File

@ -3,12 +3,20 @@
<div *ngIf="unreadNotifications >= 100" class="unread-notifications">99+</div>
</ng-template>
<ng-template #notificationIcon>
@if (unreadNotifications) {
<my-global-icon iconName="opened-bell"></my-global-icon>
} @else {
<my-global-icon iconName="bell"></my-global-icon>
}
</ng-template>
@if (isInMobileView) {
<div i18n-title title="View your notifications" class="peertube-button tertiary-button rounded-icon-button">
<ng-container *ngTemplateOutlet="notificationNumber"></ng-container>
<a routerLink="/my-account/notifications" routerLinkActive="active" #link (click)="onNavigate(link)">
<my-global-icon iconName="bell"></my-global-icon>
<ng-container *ngTemplateOutlet="notificationIcon"></ng-container>
</a>
</div>
} @else {
@ -22,8 +30,7 @@
ngbDropdownToggle
>
<ng-container *ngTemplateOutlet="notificationNumber"></ng-container>
<my-global-icon iconName="bell"></my-global-icon>
<ng-container *ngTemplateOutlet="notificationIcon"></ng-container>
</button>
<div ngbDropdownMenu>

View File

@ -97,7 +97,6 @@
background-color: pvar(--primary);
color: pvar(--on-primary);
border: 1px solid pvar(--on-primary);
font-weight: $font-bold;
font-size: 10px;

View File

@ -1,7 +1,6 @@
import { Component, OnInit } from '@angular/core'
import { AuthService, ServerService } from '@app/core'
import { HorizontalMenuComponent } from '@app/shared/shared-main/menu/horizontal-menu.component'
import { ListOverflowItem } from '@app/shared/shared-main/menu/list-overflow.component'
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
@Component({
selector: 'my-home-menu',
@ -10,7 +9,7 @@ import { ListOverflowItem } from '@app/shared/shared-main/menu/list-overflow.com
imports: [ HorizontalMenuComponent ]
})
export class HomeMenuComponent implements OnInit {
menuEntries: ListOverflowItem[] = []
menuEntries: HorizontalMenuEntry[] = []
constructor (
private server: ServerService,

View File

@ -27,6 +27,7 @@ const icons = {
'config': require('../../../assets/images/feather/config.svg'),
'award': require('../../../assets/images/feather/award.svg'),
'bell': require('../../../assets/images/feather/bell.svg'),
'opened-bell': require('../../../assets/images/feather/opened-bell.svg'),
'channel': require('../../../assets/images/feather/channel.svg'),
'chevrons-up': require('../../../assets/images/feather/chevrons-up.svg'),
'chevron-left': require('../../../assets/images/feather/chevron-left.svg'),

View File

@ -1,5 +1,9 @@
@if (ptRouterLink) {
<a class="action-button" [ngClass]="classes" [ngbTooltip]="title" [routerLink]="ptRouterLink">
@if (ptRouterLink || ptQueryParams) {
<a
class="action-button"
[ngClass]="classes" [ngbTooltip]="title"
[routerLink]="ptRouterLink" [queryParams]="ptQueryParams" [queryParamsHandling]="ptQueryParamsHandling"
>
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>
} @else {

View File

@ -1,12 +1,22 @@
import { ObserversModule } from '@angular/cdk/observers'
import { NgClass, NgIf, NgTemplateOutlet } from '@angular/common'
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, ViewChild, booleanAttribute } from '@angular/core'
import { RouterLink } from '@angular/router'
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnChanges,
ViewChild,
booleanAttribute
} from '@angular/core'
import { Params, QueryParamsHandling, RouterLink, RouterLinkActive } from '@angular/router'
import { GlobalIconName } from '@app/shared/shared-icons/global-icon.component'
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
import debug from 'debug'
import { GlobalIconComponent } from '../../shared-icons/global-icon.component'
import { LoaderComponent } from '../common/loader.component'
import debug from 'debug'
import { ObserversModule } from '@angular/cdk/observers'
const debugLogger = debug('peertube:button')
@ -16,15 +26,29 @@ const debugLogger = debug('peertube:button')
templateUrl: './button.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ NgIf, NgClass, NgbTooltip, NgTemplateOutlet, RouterLink, LoaderComponent, GlobalIconComponent, ObserversModule ]
imports: [
NgIf,
NgClass,
NgbTooltip,
NgTemplateOutlet,
RouterLink,
LoaderComponent,
GlobalIconComponent,
ObserversModule
]
})
export class ButtonComponent implements OnChanges, AfterViewInit {
@Input() label = ''
@Input() theme: 'primary' | 'secondary' | 'tertiary' = 'secondary'
@Input() icon: GlobalIconName
@Input() ptRouterLink: string[] | string
@Input() ptQueryParams: Params
@Input() ptQueryParamsHandling: QueryParamsHandling
@Input() title: string
@Input({ transform: booleanAttribute }) active = false
@Input({ transform: booleanAttribute }) loading = false
@Input({ transform: booleanAttribute }) disabled = false
@ -46,9 +70,12 @@ export class ButtonComponent implements OnChanges, AfterViewInit {
}
private buildClasses () {
const isButtonLink = !!this.ptRouterLink
this.classes = {
'peertube-button': !this.ptRouterLink,
'peertube-button-link': !!this.ptRouterLink,
'active': this.active,
'peertube-button': !isButtonLink,
'peertube-button-link': isButtonLink,
'primary-button': this.theme === 'primary',
'secondary-button': this.theme === 'secondary',
'tertiary-button': this.theme === 'tertiary',

View File

@ -2,12 +2,24 @@
<div class="first-row">
<div>
<my-button theme="secondary" icon="add" i18n class="me-2">Recently added</my-button>
<div class="d-flex flex-wrap me-2">
@for (quickFilter of quickFilters; track quickFilter) {
<my-button
theme="secondary" [icon]="quickFilter.iconName" class="me-2 mb-2"
ptRouterLink="." [ptQueryParams]="quickFilter.queryParams" [active]="quickFilter.isActive()" ptQueryParamsHandling="merge"
>{{ quickFilter.label }} </my-button>
}
</div>
<my-button theme="secondary" icon="trending" i18n class="me-2">Trending</my-button>
<div class="filters-summary d-inline-block ms-4">
<div class="d-inline-block active-filters">{{ getActiveFilters() }}</div>
<div class="filters-summary d-inline-block">
<div class="d-inline-block active-filters">
@for (filter of filters.getActiveFilters(); track filter.key + (filter.value || '')) {
<div class="d-inline-block">
<span i18n *ngIf="filter.value">{{ filter.label }}: </span>
<strong>{{ getFilterValue(filter) || filter.label }}</strong>
</div>
}
</div>
<button
class="filters-toggle peertube-button-like-link" (click)="areFiltersCollapsed = !areFiltersCollapsed"
@ -25,8 +37,8 @@
</div>
</div>
<div class="d-flex flex-wrap align-items-center" *ngIf="!hideScope">
<label for="scope" i18n class="select-label">Display videos of</label>
<div class="d-flex flex-wrap align-items-center ms-3" *ngIf="!hideScope">
<label for="scope" i18n class="select-label">Display videos of:</label>
<my-select-options inputId="scope" class="scope-select" formControlName="scope" [items]="availableScopes"></my-select-options>
</div>

View File

@ -20,6 +20,7 @@ $filters-background: pvar(--bg-secondary-400);
font-weight: normal;
margin-bottom: 0;
color: pvar(--fg-200);
font-size: 1rem;
@include margin-right(0.5rem);
}
@ -66,12 +67,21 @@ $filters-background: pvar(--bg-secondary-400);
}
}
.active-filters {
> div:not(:last-child)::after {
content: '';
font-weight: normal;
display: inline-block;
margin: 0 5px;
}
}
.filters-summary {
color: pvar(--fg-200);
}
.filters-toggle {
padding: pvar(--input-y-padding) 0.25rem calc(#{pvar(--input-y-padding)} + 0.75rem) 0.5rem;
padding: pvar(--input-y-padding) 0.5rem calc(#{pvar(--input-y-padding)} + 0.75rem) 0.75rem;
@include margin-left(0.25rem);

View File

@ -1,8 +1,8 @@
import { NgClass, NgIf, NgTemplateOutlet } from '@angular/common'
import { NgClass, NgIf } from '@angular/common'
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterLink } from '@angular/router'
import { AuthService } from '@app/core'
import { ActivatedRoute, Params, RouterLink } from '@angular/router'
import { AuthService, RedirectService } from '@app/core'
import { ServerService } from '@app/core/server/server.service'
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'
import { UserRight, VideoConstant } from '@peertube/peertube-models'
@ -13,14 +13,20 @@ import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.com
import { SelectCategoriesComponent } from '../shared-forms/select/select-categories.component'
import { SelectLanguagesComponent } from '../shared-forms/select/select-languages.component'
import { SelectOptionsComponent } from '../shared-forms/select/select-options.component'
import { GlobalIconComponent } from '../shared-icons/global-icon.component'
import { GlobalIconComponent, GlobalIconName } from '../shared-icons/global-icon.component'
import { ButtonComponent } from '../shared-main/buttons/button.component'
import { PeerTubeTemplateDirective } from '../shared-main/common/peertube-template.directive'
import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service'
import { VideoFilterActive, VideoFilters } from './video-filters.model'
const debugLogger = debug('peertube:videos:VideoFiltersHeaderComponent')
type QuickFilter = {
iconName: GlobalIconName
label: string
isActive: () => boolean
queryParams: Params
}
@Component({
selector: 'my-video-filters-header',
styleUrls: [ './video-filters-header.component.scss' ],
@ -34,12 +40,10 @@ const debugLogger = debug('peertube:videos:VideoFiltersHeaderComponent')
NgIf,
GlobalIconComponent,
NgbCollapse,
NgTemplateOutlet,
SelectLanguagesComponent,
SelectCategoriesComponent,
PeertubeCheckboxComponent,
SelectOptionsComponent,
PeerTubeTemplateDirective,
ButtonComponent
]
})
@ -57,6 +61,8 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
sortItems: SelectOptionsItem[] = []
availableScopes: SelectOptionsItem[] = []
quickFilters: QuickFilter[] = []
private videoCategories: VideoConstant<number>[] = []
private videoLanguages: VideoConstant<string>[] = []
@ -66,7 +72,9 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
private auth: AuthService,
private serverService: ServerService,
private fb: FormBuilder,
private modalService: PeertubeModalService
private modalService: PeertubeModalService,
private redirectService: RedirectService,
private route: ActivatedRoute
) {
}
@ -83,6 +91,11 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
this.patchForm(false)
this.routeSub = this.route.queryParams.subscribe(query => {
this.filters.load(query)
this.filtersChanged.emit()
})
this.filters.onChange(() => {
this.patchForm(false)
})
@ -100,12 +113,13 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
this.serverService.getVideoLanguages()
.subscribe(languages => this.videoLanguages = languages)
this.buildSortItems()
this.availableScopes = [
{ id: 'local', label: $localize`This platform only` },
{ id: 'federated', label: $localize`All platforms` }
]
this.buildSortItems()
this.buildQuickFilters()
}
ngOnDestroy () {
@ -119,6 +133,30 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS)
}
// ---------------------------------------------------------------------------
private buildQuickFilters () {
const trendingSort = this.redirectService.getDefaultTrendingSort()
this.quickFilters = [
{
label: $localize`Recently added`,
iconName: 'add',
isActive: () => this.filters.sort === '-publishedAt',
queryParams: { sort: '-publishedAt' }
},
{
label: $localize`Trending`,
iconName: 'trending',
isActive: () => this.filters.sort === trendingSort,
queryParams: { sort: trendingSort }
}
]
}
// ---------------------------------------------------------------------------
private buildSortItems () {
this.sortItems = [
{ id: '-publishedAt', label: $localize`Recently Added` },
@ -147,29 +185,7 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
return serverConfig.trending.videos.algorithms.enabled.includes(sort)
}
getActiveFilters () {
const store: string[] = []
for (const activeFilter of this.filters.getActiveFilters()) {
if (activeFilter.value) {
store.push($localize`${activeFilter.label}\: ${this.getFilterValue(activeFilter)}`)
} else {
store.push(activeFilter.label)
}
}
const output = store.reduce((p, c) => {
if (!p) return c
return $localize`${p}, ${c}`
}, '')
if (output) return `${output}.`
return output
}
private getFilterValue (filter: VideoFilterActive) {
getFilterValue (filter: VideoFilterActive) {
if ((filter.key === 'categoryOneOf' || filter.key === 'languageOneOf') && Array.isArray(filter.rawValue)) {
if (filter.rawValue.length > 2) {
return filter.rawValue.length

View File

@ -192,13 +192,13 @@ export class VideoFilters {
this.activeFilters.push({
key: 'live',
canRemove: true,
label: $localize`Live videos`
label: $localize`Only lives`
})
} else if (this.live === 'false') {
this.activeFilters.push({
key: 'live',
canRemove: true,
label: $localize`VOD videos`
label: $localize`Only VOD`
})
}

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="opened-bell.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="29.429111"
inkscape:cx="12.402685"
inkscape:cy="12.504625"
inkscape:window-width="1916"
inkscape:window-height="1036"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<g
clip-path="url(#clip0_951_11135)"
id="g2"
transform="matrix(1.0570193,0,0,1.0639227,2.2012811,1.3901775)">
<path
d="m 9.24991,20.2 c -0.49407,0.0164 -0.98377,-0.0974 -1.42,-0.33 -0.41075,-0.2457 -0.75429,-0.5893 -1,-1 C 6.75756,18.7562 6.70898,18.6289 6.68709,18.4958 6.6652,18.3627 6.67045,18.2266 6.70254,18.0956 c 0.03209,-0.131 0.09034,-0.2541 0.17126,-0.362 0.08091,-0.1079 0.18282,-0.1983 0.29958,-0.2658 0.11677,-0.0675 0.24598,-0.1107 0.37987,-0.127 0.13388,-0.0162 0.26967,-0.0053 0.3992,0.0323 0.12953,0.0376 0.25011,0.101 0.35451,0.1864 0.10439,0.0854 0.19044,0.191 0.25295,0.3105 0.07338,0.1235 0.17652,0.2266 0.3,0.3 0.12681,0.0709 0.26969,0.1082 0.415,0.1082 0.14531,0 0.28819,-0.0373 0.415,-0.1082 0.12856,-0.0701 0.23569,-0.1738 0.31,-0.3 0.06529,-0.1146 0.15269,-0.2151 0.25699,-0.2958 0.1044,-0.0807 0.2237,-0.1399 0.351,-0.1743 0.1273,-0.0344 0.2602,-0.0433 0.391,-0.0261 0.1308,0.0172 0.2569,0.0601 0.371,0.1262 0.1138,0.0662 0.2133,0.1541 0.293,0.2589 0.0797,0.1047 0.138,0.2241 0.1714,0.3514 0.0334,0.1272 0.0414,0.2599 0.0235,0.3902 -0.0179,0.1304 -0.0614,0.256 -0.1279,0.3695 -0.244,0.4121 -0.5879,0.756 -1,1 -0.4528,0.246 -0.96552,0.3603 -1.47999,0.33 z"
fill="currentColor"
id="path1" />
<path
d="M 15.3902,13.62 H 3.11023 C 4.2914,11.392 4.85748,8.88946 4.75023,6.37 4.74911,5.779 4.86519,5.19365 5.09177,4.6478 5.31835,4.10196 5.65091,3.60647 6.07023,3.19 6.20584,3.04845 6.33128,2.92195 6.45814,2.80776 6.95281,2.3625 7.41615,1.83811 7.69504,1.23382 7.91689,0.753114 8.20845,0.291554 8.5,0 6.5,0 5.15472,1.0781 4.2088,2.23908 3.26289,3.40007 2.74757,4.85245 2.75023,6.35 c 0,5.69 -2.299998,7.41 -2.299998,7.42 -0.186666,0.1155 -0.33012,0.2893 -0.4082307,0.4945 -0.0781105,0.2051 -0.0865361,0.4303 -0.0239754,0.6407 0.0625608,0.2105 0.1926311,0.3945 0.3701401,0.5236 C 0.565675,15.558 0.78076,15.6252 1.00023,15.62 H 17.5002 c 0.2104,-0.0029 0.4146,-0.0722 0.5833,-0.1979 0.1688,-0.1256 0.2936,-0.3014 0.3567,-0.5021 0.14,-0.41 -0.15,-1.37 -0.94,-1.35 -0.62,0.03 -1.41,0.05 -2.11,0.05 z"
fill="currentColor"
id="path2" />
</g>
<defs
id="defs2">
<clipPath
id="clip0_951_11135">
<rect
width="18.48"
height="20.200001"
fill="#ff0000"
id="rect2"
x="0"
y="0" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -11,6 +11,7 @@
}
&:active,
&.active,
&:focus,
&:focus-visible {
color: $fg;
@ -48,6 +49,7 @@
&,
&:active,
&.active,
&:focus {
color: pvar(--on-primary);
background-color: pvar(--primary);
@ -127,6 +129,7 @@
&:hover,
&:active,
&.active,
&:focus:not(:focus-visible) {
opacity: 0.8;
}

View File

@ -243,7 +243,7 @@ body .p-dropdown-panel .p-dropdown-items .p-dropdown-item {
}
.p-dropdown-panel .p-dropdown-items .p-dropdown-item:not(.p-highlight):not(.p-disabled):hover {
color: pvar(--fg);
background: pvar(--bg-secondary-500);
background: pvar(--bg-secondary-400);
}
.p-dropdown-panel .p-dropdown-items .p-dropdown-item-group {
margin: 0;
@ -565,7 +565,7 @@ p-chips.p-chips-clearable .p-chips-clear-icon {
background: pvar(--bg-secondary-500);
}
.p-multiselect.p-variant-filled:not(.p-disabled):hover {
background-color: pvar(--bg-secondary-500);
background-color: pvar(--bg-secondary-400);
}
.p-multiselect.p-variant-filled:not(.p-disabled).p-focus {
background-color: pvar(--bg-secondary-500);