diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts index 55a5a6dde..0843743c9 100644 --- a/client/src/app/core/auth/auth-user.model.ts +++ b/client/src/app/core/auth/auth-user.model.ts @@ -1,11 +1,10 @@ import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' import { UserRight } from '../../../../../shared/models/users/user-right.enum' -import { User as ServerUserModel } from '../../../../../shared/models/users/user.model' +import { MyUser as ServerMyUserModel, MyUserSpecialPlaylist } from '../../../../../shared/models/users/user.model' // Do not use the barrel (dependency loop) import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' import { User } from '../../shared/users/user.model' import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' -import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' export type TokenOptions = { accessToken: string @@ -67,7 +66,7 @@ class Tokens { } } -export class AuthUser extends User { +export class AuthUser extends User implements ServerMyUserModel { private static KEYS = { ID: 'id', ROLE: 'role', @@ -80,7 +79,7 @@ export class AuthUser extends User { } tokens: Tokens - specialPlaylists: Partial[] + specialPlaylists: MyUserSpecialPlaylist[] static load () { const usernameLocalStorage = peertubeLocalStorage.getItem(this.KEYS.USERNAME) @@ -115,9 +114,11 @@ export class AuthUser extends User { Tokens.flush() } - constructor (userHash: Partial, hashTokens: TokenOptions) { + constructor (userHash: Partial, hashTokens: TokenOptions) { super(userHash) + this.tokens = new Tokens(hashTokens) + this.specialPlaylists = userHash.specialPlaylists } getAccessToken () { @@ -141,7 +142,7 @@ export class AuthUser extends User { return hasUserRight(this.role, right) } - canManage (user: ServerUserModel) { + canManage (user: ServerMyUserModel) { const myRole = this.role if (myRole === UserRole.ADMINISTRATOR) return true diff --git a/client/src/app/shared/video-playlist/video-playlist.service.ts b/client/src/app/shared/video-playlist/video-playlist.service.ts index fc3b77b2a..d78fdc09f 100644 --- a/client/src/app/shared/video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/video-playlist/video-playlist.service.ts @@ -1,4 +1,4 @@ -import { bufferTime, catchError, filter, first, map, share, switchMap } from 'rxjs/operators' +import { bufferTime, catchError, distinctUntilChanged, filter, first, map, share, switchMap } from 'rxjs/operators' import { Injectable } from '@angular/core' import { Observable, ReplaySubject, Subject } from 'rxjs' import { RestExtractor } from '../rest/rest-extractor.service' @@ -30,7 +30,6 @@ export class VideoPlaylistService { // Use a replay subject because we "next" a value before subscribing private videoExistsInPlaylistSubject: Subject = new ReplaySubject(1) private readonly videoExistsInPlaylistObservable: Observable - private cachedWatchLaterPlaylists: VideoPlaylist[] constructor ( private authHttp: HttpClient, @@ -39,6 +38,7 @@ export class VideoPlaylistService { private restService: RestService ) { this.videoExistsInPlaylistObservable = this.videoExistsInPlaylistSubject.pipe( + distinctUntilChanged(), bufferTime(500), filter(videoIds => videoIds.length !== 0), switchMap(videoIds => this.doVideosExistInPlaylist(videoIds)), @@ -224,7 +224,7 @@ export class VideoPlaylistService { let params = new HttpParams() params = this.restService.addObjectParams(params, { videoIds }) - return this.authHttp.get(url, { params }) + return this.authHttp.get(url, { params, headers: { ignoreLoadingBar: '' } }) .pipe(catchError(err => this.restExtractor.handleError(err))) } } diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html index c6fd570b7..036825e61 100644 --- a/client/src/app/shared/video/video-miniature.component.html +++ b/client/src/app/shared/video/video-miniature.component.html @@ -1,5 +1,8 @@
- +
diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts index ba65d33b6..a603f87e5 100644 --- a/client/src/app/shared/video/video-miniature.component.ts +++ b/client/src/app/shared/video/video-miniature.component.ts @@ -1,12 +1,24 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, LOCALE_ID, OnInit, Output, ViewChild } from '@angular/core' +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Inject, + Input, + LOCALE_ID, + OnInit, + Output +} from '@angular/core' import { User } from '../users' import { Video } from './video.model' -import { ServerService } from '@app/core' -import { ServerConfig, VideoPrivacy, VideoState } from '../../../../../shared' +import { AuthService, ServerService } from '@app/core' +import { ServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '../../../../../shared' import { I18n } from '@ngx-translate/i18n-polyfill' import { VideoActionsDisplayType } from '@app/shared/video/video-actions-dropdown.component' import { ScreenService } from '@app/shared/misc/screen.service' -import { VideoThumbnailComponent } from './video-thumbnail.component' +import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' +import { forkJoin } from 'rxjs' +import { first } from 'rxjs/operators' export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto' export type MiniatureDisplayOptions = { @@ -47,8 +59,6 @@ export class VideoMiniatureComponent implements OnInit { @Output() videoUnblacklisted = new EventEmitter() @Output() videoRemoved = new EventEmitter() - @ViewChild('thumbnail', { static: true }) thumbnail: VideoThumbnailComponent - videoActionsDisplayOptions: VideoActionsDisplayType = { playlist: true, download: false, @@ -60,14 +70,28 @@ export class VideoMiniatureComponent implements OnInit { showActions = false serverConfig: ServerConfig + addToWatchLaterText: string + addedToWatchLaterText: string + inWatchLaterPlaylist: boolean + + watchLaterPlaylist: { + id: number + playlistElementId?: number + } + private ownerDisplayTypeChosen: 'account' | 'videoChannel' constructor ( private screenService: ScreenService, private serverService: ServerService, private i18n: I18n, + private authService: AuthService, + private videoPlaylistService: VideoPlaylistService, + private cd: ChangeDetectorRef, @Inject(LOCALE_ID) private localeId: string - ) { } + ) { + + } get isVideoBlur () { return this.video.isVideoNSFWForUser(this.user, this.serverConfig) @@ -131,7 +155,8 @@ export class VideoMiniatureComponent implements OnInit { loadActions () { if (this.displayVideoActions) this.showActions = true - this.thumbnail.load() + + this.loadWatchLater() } onVideoBlacklisted () { @@ -146,6 +171,38 @@ export class VideoMiniatureComponent implements OnInit { this.videoRemoved.emit() } + isUserLoggedIn () { + return this.authService.isLoggedIn() + } + + onWatchLaterClick (currentState: boolean) { + if (currentState === true) this.removeFromWatchLater() + else this.addToWatchLater() + + this.inWatchLaterPlaylist = !currentState + } + + addToWatchLater () { + const body = { videoId: this.video.id } + + this.videoPlaylistService.addVideoInPlaylist(this.watchLaterPlaylist.id, body).subscribe( + res => { + this.watchLaterPlaylist.playlistElementId = res.videoPlaylistElement.id + } + ) + } + + removeFromWatchLater () { + this.videoPlaylistService.removeVideoFromPlaylist(this.watchLaterPlaylist.id, this.watchLaterPlaylist.playlistElementId) + .subscribe( + _ => { /* empty */ } + ) + } + + isWatchLaterPlaylistDisplayed () { + return this.inWatchLaterPlaylist !== undefined + } + private setUpBy () { if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') { this.ownerDisplayTypeChosen = this.ownerDisplayType @@ -163,4 +220,29 @@ export class VideoMiniatureComponent implements OnInit { this.ownerDisplayTypeChosen = 'videoChannel' } } + + private loadWatchLater () { + if (!this.isUserLoggedIn()) return + + forkJoin([ + this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id), + this.authService.userInformationLoaded.pipe(first()) + ]).subscribe( + ([ existResult ]) => { + const watchLaterPlaylist = this.authService.getUser().specialPlaylists.find(p => p.type === VideoPlaylistType.WATCH_LATER) + const existsInWatchLater = existResult[ this.video.id ].find(r => r.playlistId === watchLaterPlaylist.id) + this.inWatchLaterPlaylist = false + + this.watchLaterPlaylist = { + id: watchLaterPlaylist.id + } + + if (existsInWatchLater) { + this.inWatchLaterPlaylist = true + this.watchLaterPlaylist.playlistElementId = existsInWatchLater.playlistElementId + } + + this.cd.markForCheck() + }) + } } diff --git a/client/src/app/shared/video/video-thumbnail.component.html b/client/src/app/shared/video/video-thumbnail.component.html index 9679dfefb..c30a43557 100644 --- a/client/src/app/shared/video/video-thumbnail.component.html +++ b/client/src/app/shared/video/video-thumbnail.component.html @@ -1,18 +1,18 @@ - + -
- -
+
+ +
- -
+ + +
diff --git a/client/src/app/shared/video/video-thumbnail.component.scss b/client/src/app/shared/video/video-thumbnail.component.scss index ad221d6ed..573a64987 100644 --- a/client/src/app/shared/video/video-thumbnail.component.scss +++ b/client/src/app/shared/video/video-thumbnail.component.scss @@ -35,13 +35,6 @@ bottom: 5px; } - &:focus, - &:hover { - .video-thumbnail-actions-overlay { - opacity: 1; - } - } - .video-thumbnail-actions-overlay { position: absolute; display: flex; diff --git a/client/src/app/shared/video/video-thumbnail.component.ts b/client/src/app/shared/video/video-thumbnail.component.ts index 6f9292d52..2420ec715 100644 --- a/client/src/app/shared/video/video-thumbnail.component.ts +++ b/client/src/app/shared/video/video-thumbnail.component.ts @@ -1,9 +1,7 @@ -import { Component, Input, OnInit, ChangeDetectorRef } from '@angular/core' +import { Component, EventEmitter, Input, Output } from '@angular/core' import { Video } from './video.model' import { ScreenService } from '@app/shared/misc/screen.service' -import { AuthService, ThemeService } from '@app/core' -import { VideoPlaylistService } from '../video-playlist/video-playlist.service' -import { VideoPlaylistElementCreate } from '../../../../../shared' +import { I18n } from '@ngx-translate/i18n-polyfill' @Component({ selector: 'my-video-thumbnail', @@ -16,45 +14,20 @@ export class VideoThumbnailComponent { @Input() routerLink: any[] @Input() queryParams: any[] - addToWatchLaterText = 'Add to watch later' - addedToWatchLaterText = 'Added to watch later' - addedToWatchLater: boolean + @Input() displayWatchLaterPlaylist: boolean + @Input() inWatchLaterPlaylist: boolean - watchLaterPlaylist: any + @Output() watchLaterClick = new EventEmitter() + + addToWatchLaterText: string + addedToWatchLaterText: string constructor ( private screenService: ScreenService, - private authService: AuthService, - private videoPlaylistService: VideoPlaylistService, - private cd: ChangeDetectorRef - ) {} - - load () { - if (this.addedToWatchLater !== undefined) return - if (!this.isUserLoggedIn()) return - - this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id) - .subscribe( - existResult => { - for (const playlist of this.authService.getUser().specialPlaylists) { - const existingPlaylist = existResult[ this.video.id ].find(p => p.playlistId === playlist.id) - this.addedToWatchLater = !!existingPlaylist - - if (existingPlaylist) { - this.watchLaterPlaylist = { - playlistId: existingPlaylist.playlistId, - playlistElementId: existingPlaylist.playlistElementId - } - } else { - this.watchLaterPlaylist = { - playlistId: playlist.id - } - } - - this.cd.markForCheck() - } - } - ) + private i18n: I18n + ) { + this.addToWatchLaterText = this.i18n('Add to watch later') + this.addedToWatchLaterText = this.i18n('Remove from watch later') } getImageUrl () { @@ -81,36 +54,10 @@ export class VideoThumbnailComponent { return [ '/videos/watch', this.video.uuid ] } - isUserLoggedIn () { - return this.authService.isLoggedIn() - } + onWatchLaterClick (event: Event) { + this.watchLaterClick.emit(this.inWatchLaterPlaylist) - addToWatchLater () { - if (this.addedToWatchLater === undefined) return - this.addedToWatchLater = true - - this.videoPlaylistService.addVideoInPlaylist( - this.watchLaterPlaylist.playlistId, - { videoId: this.video.id } as VideoPlaylistElementCreate - ).subscribe( - res => { - this.addedToWatchLater = true - this.watchLaterPlaylist.playlistElementId = res.videoPlaylistElement.id - } - ) - } - - removeFromWatchLater () { - if (this.addedToWatchLater === undefined) return - this.addedToWatchLater = false - - this.videoPlaylistService.removeVideoFromPlaylist( - this.watchLaterPlaylist.playlistId, - this.watchLaterPlaylist.playlistElementId - ).subscribe( - _ => { - this.addedToWatchLater = false - } - ) + event.stopPropagation() + return false } } diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts index 28e10e562..aa3a85995 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts @@ -14,7 +14,6 @@ import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard. import { FormValidatorService, UserService } from '@app/shared' import { VideoCaptionService } from '@app/shared/video-caption' import { scrollToTop } from '@app/shared/misc/utils' -import { ServerConfig } from '@shared/models' @Component({ selector: 'my-video-upload',