Move watch later logic in miniature

pull/2362/head
Chocobozzz 2020-01-03 15:01:17 +01:00
parent ac0868bcc0
commit b7819090de
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
8 changed files with 127 additions and 102 deletions

View File

@ -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<VideoPlaylist>[]
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<ServerUserModel>, hashTokens: TokenOptions) {
constructor (userHash: Partial<ServerMyUserModel>, 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

View File

@ -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<number> = new ReplaySubject(1)
private readonly videoExistsInPlaylistObservable: Observable<VideoExistInPlaylist>
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<VideoExistInPlaylist>(url, { params })
return this.authHttp.get<VideoExistInPlaylist>(url, { params, headers: { ignoreLoadingBar: '' } })
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
}

View File

@ -1,5 +1,8 @@
<div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow }" (mouseenter)="loadActions()">
<my-video-thumbnail #thumbnail [video]="video" [nsfw]="isVideoBlur"></my-video-thumbnail>
<my-video-thumbnail
[video]="video" [nsfw]="isVideoBlur"
[displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)"
></my-video-thumbnail>
<div class="video-bottom">
<div class="video-miniature-information">

View File

@ -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()
})
}
}

View File

@ -1,18 +1,18 @@
<a
[routerLink]="getVideoRouterLink()" [queryParams]="queryParams" [attr.title]="video.name"
class="video-thumbnail"
(mouseenter)="load()" (focus)="load()"
>
<img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
<img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" loading="lazy" />
<div *ngIf="isUserLoggedIn()" class="video-thumbnail-actions-overlay">
<ng-container *ngIf="addedToWatchLater !== true">
<div class="video-thumbnail-watch-later-overlay" placement="left" [ngbTooltip]="addToWatchLaterText" container="body" (click)="addToWatchLater();$event.stopPropagation();false">
<div *ngIf="displayWatchLaterPlaylist" class="video-thumbnail-actions-overlay">
<ng-container *ngIf="inWatchLaterPlaylist !== true">
<div class="video-thumbnail-watch-later-overlay" placement="left" [ngbTooltip]="addToWatchLaterText" container="body" (click)="onWatchLaterClick($event)">
<my-global-icon iconName="clock" [attr.aria-label]="addToWatchLaterText" role="button"></my-global-icon>
</div>
</ng-container>
<ng-container *ngIf="addedToWatchLater === true">
<div class="video-thumbnail-watch-later-overlay" placement="left" [ngbTooltip]="addedToWatchLaterText" container="body" (click)="removeFromWatchLater();$event.stopPropagation();false">
<ng-container *ngIf="inWatchLaterPlaylist === true">
<div class="video-thumbnail-watch-later-overlay" placement="left" [ngbTooltip]="addedToWatchLaterText" container="body" (click)="onWatchLaterClick($event)">
<my-global-icon iconName="tick" [attr.aria-label]="addedToWatchLaterText" role="button"></my-global-icon>
</div>
</ng-container>

View File

@ -35,13 +35,6 @@
bottom: 5px;
}
&:focus,
&:hover {
.video-thumbnail-actions-overlay {
opacity: 1;
}
}
.video-thumbnail-actions-overlay {
position: absolute;
display: flex;

View File

@ -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<boolean>()
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
}
}

View File

@ -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',