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
}
tokens: Tokens
+ specialPlaylists: Partial<VideoPlaylist>[]
static load () {
const usernameLocalStorage = peertubeLocalStorage.getItem(this.KEYS.USERNAME)
import { Injectable } from '@angular/core'
import { Router } from '@angular/router'
import { Notifier } from '@app/core/notification/notifier.service'
-import { OAuthClientLocal, User as UserServerModel, UserRefreshToken } from '../../../../../shared'
+import { OAuthClientLocal, MyUser as UserServerModel, UserRefreshToken } from '../../../../../shared'
import { User } from '../../../../../shared/models/users'
import { UserLogin } from '../../../../../shared/models/users/user-login.model'
import { environment } from '../../../environments/environment'
constructor (
protected formValidatorService: FormValidatorService,
- private router: Router,
private route: ActivatedRoute,
private modalService: NgbModal,
private loginValidatorsService: LoginValidatorsService,
private authService: AuthService,
private userService: UserService,
- private serverService: ServerService,
private redirectService: RedirectService,
private notifier: Notifier,
private i18n: I18n
'sparkle': require('!!raw-loader?!../../../assets/images/global/sparkle.svg'),
'alert': require('!!raw-loader?!../../../assets/images/global/alert.svg'),
'cloud-error': require('!!raw-loader?!../../../assets/images/global/cloud-error.svg'),
+ 'clock': require('!!raw-loader?!../../../assets/images/global/clock.svg'),
'user-add': require('!!raw-loader?!../../../assets/images/global/user-add.svg'),
'no': require('!!raw-loader?!../../../assets/images/global/no.svg'),
'cloud-download': require('!!raw-loader?!../../../assets/images/global/cloud-download.svg'),
// 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,
<a
[routerLink]="getVideoRouterLink()" [queryParams]="queryParams" [attr.title]="video.name"
class="video-thumbnail"
+ (mouseenter)="load()"
>
<img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
+ <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">
+ <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">
+ <my-global-icon iconName="tick" [attr.aria-label]="addedToWatchLaterText" role="button"></my-global-icon>
+ </div>
+ </ng-container>
+ </div>
+
<div class="video-thumbnail-duration-overlay">{{ video.durationLabel }}</div>
<div class="play-overlay">
}
}
+ .video-thumbnail-watch-later-overlay,
.video-thumbnail-duration-overlay {
@include static-thumbnail-overlay;
- position: absolute;
- right: 5px;
- bottom: 5px;
- padding: 0 5px;
border-radius: 3px;
font-size: 12px;
font-weight: $font-bold;
z-index: 1;
}
+
+ .video-thumbnail-duration-overlay {
+ position: absolute;
+ padding: 0 5px;
+ right: 5px;
+ bottom: 5px;
+ }
+
+ &:hover {
+ .video-thumbnail-actions-overlay {
+ opacity: 1;
+ }
+ }
+
+ .video-thumbnail-actions-overlay {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ right: 5px;
+ top: 5px;
+ opacity: 0;
+
+ div:not(:first-child) {
+ margin-top: 2px;
+ }
+
+ .video-thumbnail-watch-later-overlay {
+ padding: 3px;
+
+ my-global-icon {
+ width: 22px;
+ height: 22px;
+
+ @include apply-svg-color(#fff);
+ }
+ }
+ }
}
-import { Component, Input } from '@angular/core'
+import { Component, Input, OnInit, ChangeDetectorRef } 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 { VideoPlaylistType } from '@shared/models'
+import { forkJoin } from 'rxjs'
+import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
+import { VideoPlaylist } from '../video-playlist/video-playlist.model'
+import { VideoPlaylistElementCreate } from '../../../../../shared'
+import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model'
@Component({
selector: 'my-video-thumbnail',
@Input() routerLink: any[]
@Input() queryParams: any[]
- constructor (private screenService: ScreenService) {
+ addToWatchLaterText = 'Add to watch later'
+ addedToWatchLaterText = 'Added to watch later'
+ addedToWatchLater: boolean
+
+ watchLaterPlaylist: any
+
+ constructor (
+ private screenService: ScreenService,
+ private authService: AuthService,
+ private videoPlaylistService: VideoPlaylistService,
+ private cd: ChangeDetectorRef
+ ) {}
+
+ load () {
+ if (this.addedToWatchLater !== undefined) 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()
+ }
+ }
+ )
}
getImageUrl () {
return [ '/videos/watch', this.video.uuid ]
}
+
+ isUserLoggedIn () {
+ return this.authService.isLoggedIn()
+ }
+
+ 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
+ }
+ )
+ }
}
--- /dev/null
+<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">
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Artboard-4" transform="translate(-488.000000, -159.000000)" stroke="#000000" stroke-width="2">
+ <g id="31" transform="translate(488.000000, 159.000000)">
+ <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"/>
+ <path d="M12,12 L16,12" id="Path-18" stroke-linecap="round"/>
+ <path d="M12,12 L12,7" id="Path-40" stroke-linecap="round" stroke-linejoin="round"/>
+ </g>
+ </g>
+ </g>
+</svg>
\ No newline at end of file
// We did not load channels in res.locals.user
const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
- return res.json(user.toFormattedJSON())
+ return res.json(user.toFormattedJSON({ me: true }))
}
async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) {
Table,
UpdatedAt
} from 'sequelize-typescript'
-import { hasUserRight, USER_ROLE_LABELS, UserRight, VideoPrivacy } from '../../../shared'
+import { hasUserRight, USER_ROLE_LABELS, UserRight, VideoPrivacy, MyUser } from '../../../shared'
import { User, UserRole } from '../../../shared/models/users'
import {
isNoInstanceConfigWarningModal,
import { OAuthTokenModel } from '../oauth/oauth-token'
import { getSort, throwIfNotValid } from '../utils'
import { VideoChannelModel } from '../video/video-channel'
+import { VideoPlaylistModel } from '../video/video-playlist'
import { AccountModel } from './account'
import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
import { values } from 'lodash'
} from '@server/typings/models'
enum ScopeNames {
- WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
+ WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL',
+ WITH_SPECIAL_PLAYLISTS = 'WITH_SPECIAL_PLAYLISTS'
}
@DefaultScope(() => ({
required: true
}
]
+ },
+ [ScopeNames.WITH_SPECIAL_PLAYLISTS]: {
+ attributes: {
+ include: [
+ [
+ literal('(select array(select "id" from "videoPlaylist" where "ownerAccountId" in (select id from public.account where "userId" = "UserModel"."id") and name LIKE \'Watch later\'))'),
+ 'specialPlaylists'
+ ]
+ ]
+ }
}
}))
@Table({
}
}
- return UserModel.scope(ScopeNames.WITH_VIDEO_CHANNEL).findOne(query)
+ return UserModel.scope([
+ ScopeNames.WITH_VIDEO_CHANNEL,
+ ScopeNames.WITH_SPECIAL_PLAYLISTS
+ ]).findOne(query)
}
static loadByEmail (email: string): Bluebird<MUserDefault> {
return comparePassword(password, this.password)
}
- toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
+ toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean, me?: boolean } = {}): User | MyUser {
const videoQuotaUsed = this.get('videoQuotaUsed')
const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
- const json: User = {
+ const json: User | MyUser = {
id: this.id,
username: this.username,
email: this.email,
})
}
+ if (parameters.me) {
+ Object.assign(json, {
+ specialPlaylists: (this.get('specialPlaylists') as Array<number>).map(p => ({ id: p }))
+ })
+ }
+
return json
}
import * as chai from 'chai'
import 'mocha'
-import { User, UserRole, Video } from '../../../../shared/index'
+import { User, UserRole, Video, MyUser } from '../../../../shared/index'
import {
blockUser,
cleanupTests,
it('Should be able to get user information', async function () {
const res1 = await getMyUserInformation(server.url, accessTokenUser)
- const userMe: User = res1.body
+ const userMe: User & MyUser = res1.body
const res2 = await getUserInformation(server.url, server.accessToken, userMe.id)
const userGet: User = res2.body
expect(userMe.adminFlags).to.be.undefined
expect(userGet.adminFlags).to.equal(UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST)
+
+ expect(userMe.specialPlaylists).to.have.lengthOf(1)
})
})
import { Account } from '../actors'
import { VideoChannel } from '../videos/channel/video-channel.model'
+import { VideoPlaylist } from '../videos/playlist/video-playlist.model'
import { UserRole } from './user-role'
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
import { UserNotificationSetting } from './user-notification-setting.model'
createdAt: Date
}
+
+export interface MyUser extends User {
+ specialPlaylists: Partial<VideoPlaylist>[]
+}