diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-01-02 13:07:18 +0100 |
---|---|---|
committer | Rigel Kent <sendmemail@rigelk.eu> | 2020-01-02 14:50:14 +0100 |
commit | 29128b2f5ce00093ad81b4b72daae0e3444fd5a8 (patch) | |
tree | f1a90ead86c16892255e2c661da3eed5f302a260 /client | |
parent | cca1e13b96799377f19bcc95110fbf76ff741e20 (diff) | |
download | PeerTube-29128b2f5ce00093ad81b4b72daae0e3444fd5a8.tar.gz PeerTube-29128b2f5ce00093ad81b4b72daae0e3444fd5a8.tar.zst PeerTube-29128b2f5ce00093ad81b4b72daae0e3444fd5a8.zip |
Add miniature quick actions to add video to Watch later playlist
Diffstat (limited to 'client')
9 files changed, 148 insertions, 9 deletions
diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts index d371a923f..55a5a6dde 100644 --- a/client/src/app/core/auth/auth-user.model.ts +++ b/client/src/app/core/auth/auth-user.model.ts | |||
@@ -5,6 +5,7 @@ import { User as ServerUserModel } from '../../../../../shared/models/users/user | |||
5 | import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' | 5 | import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' |
6 | import { User } from '../../shared/users/user.model' | 6 | import { User } from '../../shared/users/user.model' |
7 | import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' | 7 | import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' |
8 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | ||
8 | 9 | ||
9 | export type TokenOptions = { | 10 | export type TokenOptions = { |
10 | accessToken: string | 11 | accessToken: string |
@@ -79,6 +80,7 @@ export class AuthUser extends User { | |||
79 | } | 80 | } |
80 | 81 | ||
81 | tokens: Tokens | 82 | tokens: Tokens |
83 | specialPlaylists: Partial<VideoPlaylist>[] | ||
82 | 84 | ||
83 | static load () { | 85 | static load () { |
84 | const usernameLocalStorage = peertubeLocalStorage.getItem(this.KEYS.USERNAME) | 86 | const usernameLocalStorage = peertubeLocalStorage.getItem(this.KEYS.USERNAME) |
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index d601cadf5..9ae008e39 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts | |||
@@ -4,7 +4,7 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' | |||
4 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
5 | import { Router } from '@angular/router' | 5 | import { Router } from '@angular/router' |
6 | import { Notifier } from '@app/core/notification/notifier.service' | 6 | import { Notifier } from '@app/core/notification/notifier.service' |
7 | import { OAuthClientLocal, User as UserServerModel, UserRefreshToken } from '../../../../../shared' | 7 | import { OAuthClientLocal, MyUser as UserServerModel, UserRefreshToken } from '../../../../../shared' |
8 | import { User } from '../../../../../shared/models/users' | 8 | import { User } from '../../../../../shared/models/users' |
9 | import { UserLogin } from '../../../../../shared/models/users/user-login.model' | 9 | import { UserLogin } from '../../../../../shared/models/users/user-login.model' |
10 | import { environment } from '../../../environments/environment' | 10 | import { environment } from '../../../environments/environment' |
diff --git a/client/src/app/login/login.component.ts b/client/src/app/login/login.component.ts index cf923492a..ffadc9aa4 100644 --- a/client/src/app/login/login.component.ts +++ b/client/src/app/login/login.component.ts | |||
@@ -28,13 +28,11 @@ export class LoginComponent extends FormReactive implements OnInit { | |||
28 | 28 | ||
29 | constructor ( | 29 | constructor ( |
30 | protected formValidatorService: FormValidatorService, | 30 | protected formValidatorService: FormValidatorService, |
31 | private router: Router, | ||
32 | private route: ActivatedRoute, | 31 | private route: ActivatedRoute, |
33 | private modalService: NgbModal, | 32 | private modalService: NgbModal, |
34 | private loginValidatorsService: LoginValidatorsService, | 33 | private loginValidatorsService: LoginValidatorsService, |
35 | private authService: AuthService, | 34 | private authService: AuthService, |
36 | private userService: UserService, | 35 | private userService: UserService, |
37 | private serverService: ServerService, | ||
38 | private redirectService: RedirectService, | 36 | private redirectService: RedirectService, |
39 | private notifier: Notifier, | 37 | private notifier: Notifier, |
40 | private i18n: I18n | 38 | private i18n: I18n |
diff --git a/client/src/app/shared/images/global-icon.component.ts b/client/src/app/shared/images/global-icon.component.ts index 8a4965926..17186cff4 100644 --- a/client/src/app/shared/images/global-icon.component.ts +++ b/client/src/app/shared/images/global-icon.component.ts | |||
@@ -10,6 +10,7 @@ const icons = { | |||
10 | 'sparkle': require('!!raw-loader?!../../../assets/images/global/sparkle.svg'), | 10 | 'sparkle': require('!!raw-loader?!../../../assets/images/global/sparkle.svg'), |
11 | 'alert': require('!!raw-loader?!../../../assets/images/global/alert.svg'), | 11 | 'alert': require('!!raw-loader?!../../../assets/images/global/alert.svg'), |
12 | 'cloud-error': require('!!raw-loader?!../../../assets/images/global/cloud-error.svg'), | 12 | 'cloud-error': require('!!raw-loader?!../../../assets/images/global/cloud-error.svg'), |
13 | 'clock': require('!!raw-loader?!../../../assets/images/global/clock.svg'), | ||
13 | 'user-add': require('!!raw-loader?!../../../assets/images/global/user-add.svg'), | 14 | 'user-add': require('!!raw-loader?!../../../assets/images/global/user-add.svg'), |
14 | 'no': require('!!raw-loader?!../../../assets/images/global/no.svg'), | 15 | 'no': require('!!raw-loader?!../../../assets/images/global/no.svg'), |
15 | 'cloud-download': require('!!raw-loader?!../../../assets/images/global/cloud-download.svg'), | 16 | 'cloud-download': require('!!raw-loader?!../../../assets/images/global/cloud-download.svg'), |
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 5f74dcd4c..fc3b77b2a 100644 --- a/client/src/app/shared/video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/video-playlist/video-playlist.service.ts | |||
@@ -30,6 +30,7 @@ export class VideoPlaylistService { | |||
30 | // Use a replay subject because we "next" a value before subscribing | 30 | // Use a replay subject because we "next" a value before subscribing |
31 | private videoExistsInPlaylistSubject: Subject<number> = new ReplaySubject(1) | 31 | private videoExistsInPlaylistSubject: Subject<number> = new ReplaySubject(1) |
32 | private readonly videoExistsInPlaylistObservable: Observable<VideoExistInPlaylist> | 32 | private readonly videoExistsInPlaylistObservable: Observable<VideoExistInPlaylist> |
33 | private cachedWatchLaterPlaylists: VideoPlaylist[] | ||
33 | 34 | ||
34 | constructor ( | 35 | constructor ( |
35 | private authHttp: HttpClient, | 36 | private authHttp: HttpClient, |
diff --git a/client/src/app/shared/video/video-thumbnail.component.html b/client/src/app/shared/video/video-thumbnail.component.html index b302ebd0f..df15698c0 100644 --- a/client/src/app/shared/video/video-thumbnail.component.html +++ b/client/src/app/shared/video/video-thumbnail.component.html | |||
@@ -1,9 +1,23 @@ | |||
1 | <a | 1 | <a |
2 | [routerLink]="getVideoRouterLink()" [queryParams]="queryParams" [attr.title]="video.name" | 2 | [routerLink]="getVideoRouterLink()" [queryParams]="queryParams" [attr.title]="video.name" |
3 | class="video-thumbnail" | 3 | class="video-thumbnail" |
4 | (mouseenter)="load()" | ||
4 | > | 5 | > |
5 | <img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" /> | 6 | <img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" /> |
6 | 7 | ||
8 | <div *ngIf="isUserLoggedIn()" class="video-thumbnail-actions-overlay"> | ||
9 | <ng-container *ngIf="addedToWatchLater !== true"> | ||
10 | <div class="video-thumbnail-watch-later-overlay" placement="left" [ngbTooltip]="addToWatchLaterText" container="body" (click)="addToWatchLater();$event.stopPropagation();false"> | ||
11 | <my-global-icon iconName="clock" [attr.aria-label]="addToWatchLaterText" role="button"></my-global-icon> | ||
12 | </div> | ||
13 | </ng-container> | ||
14 | <ng-container *ngIf="addedToWatchLater === true"> | ||
15 | <div class="video-thumbnail-watch-later-overlay" placement="left" [ngbTooltip]="addedToWatchLaterText" container="body" (click)="removeFromWatchLater();$event.stopPropagation();false"> | ||
16 | <my-global-icon iconName="tick" [attr.aria-label]="addedToWatchLaterText" role="button"></my-global-icon> | ||
17 | </div> | ||
18 | </ng-container> | ||
19 | </div> | ||
20 | |||
7 | <div class="video-thumbnail-duration-overlay">{{ video.durationLabel }}</div> | 21 | <div class="video-thumbnail-duration-overlay">{{ video.durationLabel }}</div> |
8 | 22 | ||
9 | <div class="play-overlay"> | 23 | <div class="play-overlay"> |
diff --git a/client/src/app/shared/video/video-thumbnail.component.scss b/client/src/app/shared/video/video-thumbnail.component.scss index e48629778..aac50fd1b 100644 --- a/client/src/app/shared/video/video-thumbnail.component.scss +++ b/client/src/app/shared/video/video-thumbnail.component.scss | |||
@@ -18,16 +18,50 @@ | |||
18 | } | 18 | } |
19 | } | 19 | } |
20 | 20 | ||
21 | .video-thumbnail-watch-later-overlay, | ||
21 | .video-thumbnail-duration-overlay { | 22 | .video-thumbnail-duration-overlay { |
22 | @include static-thumbnail-overlay; | 23 | @include static-thumbnail-overlay; |
23 | 24 | ||
24 | position: absolute; | ||
25 | right: 5px; | ||
26 | bottom: 5px; | ||
27 | padding: 0 5px; | ||
28 | border-radius: 3px; | 25 | border-radius: 3px; |
29 | font-size: 12px; | 26 | font-size: 12px; |
30 | font-weight: $font-bold; | 27 | font-weight: $font-bold; |
31 | z-index: 1; | 28 | z-index: 1; |
32 | } | 29 | } |
30 | |||
31 | .video-thumbnail-duration-overlay { | ||
32 | position: absolute; | ||
33 | padding: 0 5px; | ||
34 | right: 5px; | ||
35 | bottom: 5px; | ||
36 | } | ||
37 | |||
38 | &:hover { | ||
39 | .video-thumbnail-actions-overlay { | ||
40 | opacity: 1; | ||
41 | } | ||
42 | } | ||
43 | |||
44 | .video-thumbnail-actions-overlay { | ||
45 | position: absolute; | ||
46 | display: flex; | ||
47 | flex-direction: column; | ||
48 | right: 5px; | ||
49 | top: 5px; | ||
50 | opacity: 0; | ||
51 | |||
52 | div:not(:first-child) { | ||
53 | margin-top: 2px; | ||
54 | } | ||
55 | |||
56 | .video-thumbnail-watch-later-overlay { | ||
57 | padding: 3px; | ||
58 | |||
59 | my-global-icon { | ||
60 | width: 22px; | ||
61 | height: 22px; | ||
62 | |||
63 | @include apply-svg-color(#fff); | ||
64 | } | ||
65 | } | ||
66 | } | ||
33 | } | 67 | } |
diff --git a/client/src/app/shared/video/video-thumbnail.component.ts b/client/src/app/shared/video/video-thumbnail.component.ts index fe65ade94..0f605e425 100644 --- a/client/src/app/shared/video/video-thumbnail.component.ts +++ b/client/src/app/shared/video/video-thumbnail.component.ts | |||
@@ -1,6 +1,14 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input, OnInit, ChangeDetectorRef } from '@angular/core' |
2 | import { Video } from './video.model' | 2 | import { Video } from './video.model' |
3 | import { ScreenService } from '@app/shared/misc/screen.service' | 3 | import { ScreenService } from '@app/shared/misc/screen.service' |
4 | import { AuthService, ThemeService } from '@app/core' | ||
5 | import { VideoPlaylistService } from '../video-playlist/video-playlist.service' | ||
6 | import { VideoPlaylistType } from '@shared/models' | ||
7 | import { forkJoin } from 'rxjs' | ||
8 | import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' | ||
9 | import { VideoPlaylist } from '../video-playlist/video-playlist.model' | ||
10 | import { VideoPlaylistElementCreate } from '../../../../../shared' | ||
11 | import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model' | ||
4 | 12 | ||
5 | @Component({ | 13 | @Component({ |
6 | selector: 'my-video-thumbnail', | 14 | selector: 'my-video-thumbnail', |
@@ -13,7 +21,44 @@ export class VideoThumbnailComponent { | |||
13 | @Input() routerLink: any[] | 21 | @Input() routerLink: any[] |
14 | @Input() queryParams: any[] | 22 | @Input() queryParams: any[] |
15 | 23 | ||
16 | constructor (private screenService: ScreenService) { | 24 | addToWatchLaterText = 'Add to watch later' |
25 | addedToWatchLaterText = 'Added to watch later' | ||
26 | addedToWatchLater: boolean | ||
27 | |||
28 | watchLaterPlaylist: any | ||
29 | |||
30 | constructor ( | ||
31 | private screenService: ScreenService, | ||
32 | private authService: AuthService, | ||
33 | private videoPlaylistService: VideoPlaylistService, | ||
34 | private cd: ChangeDetectorRef | ||
35 | ) {} | ||
36 | |||
37 | load () { | ||
38 | if (this.addedToWatchLater !== undefined) return | ||
39 | |||
40 | this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id) | ||
41 | .subscribe( | ||
42 | existResult => { | ||
43 | for (const playlist of this.authService.getUser().specialPlaylists) { | ||
44 | const existingPlaylist = existResult[ this.video.id ].find(p => p.playlistId === playlist.id) | ||
45 | this.addedToWatchLater = !!existingPlaylist | ||
46 | |||
47 | if (existingPlaylist) { | ||
48 | this.watchLaterPlaylist = { | ||
49 | playlistId: existingPlaylist.playlistId, | ||
50 | playlistElementId: existingPlaylist.playlistElementId | ||
51 | } | ||
52 | } else { | ||
53 | this.watchLaterPlaylist = { | ||
54 | playlistId: playlist.id | ||
55 | } | ||
56 | } | ||
57 | |||
58 | this.cd.markForCheck() | ||
59 | } | ||
60 | } | ||
61 | ) | ||
17 | } | 62 | } |
18 | 63 | ||
19 | getImageUrl () { | 64 | getImageUrl () { |
@@ -39,4 +84,37 @@ export class VideoThumbnailComponent { | |||
39 | 84 | ||
40 | return [ '/videos/watch', this.video.uuid ] | 85 | return [ '/videos/watch', this.video.uuid ] |
41 | } | 86 | } |
87 | |||
88 | isUserLoggedIn () { | ||
89 | return this.authService.isLoggedIn() | ||
90 | } | ||
91 | |||
92 | addToWatchLater () { | ||
93 | if (this.addedToWatchLater === undefined) return | ||
94 | this.addedToWatchLater = true | ||
95 | |||
96 | this.videoPlaylistService.addVideoInPlaylist( | ||
97 | this.watchLaterPlaylist.playlistId, | ||
98 | { videoId: this.video.id } as VideoPlaylistElementCreate | ||
99 | ).subscribe( | ||
100 | res => { | ||
101 | this.addedToWatchLater = true | ||
102 | this.watchLaterPlaylist.playlistElementId = res.videoPlaylistElement.id | ||
103 | } | ||
104 | ) | ||
105 | } | ||
106 | |||
107 | removeFromWatchLater () { | ||
108 | if (this.addedToWatchLater === undefined) return | ||
109 | this.addedToWatchLater = false | ||
110 | |||
111 | this.videoPlaylistService.removeVideoFromPlaylist( | ||
112 | this.watchLaterPlaylist.playlistId, | ||
113 | this.watchLaterPlaylist.playlistElementId | ||
114 | ).subscribe( | ||
115 | _ => { | ||
116 | this.addedToWatchLater = false | ||
117 | } | ||
118 | ) | ||
119 | } | ||
42 | } | 120 | } |
diff --git a/client/src/assets/images/global/clock.svg b/client/src/assets/images/global/clock.svg new file mode 100644 index 000000000..f2d4f0397 --- /dev/null +++ b/client/src/assets/images/global/clock.svg | |||
@@ -0,0 +1,11 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g id="Artboard-4" transform="translate(-488.000000, -159.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="31" transform="translate(488.000000, 159.000000)"> | ||
5 | <path d="M12,21 C7.02943725,21 3,16.9705627 3,12 C3,7.02943725 7.02943725,3 12,3 C16.9705627,3 21,7.02943725 21,12 C21,16.9705627 16.9705627,21 12,21 Z" id="Base"/> | ||
6 | <path d="M12,12 L16,12" id="Path-18" stroke-linecap="round"/> | ||
7 | <path d="M12,12 L12,7" id="Path-40" stroke-linecap="round" stroke-linejoin="round"/> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> \ No newline at end of file | ||