diff --git a/client/src/app/+accounts/account-videos/account-videos.component.html b/client/src/app/+accounts/account-videos/account-videos.component.html index 6b9e2bb9f..86cfca3e1 100644 --- a/client/src/app/+accounts/account-videos/account-videos.component.html +++ b/client/src/app/+accounts/account-videos/account-videos.component.html @@ -4,20 +4,22 @@ *ngIf="account" [title]="title" - [displayTitle]="false" + displayTitle="false" [getVideosObservableFunction]="getVideosObservableFunction" [getSyndicationItemsFunction]="getSyndicationItemsFunction" [defaultSort]="defaultSort" - [displayFilters]="true" - [displayModerationBlock]="true" + displayFilters="true" + displayModerationBlock="true" [displayAsRow]="displayAsRow()" - [hideScopeFilter]="true" + hideScopeFilter="true" - [loadUserVideoPreferences]="true" + loadUserVideoPreferences="true" + + highlightLives="true" [disabled]="disabled" > diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.html b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.html index b70f5569a..4e8f4bba2 100644 --- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.html +++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.html @@ -4,21 +4,24 @@ *ngIf="videoChannel" [title]="title" - [displayTitle]="false" + displayTitle="false" [getVideosObservableFunction]="getVideosObservableFunction" [getSyndicationItemsFunction]="getSyndicationItemsFunction" [defaultSort]="defaultSort" - [displayFilters]="true" - [displayModerationBlock]="true" + displayFilters="true" + displayModerationBlock="true" + [displayOptions]="displayOptions" [displayAsRow]="displayAsRow()" - [hideScopeFilter]="true" + hideScopeFilter="true" - [loadUserVideoPreferences]="true" + loadUserVideoPreferences="true" + + highlightLives="true" [disabled]="disabled" diff --git a/client/src/app/+videos/video-list/video-user-subscriptions.component.html b/client/src/app/+videos/video-list/video-user-subscriptions.component.html index 5536bb945..90eceaf9b 100644 --- a/client/src/app/+videos/video-list/video-user-subscriptions.component.html +++ b/client/src/app/+videos/video-list/video-user-subscriptions.component.html @@ -6,11 +6,13 @@ [defaultSort]="defaultSort" - [displayFilters]="false" - [displayModerationBlock]="false" + displayFilters="false" + displayModerationBlock="false" - [loadUserVideoPreferences]="false" - [groupByDate]="true" + loadUserVideoPreferences="false" + groupByDate="true" + + highlightLives="false" [disabled]="disabled" diff --git a/client/src/app/+videos/video-list/videos-list-common-page.component.html b/client/src/app/+videos/video-list/videos-list-common-page.component.html index 2831f996f..f5139dcb5 100644 --- a/client/src/app/+videos/video-list/videos-list-common-page.component.html +++ b/client/src/app/+videos/video-list/videos-list-common-page.component.html @@ -9,12 +9,14 @@ [defaultSort]="defaultSort" [defaultScope]="defaultScope" - [displayFilters]="true" - [displayModerationBlock]="true" + displayFilters="true" + displayModerationBlock="true" - [loadUserVideoPreferences]="true" + loadUserVideoPreferences="true" [groupByDate]="groupByDate" + highlightLives="true" + [disabled]="disabled" (filtersChanged)="onFiltersChanged($event)" diff --git a/client/src/app/shared/shared-main/buttons/action-dropdown.component.scss b/client/src/app/shared/shared-main/buttons/action-dropdown.component.scss index 05777a7db..d39f7c048 100644 --- a/client/src/app/shared/shared-main/buttons/action-dropdown.component.scss +++ b/client/src/app/shared/shared-main/buttons/action-dropdown.component.scss @@ -86,7 +86,7 @@ .owner-moderator-privilege { margin: 0 !important; - @include margin-left(1rem !important); + @include margin-left(0.25rem !important); width: 13px !important; } diff --git a/client/src/app/shared/shared-video-miniature/video-filters.model.ts b/client/src/app/shared/shared-video-miniature/video-filters.model.ts index 8db17c015..74660b4fa 100644 --- a/client/src/app/shared/shared-video-miniature/video-filters.model.ts +++ b/client/src/app/shared/shared-video-miniature/video-filters.model.ts @@ -57,6 +57,8 @@ export class VideoFilters { this.reset() } + // --------------------------------------------------------------------------- + onChange (cb: () => void) { this.onChangeCallbacks.push(cb) } @@ -73,6 +75,8 @@ export class VideoFilters { } } + // --------------------------------------------------------------------------- + setDefaultScope (scope: VideoFilterScope) { this.defaultValues.set('scope', scope) } @@ -85,6 +89,8 @@ export class VideoFilters { this.updateDefaultNSFW(nsfwPolicy) } + // --------------------------------------------------------------------------- + reset (specificKey?: string) { for (const [ key, value ] of this.defaultValues) { if (specificKey && specificKey !== key) continue @@ -95,6 +101,8 @@ export class VideoFilters { this.buildActiveFilters() } + // --------------------------------------------------------------------------- + load (obj: Partial>) { // FIXME: We may use that doesn't escape HTML so prefer to escape things // https://github.com/ng-select/ng-select/issues/1363 @@ -122,6 +130,16 @@ export class VideoFilters { this.buildActiveFilters() } + clone () { + const cloned = new VideoFilters(this.defaultValues.get('sort'), this.defaultValues.get('scope'), this.hiddenFields) + + cloned.load(this.toUrlObject()) + + return cloned + } + + // --------------------------------------------------------------------------- + buildActiveFilters () { this.activeFilters = [] @@ -189,6 +207,8 @@ export class VideoFilters { return this.activeFilters } + // --------------------------------------------------------------------------- + toFormObject (): VideoFiltersKeys { const result: Partial = {} @@ -242,6 +262,8 @@ export class VideoFilters { } } + // --------------------------------------------------------------------------- + getNSFWDisplayLabel () { if (this.defaultNSFWPolicy === 'blur') return $localize`Blurred` 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 6fea36f4c..ae4c5cda6 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 @@ -40,10 +40,31 @@
No results.
+ +

+ Current lives +

+ + +
+ + +
+
+ +

+ Videos +

+
+

{{ getCurrentGroupedDateLabel(video) }} 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 f00fc79bb..d1a35f899 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 @@ -76,6 +76,11 @@ $margin-top: 2rem; } } +my-global-icon[iconname=live] { + position: relative; + top: -1px; +} + @media screen and (max-width: $mobile-view) { my-video-filters-header { @include margin-left(1rem); diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts index 72010417c..81b6488a6 100644 --- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts @@ -1,8 +1,6 @@ -import * as debug from 'debug' -import { fromEvent, Observable, Subject, Subscription } from 'rxjs' -import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators' -import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core' -import { ActivatedRoute, RouterLinkActive, RouterLink } from '@angular/router' +import { NgClass, NgFor, NgIf, NgTemplateOutlet } from '@angular/common' +import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, booleanAttribute } from '@angular/core' +import { ActivatedRoute, RouterLink, RouterLinkActive } from '@angular/router' import { AuthService, ComponentPaginationLight, @@ -13,20 +11,22 @@ import { User, UserService } from '@app/core' -import { GlobalIconName } from '@app/shared/shared-icons/global-icon.component' -import { logger } from '@root-helpers/logger' +import { GlobalIconComponent, GlobalIconName } from '@app/shared/shared-icons/global-icon.component' +import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap' import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@peertube/peertube-core-utils' import { ResultList, UserRight, VideoSortField } from '@peertube/peertube-models' -import { VideoFilters, VideoFilterScope } from './video-filters.model' -import { MiniatureDisplayOptions, VideoMiniatureComponent } from './video-miniature.component' +import { logger } from '@root-helpers/logger' +import * as debug from 'debug' +import { Observable, Subject, Subscription, forkJoin, fromEvent, of } from 'rxjs' +import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators' import { InfiniteScrollerDirective } from '../shared-main/angular/infinite-scroller.directive' -import { VideoFiltersHeaderComponent } from './video-filters-header.component' import { ButtonComponent } from '../shared-main/buttons/button.component' import { FeedComponent } from '../shared-main/feeds/feed.component' -import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap' -import { NgIf, NgClass, NgFor, NgTemplateOutlet } from '@angular/common' -import { Video } from '../shared-main/video/video.model' import { Syndication } from '../shared-main/feeds/syndication.model' +import { Video } from '../shared-main/video/video.model' +import { VideoFiltersHeaderComponent } from './video-filters-header.component' +import { VideoFilterScope, VideoFilters } from './video-filters.model' +import { MiniatureDisplayOptions, VideoMiniatureComponent } from './video-miniature.component' const debugLogger = debug('peertube:videos:VideosListComponent') @@ -66,7 +66,8 @@ enum GroupDate { ButtonComponent, VideoFiltersHeaderComponent, InfiniteScrollerDirective, - VideoMiniatureComponent + VideoMiniatureComponent, + GlobalIconComponent ] }) export class VideosListComponent implements OnInit, OnChanges, OnDestroy { @@ -76,35 +77,38 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { @Input() title: string @Input() titleTooltip: string - @Input() displayTitle = true + @Input({ transform: booleanAttribute }) displayTitle = true @Input() defaultSort: VideoSortField @Input() defaultScope: VideoFilterScope = 'federated' - @Input() displayFilters = false - @Input() displayModerationBlock = false + @Input({ transform: booleanAttribute }) displayFilters = false + @Input({ transform: booleanAttribute }) displayModerationBlock = false - @Input() loadUserVideoPreferences = false + @Input({ transform: booleanAttribute }) loadUserVideoPreferences = false - @Input() displayAsRow = false - @Input() displayVideoActions = true - @Input() groupByDate = false + @Input({ transform: booleanAttribute }) displayAsRow = false + @Input({ transform: booleanAttribute }) displayVideoActions = true + @Input({ transform: booleanAttribute }) groupByDate = false + @Input({ transform: booleanAttribute }) highlightLives = false @Input() headerActions: HeaderAction[] = [] - @Input() hideScopeFilter = false + @Input({ transform: booleanAttribute }) hideScopeFilter = false @Input() displayOptions: MiniatureDisplayOptions - @Input() disabled = false + @Input({ transform: booleanAttribute }) disabled = false @Output() filtersChanged = new EventEmitter() @Output() videosLoaded = new EventEmitter() videos: Video[] = [] + highlightedLives: Video[] = [] + filters: VideoFilters syndicationItems: Syndication[] - onDataSubject = new Subject() + onVideosDataSubject = new Subject() hasDoneFirstQuery = false userMiniature: User @@ -133,7 +137,11 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { private lastQueryLength: number - private videoRequests = new Subject<{ reset: boolean, obs: Observable> }>() + private videoRequests = new Subject<{ + reset: boolean + obsVideos: Observable, 'data'>> + obsHighlightedLives: Observable, 'data'>> + }>() private alreadyDoneSearch = false @@ -161,12 +169,12 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { this.groupedDateLabels = { [GroupDate.UNKNOWN]: null, - [GroupDate.TODAY]: $localize`Today`, - [GroupDate.YESTERDAY]: $localize`Yesterday`, - [GroupDate.THIS_WEEK]: $localize`This week`, - [GroupDate.THIS_MONTH]: $localize`This month`, - [GroupDate.LAST_MONTH]: $localize`Last month`, - [GroupDate.OLDER]: $localize`Older` + [GroupDate.TODAY]: $localize`Today's videos`, + [GroupDate.YESTERDAY]: $localize`Yesterday's videos`, + [GroupDate.THIS_WEEK]: $localize`This week's videos`, + [GroupDate.THIS_MONTH]: $localize`This month's videos`, + [GroupDate.LAST_MONTH]: $localize`Last month's videos`, + [GroupDate.OLDER]: $localize`Older videos` } this.resizeSub = fromEvent(window, 'resize') @@ -256,12 +264,31 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { } loadMoreVideos (reset = false) { + let liveFilters: VideoFilters + let videoFilters: VideoFilters + if (reset) { this.hasDoneFirstQuery = false this.videos = [] + this.highlightedLives = [] + + if (this.highlightLives && (!this.filters.live || this.filters.live === 'both')) { + liveFilters = this.filters.clone() + liveFilters.live = 'true' + + videoFilters = this.filters.clone() + videoFilters.live = 'false' + } } - this.videoRequests.next({ reset, obs: this.getVideosObservableFunction(this.pagination, this.filters) }) + this.videoRequests.next({ + reset, + obsVideos: this.getVideosObservableFunction(this.pagination, videoFilters ?? this.filters), + + obsHighlightedLives: liveFilters + ? this.getVideosObservableFunction(this.pagination, liveFilters) + : of(({ data: this.highlightedLives })) + }) } reloadVideos () { @@ -271,6 +298,7 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { removeVideoFromArray (video: Video) { this.videos = this.videos.filter(v => v.id !== video.id) + this.highlightedLives = this.highlightedLives.filter(v => v.id !== video.id) } buildGroupedDateLabels () { @@ -338,6 +366,7 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { scheduleOnFiltersChanged (customizedByUser: boolean) { // We'll reload videos, but avoid weird UI effect this.videos = [] + this.highlightedLives = [] setTimeout(() => this.onFiltersChanged(customizedByUser)) } @@ -444,19 +473,29 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { private subscribeToVideoRequests () { this.videoRequests - .pipe(concatMap(({ reset, obs }) => obs.pipe(map(({ data }) => ({ data, reset }))))) + .pipe( + concatMap(({ reset, obsHighlightedLives, obsVideos }) => { + return forkJoin([ obsHighlightedLives, obsVideos ]) + .pipe( + map(([ resHighlightedLives, resVideos ]) => ({ highlightedLives: resHighlightedLives.data, videos: resVideos.data, reset })) + ) + }) + ) .subscribe({ - next: ({ data, reset }) => { + next: ({ videos, highlightedLives, reset }) => { this.hasDoneFirstQuery = true - this.lastQueryLength = data.length + this.lastQueryLength = videos.length - if (reset) this.videos = [] + if (reset) { + this.videos = [] + this.highlightedLives = highlightedLives + } - this.videos = this.videos.concat(data) + this.videos = this.videos.concat(videos) if (this.groupByDate) this.buildGroupedDateLabels() - this.onDataSubject.next(data) + this.onVideosDataSubject.next(videos) this.videosLoaded.emit(this.videos) },