From 911186dae411d78788ccede093c251303187589a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 29 Jun 2021 17:18:30 +0200 Subject: Reorganize watch components --- .../+video-watch/shared/recommendations/index.ts | 5 ++ .../recent-videos-recommendation.service.ts | 79 ++++++++++++++++++ .../recommendations/recommendation-info.model.ts | 4 + .../recommendations/recommendations.module.ts | 35 ++++++++ .../recommendations/recommendations.service.ts | 7 ++ .../recommended-videos.component.html | 26 ++++++ .../recommended-videos.component.scss | 68 ++++++++++++++++ .../recommended-videos.component.ts | 95 ++++++++++++++++++++++ .../recommendations/recommended-videos.store.ts | 37 +++++++++ 9 files changed, 356 insertions(+) create mode 100644 client/src/app/+videos/+video-watch/shared/recommendations/index.ts create mode 100644 client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts create mode 100644 client/src/app/+videos/+video-watch/shared/recommendations/recommendation-info.model.ts create mode 100644 client/src/app/+videos/+video-watch/shared/recommendations/recommendations.module.ts create mode 100644 client/src/app/+videos/+video-watch/shared/recommendations/recommendations.service.ts create mode 100644 client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.html create mode 100644 client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss create mode 100644 client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.ts create mode 100644 client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.store.ts (limited to 'client/src/app/+videos/+video-watch/shared/recommendations') diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/index.ts b/client/src/app/+videos/+video-watch/shared/recommendations/index.ts new file mode 100644 index 000000000..ffcf84585 --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/recommendations/index.ts @@ -0,0 +1,5 @@ +export * from './recent-videos-recommendation.service' +export * from './recommendation-info.model' +export * from './recommendations.module' +export * from './recommended-videos.component' +export * from './recommended-videos.store' diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts new file mode 100644 index 000000000..4654da847 --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts @@ -0,0 +1,79 @@ +import { Observable, of } from 'rxjs' +import { map, switchMap } from 'rxjs/operators' +import { Injectable } from '@angular/core' +import { ServerService, UserService } from '@app/core' +import { Video, VideoService } from '@app/shared/shared-main' +import { AdvancedSearch, SearchService } from '@app/shared/shared-search' +import { HTMLServerConfig } from '@shared/models' +import { RecommendationInfo } from './recommendation-info.model' +import { RecommendationService } from './recommendations.service' + +/** + * Provides "recommendations" by providing the most recently uploaded videos. + */ +@Injectable() +export class RecentVideosRecommendationService implements RecommendationService { + readonly pageSize = 5 + + private config: HTMLServerConfig + + constructor ( + private videos: VideoService, + private searchService: SearchService, + private userService: UserService, + private serverService: ServerService + ) { + this.config = this.serverService.getHTMLConfig() + } + + getRecommendations (recommendation: RecommendationInfo): Observable { + + return this.fetchPage(1, recommendation) + .pipe( + map(videos => { + const otherVideos = videos.filter(v => v.uuid !== recommendation.uuid) + return otherVideos.slice(0, this.pageSize) + }) + ) + } + + private fetchPage (page: number, recommendation: RecommendationInfo): Observable { + const pagination = { currentPage: page, itemsPerPage: this.pageSize + 1 } + const defaultSubscription = this.videos.getVideos({ videoPagination: pagination, sort: '-createdAt' }) + .pipe(map(v => v.data)) + + const tags = recommendation.tags + const searchIndexConfig = this.config.search.searchIndex + if ( + !tags || tags.length === 0 || + (searchIndexConfig.enabled === true && searchIndexConfig.disableLocalSearch === true) + ) { + return defaultSubscription + } + + return this.userService.getAnonymousOrLoggedUser() + .pipe( + map(user => { + return { + search: '', + componentPagination: pagination, + advancedSearch: new AdvancedSearch({ + tagsOneOf: recommendation.tags.join(','), + sort: '-publishedAt', + searchTarget: 'local', + nsfw: user.nsfwPolicy + ? this.videos.nsfwPolicyToParam(user.nsfwPolicy) + : undefined + }) + } + }), + switchMap(params => this.searchService.searchVideos(params)), + map(v => v.data), + switchMap(videos => { + if (videos.length <= 1) return defaultSubscription + + return of(videos) + }) + ) + } +} diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommendation-info.model.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recommendation-info.model.ts new file mode 100644 index 000000000..0233563bb --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommendation-info.model.ts @@ -0,0 +1,4 @@ +export interface RecommendationInfo { + uuid: string + tags?: string[] +} diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommendations.module.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recommendations.module.ts new file mode 100644 index 000000000..1417f3e2a --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommendations.module.ts @@ -0,0 +1,35 @@ + +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { SharedFormModule } from '@app/shared/shared-forms' +import { SharedMainModule } from '@app/shared/shared-main' +import { SharedSearchModule } from '@app/shared/shared-search' +import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' +import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' +import { RecentVideosRecommendationService } from './recent-videos-recommendation.service' +import { RecommendedVideosComponent } from './recommended-videos.component' +import { RecommendedVideosStore } from './recommended-videos.store' + +@NgModule({ + imports: [ + CommonModule, + + SharedMainModule, + SharedSearchModule, + SharedVideoPlaylistModule, + SharedVideoMiniatureModule, + SharedFormModule + ], + declarations: [ + RecommendedVideosComponent + ], + exports: [ + RecommendedVideosComponent + ], + providers: [ + RecommendedVideosStore, + RecentVideosRecommendationService + ] +}) +export class RecommendationsModule { +} diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommendations.service.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recommendations.service.ts new file mode 100644 index 000000000..1d79d35f6 --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommendations.service.ts @@ -0,0 +1,7 @@ +import { Observable } from 'rxjs' +import { Video } from '@app/shared/shared-main' +import { RecommendationInfo } from './recommendation-info.model' + +export interface RecommendationService { + getRecommendations (recommendation: RecommendationInfo): Observable +} diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.html b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.html new file mode 100644 index 000000000..e1040fead --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.html @@ -0,0 +1,26 @@ +
+ +
+

+ Other videos +

+
+ AUTOPLAY + +
+
+ + + + + +
+
+
+
diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss new file mode 100644 index 000000000..84ed25ae8 --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss @@ -0,0 +1,68 @@ +@use '_variables' as *; +@use '_mixins' as *; + +.title-page-container { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 25px; + flex-wrap: wrap-reverse; + + .title-page.active, + .title-page.title-page-single { + @include margin-right(.5rem !important); + + margin-bottom: unset; + } +} + +.title-page { + margin-top: 0; +} + +.title-page-autoplay { + @include margin-left(auto); + + display: flex; + width: max-content; + height: max-content; + align-items: center; + + span { + @include margin-right(0.3rem); + + text-transform: uppercase; + font-size: 85%; + font-weight: 600; + } +} + +hr { + margin-top: 0; +} + +my-video-miniature { + display: block; +} + +.other-videos:not(.display-as-row) my-video-miniature { + min-width: $video-thumbnail-medium-width; + max-width: $video-thumbnail-medium-width; +} + +.display-as-row { + my-video-miniature { + margin-bottom: 20px; + } + + hr { + display: none; + } + + @media screen and (max-width: $mobile-view) { + my-video-miniature { + margin-bottom: 10px; + } + } +} + diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.ts new file mode 100644 index 000000000..89b9c01b6 --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.ts @@ -0,0 +1,95 @@ +import { Observable } from 'rxjs' +import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' +import { AuthService, Notifier, SessionStorageService, User, UserService } from '@app/core' +import { Video } from '@app/shared/shared-main' +import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature' +import { VideoPlaylist } from '@app/shared/shared-video-playlist' +import { UserLocalStorageKeys } from '@root-helpers/users' +import { RecommendationInfo } from './recommendation-info.model' +import { RecommendedVideosStore } from './recommended-videos.store' + +@Component({ + selector: 'my-recommended-videos', + templateUrl: './recommended-videos.component.html', + styleUrls: [ './recommended-videos.component.scss' ] +}) +export class RecommendedVideosComponent implements OnInit, OnChanges { + @Input() inputRecommendation: RecommendationInfo + @Input() playlist: VideoPlaylist + @Input() displayAsRow: boolean + + @Output() gotRecommendations = new EventEmitter() + + autoPlayNextVideo: boolean + autoPlayNextVideoTooltip: string + + displayOptions: MiniatureDisplayOptions = { + date: true, + views: true, + by: true, + avatar: true + } + + userMiniature: User + + readonly hasVideos$: Observable + readonly videos$: Observable + + constructor ( + private userService: UserService, + private authService: AuthService, + private notifier: Notifier, + private store: RecommendedVideosStore, + private sessionStorageService: SessionStorageService + ) { + this.videos$ = this.store.recommendations$ + this.hasVideos$ = this.store.hasRecommendations$ + this.videos$.subscribe(videos => this.gotRecommendations.emit(videos)) + + if (this.authService.isLoggedIn()) { + this.autoPlayNextVideo = this.authService.getUser().autoPlayNextVideo + } else { + this.autoPlayNextVideo = this.sessionStorageService.getItem(UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true' + + this.sessionStorageService.watch([UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO]).subscribe( + () => { + this.autoPlayNextVideo = this.sessionStorageService.getItem(UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true' + } + ) + } + + this.autoPlayNextVideoTooltip = $localize`When active, the next video is automatically played after the current one.` + } + + ngOnInit () { + this.userService.getAnonymousOrLoggedUser() + .subscribe(user => this.userMiniature = user) + } + + ngOnChanges () { + if (this.inputRecommendation) { + this.store.requestNewRecommendations(this.inputRecommendation) + } + } + + onVideoRemoved () { + this.store.requestNewRecommendations(this.inputRecommendation) + } + + switchAutoPlayNextVideo () { + this.sessionStorageService.setItem(UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO, this.autoPlayNextVideo.toString()) + + if (this.authService.isLoggedIn()) { + const details = { + autoPlayNextVideo: this.autoPlayNextVideo + } + + this.userService.updateMyProfile(details).subscribe( + () => { + this.authService.refreshUserInformation() + }, + err => this.notifier.error(err.message) + ) + } + } +} diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.store.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.store.ts new file mode 100644 index 000000000..8c3fb6480 --- /dev/null +++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.store.ts @@ -0,0 +1,37 @@ +import { Observable, ReplaySubject } from 'rxjs' +import { map, shareReplay, switchMap, take } from 'rxjs/operators' +import { Inject, Injectable } from '@angular/core' +import { Video } from '@app/shared/shared-main' +import { RecentVideosRecommendationService } from './recent-videos-recommendation.service' +import { RecommendationInfo } from './recommendation-info.model' +import { RecommendationService } from './recommendations.service' + +/** + * This store is intended to provide data for the RecommendedVideosComponent. + */ +@Injectable() +export class RecommendedVideosStore { + public readonly recommendations$: Observable + public readonly hasRecommendations$: Observable + private readonly requestsForLoad$$ = new ReplaySubject(1) + + constructor ( + @Inject(RecentVideosRecommendationService) private recommendations: RecommendationService + ) { + this.recommendations$ = this.requestsForLoad$$.pipe( + switchMap(requestedRecommendation => { + return this.recommendations.getRecommendations(requestedRecommendation) + .pipe(take(1)) + }), + shareReplay() + ) + + this.hasRecommendations$ = this.recommendations$.pipe( + map(otherVideos => otherVideos.length > 0) + ) + } + + requestNewRecommendations (recommend: RecommendationInfo) { + this.requestsForLoad$$.next(recommend) + } +} -- cgit v1.2.3