-<div class="no-results">No videos in this playlist.</div>
+<div i18n class="no-results" *ngIf="pagination.totalItems === 0">No videos in this playlist.</div>
<div class="videos" myInfiniteScroller (nearOfBottom)="onNearOfBottom()">
<div *ngFor="let video of videos" class="video">
- <my-video-thumbnail [video]="video"></my-video-thumbnail>
+ <div class="position">{{ video.playlistElement.position }}</div>
- <div class="video-info">
- <div class="position">{{ video.playlistElement.position }}</div>
-
- <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
+ <my-video-thumbnail [video]="video" [nsfw]="isVideoBlur(video)"></my-video-thumbnail>
+ <div class="video-info">
<a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
<a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
+ <span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(video)}}</span>
+ </div>
+
+ <div class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom-right" (openChange)="onDropdownOpenChange()" autoClose="outside">
+ <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more"></my-global-icon>
+
+ <div ngbDropdownMenu>
+ <div class="dropdown-item" (click)="toggleDisplayTimestampsOptions($event, video)">
+ <my-global-icon iconName="edit"></my-global-icon> <ng-container i18n>Edit starts/stops at</ng-container>
+ </div>
+
+ <div class="timestamp-options" *ngIf="displayTimestampOptions">
+ <div>
+ <my-peertube-checkbox
+ inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
+ i18n-labelText labelText="Start at"
+ ></my-peertube-checkbox>
+
+ <my-timestamp-input
+ [timestamp]="timestampOptions.startTimestamp"
+ [maxTimestamp]="video.duration"
+ [disabled]="!timestampOptions.startTimestampEnabled"
+ [(ngModel)]="timestampOptions.startTimestamp"
+ ></my-timestamp-input>
+ </div>
+
+ <div>
+ <my-peertube-checkbox
+ inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled"
+ i18n-labelText labelText="Stop at"
+ ></my-peertube-checkbox>
+
+ <my-timestamp-input
+ [timestamp]="timestampOptions.stopTimestamp"
+ [maxTimestamp]="video.duration"
+ [disabled]="!timestampOptions.stopTimestampEnabled"
+ [(ngModel)]="timestampOptions.stopTimestamp"
+ ></my-timestamp-input>
+ </div>
+
+ <input type="submit" i18n-value value="Save" (click)="updateTimestamps(video)">
+ </div>
+
+ <span class="dropdown-item" (click)="removeFromPlaylist(video)">
+ <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete from {{playlist?.displayName}}</ng-container>
+ </span>
+ </div>
</div>
</div>
</div>
@import '_variables';
@import '_mixins';
+@import '_miniature';
+
+.videos {
+ .video {
+ display: flex;
+ align-items: center;
+ padding: 10px;
+ border-bottom: 1px solid $separator-border-color;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+
+ .more {
+ display: block;
+ }
+ }
+
+ .position {
+ font-weight: $font-semibold;
+ margin-right: 10px;
+ color: $grey-foreground-color;
+ }
+
+ my-video-thumbnail {
+ display: flex; // Avoids an issue with line-height that adds space below the element
+ margin-right: 10px;
+
+ /deep/ .video-thumbnail {
+ @include miniature-thumbnail(130px, 72px);
+ }
+ }
+
+ .video-info {
+ display: flex;
+ flex-direction: column;
+
+ a {
+ @include disable-default-a-behaviour;
+
+ color: var(--mainForegroundColor);
+ }
+
+ .video-info-name {
+ font-size: 18px;
+ font-weight: $font-semibold;
+ }
+
+ .video-info-account, .video-info-timestamp {
+ color: $grey-foreground-color;
+ }
+ }
+
+ .more {
+ justify-self: flex-end;
+ margin-left: auto;
+ cursor: pointer;
+ display: none;
+
+ &.show {
+ display: block;
+ }
+
+ .icon-more {
+ @include apply-svg-color($grey-foreground-color);
+
+ &::after {
+ border: none;
+ }
+ }
+
+ .dropdown-item {
+ @include dropdown-with-icon-item;
+ }
+
+ .timestamp-options {
+ padding-top: 0;
+ padding-left: 35px;
+ margin-bottom: 15px;
+
+ > div {
+ display: flex;
+ align-items: center;
+ }
+
+ input {
+ @include peertube-button;
+ @include orange-button;
+
+ margin-top: 10px;
+ }
+ }
+ }
+ }
+}
-import { Component, OnDestroy, OnInit } from '@angular/core'
-import { Notifier } from '@app/core'
+import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
+import { Notifier, ServerService } from '@app/core'
import { AuthService } from '../../core/auth'
import { ConfirmService } from '../../core/confirm'
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
import { Subscription } from 'rxjs'
import { ActivatedRoute } from '@angular/router'
import { VideoService } from '@app/shared/video/video.service'
+import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
+import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { secondsToTime } from '../../../assets/player/utils'
+import { VideoPlaylistElementUpdate } from '@shared/models'
+import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
@Component({
selector: 'my-account-video-playlist-elements',
styleUrls: [ './my-account-video-playlist-elements.component.scss' ]
})
export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy {
+ @ViewChild('moreDropdown') moreDropdown: NgbDropdown
+
videos: Video[] = []
+ playlist: VideoPlaylist
pagination: ComponentPagination = {
currentPage: 1,
totalItems: null
}
+ displayTimestampOptions = false
+
+ timestampOptions: {
+ startTimestampEnabled: boolean
+ startTimestamp: number
+ stopTimestampEnabled: boolean
+ stopTimestamp: number
+ } = {} as any
+
private videoPlaylistId: string | number
private paramsSub: Subscription
constructor (
private authService: AuthService,
+ private serverService: ServerService,
private notifier: Notifier,
private confirmService: ConfirmService,
private route: ActivatedRoute,
- private videoService: VideoService
+ private i18n: I18n,
+ private videoService: VideoService,
+ private videoPlaylistService: VideoPlaylistService
) {}
ngOnInit () {
this.paramsSub = this.route.params.subscribe(routeParams => {
this.videoPlaylistId = routeParams[ 'videoPlaylistId' ]
this.loadElements()
+
+ this.loadPlaylistInfo()
})
}
if (this.paramsSub) this.paramsSub.unsubscribe()
}
+ isVideoBlur (video: Video) {
+ return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
+ }
+
+ removeFromPlaylist (video: Video) {
+ this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, video.id)
+ .subscribe(
+ () => {
+ this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName }))
+
+ this.videos = this.videos.filter(v => v.id !== video.id)
+ },
+
+ err => this.notifier.error(err.message)
+ )
+
+ this.moreDropdown.close()
+ }
+
+ updateTimestamps (video: Video) {
+ const body: VideoPlaylistElementUpdate = {}
+
+ body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null
+ body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null
+
+ this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, video.id, body)
+ .subscribe(
+ () => {
+ this.notifier.success(this.i18n('Timestamps updated'))
+
+ video.playlistElement.startTimestamp = body.startTimestamp
+ video.playlistElement.stopTimestamp = body.stopTimestamp
+ },
+
+ err => this.notifier.error(err.message)
+ )
+
+ this.moreDropdown.close()
+ }
+
onNearOfBottom () {
// Last page
if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
this.loadElements()
}
+ formatTimestamp (video: Video) {
+ const start = video.playlistElement.startTimestamp
+ const stop = video.playlistElement.stopTimestamp
+
+ const startFormatted = secondsToTime(start, true, ':')
+ const stopFormatted = secondsToTime(stop, true, ':')
+
+ if (start === null && stop === null) return ''
+
+ if (start !== null && stop === null) return this.i18n('Starts at ') + startFormatted
+ if (start === null && stop !== null) return this.i18n('Stops at ') + stopFormatted
+
+ return this.i18n('Starts at ') + startFormatted + this.i18n(' and stops at ') + stopFormatted
+ }
+
+ onDropdownOpenChange () {
+ this.displayTimestampOptions = false
+ }
+
+ toggleDisplayTimestampsOptions (event: Event, video: Video) {
+ event.preventDefault()
+
+ this.displayTimestampOptions = !this.displayTimestampOptions
+
+ if (this.displayTimestampOptions === true) {
+ this.timestampOptions = {
+ startTimestampEnabled: false,
+ stopTimestampEnabled: false,
+ startTimestamp: 0,
+ stopTimestamp: video.duration
+ }
+
+ if (video.playlistElement.startTimestamp) {
+ this.timestampOptions.startTimestampEnabled = true
+ this.timestampOptions.startTimestamp = video.playlistElement.startTimestamp
+ }
+
+ if (video.playlistElement.stopTimestamp) {
+ this.timestampOptions.stopTimestampEnabled = true
+ this.timestampOptions.stopTimestamp = video.playlistElement.stopTimestamp
+ }
+ }
+ }
+
private loadElements () {
this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
.subscribe(({ totalVideos, videos }) => {
this.pagination.totalItems = totalVideos
})
}
+
+ private loadPlaylistInfo () {
+ this.videoPlaylistService.getVideoPlaylist(this.videoPlaylistId)
+ .subscribe(playlist => {
+ this.playlist = playlist
+ })
+ }
}
</div>
<div class="logged-in-more" ngbDropdown placement="bottom-right">
- <span class="glyphicon glyphicon-option-vertical" ngbDropdownToggle role="button"></span>
+ <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button"></my-global-icon>
<div ngbDropdownMenu>
<a *ngIf="user.account" i18n [routerLink]="[ '/accounts', user.account.nameWithHost ]" class="dropdown-item">
.logged-in-more {
margin-right: 20px;
- .glyphicon {
+ my-global-icon {
+ @include apply-svg-color(var(--mainBackgroundColor));
+
cursor: pointer;
- font-size: 18px;
&::after {
border: none;
class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }"
ngbDropdownToggle role="button"
>
- <my-global-icon *ngIf="!label" class="more-icon" iconName="more"></my-global-icon>
+ <my-global-icon *ngIf="!label" class="more-icon" iconName="more-horizontal"></my-global-icon>
<span *ngIf="label" class="dropdown-toggle">{{ label }}</span>
</div>
'dislike': require('../../../assets/images/video/dislike.html'),
'heart': require('../../../assets/images/video/heart.html'),
'like': require('../../../assets/images/video/like.html'),
- 'more': require('../../../assets/images/video/more.html'),
+ 'more-horizontal': require('../../../assets/images/global/more-horizontal.html'),
+ 'more-vertical': require('../../../assets/images/global/more-vertical.html'),
'share': require('../../../assets/images/video/share.html'),
'upload': require('../../../assets/images/video/upload.html'),
'playlist-add': require('../../../assets/images/video/playlist-add.html')
<div class="first-row">
<div i18n class="title">Save to</div>
- <div i18n class="options" (click)="displayOptions = !displayOptions">
+ <div class="options" (click)="displayOptions = !displayOptions">
<my-global-icon iconName="cog"></my-global-icon>
- Options
+ <span i18n>Options</span>
</div>
</div>
}
.options {
+ display: flex;
+ align-items: center;
font-size: 14px;
cursor: pointer;
@include apply-svg-color(#333);
width: 16px;
- height: 16px;
+ height: 23px;
+ margin-right: 3px;
}
}
}
padding: 0 10px;
display: flex;
align-items: center;
- font-size: 15px;
+ font-size: 14px;
+ font-weight: $font-semibold;
}
}
import { User } from '../'
-import { Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
+import { PlaylistElement, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model'
import { durationToString, getAbsoluteAPIUrl } from '../misc/utils'
blacklisted?: boolean
blacklistedReason?: string
+ playlistElement?: PlaylistElement
+
account: {
id: number
uuid: string
this.blacklistedReason = hash.blacklistedReason
this.userHistory = hash.userHistory
+
+ this.playlistElement = hash.playlistElement
}
isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
<div class="action-dropdown" ngbDropdown placement="top" role="button">
<div class="action-button" ngbDropdownToggle role="button">
- <my-global-icon class="more-icon" iconName="more"></my-global-icon>
+ <my-global-icon class="more-icon" iconName="more-horizontal"></my-global-icon>
</div>
<div ngbDropdownMenu>
display: inline-block;
.dropdown-menu .dropdown-item {
- padding: 6px 24px;
-
- my-global-icon {
- width: 24px;
-
- margin-right: 10px;
- position: relative;
- top: -2px;
- }
+ @include dropdown-with-icon-item;
}
}
}
--- /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 stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g transform="translate(-444.000000, -115.000000)" fill="#000000">
+ <g id="10" transform="translate(444.000000, 115.000000)">
+ <path d="M10,12 C10,10.8954305 10.8877296,10 12,10 C13.1045695,10 14,10.8877296 14,12 C14,13.1045695 13.1122704,14 12,14 C10.8954305,14 10,13.1122704 10,12 Z M17,12 C17,10.8954305 17.8877296,10 19,10 C20.1045695,10 21,10.8877296 21,12 C21,13.1045695 20.1122704,14 19,14 C17.8954305,14 17,13.1122704 17,12 Z M3,12 C3,10.8954305 3.88772964,10 5,10 C6.1045695,10 7,10.8877296 7,12 C7,13.1045695 6.11227036,14 5,14 C3.8954305,14 3,13.1122704 3,12 Z" id="Combined-Shape"></path>
+ </g>
+ </g>
+ </g>
+</svg>
--- /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(-268.000000, -1046.000000)" fill="#000000">
+ <g id="Extras" transform="translate(48.000000, 1046.000000)">
+ <g id="more-vertical" transform="translate(220.000000, 0.000000)">
+ <path d="M10,12 C10,10.8954305 10.8877296,10 12,10 C13.1045695,10 14,10.8877296 14,12 C14,13.1045695 13.1122704,14 12,14 C10.8954305,14 10,13.1122704 10,12 Z M10,5 C10,3.8954305 10.8877296,3 12,3 C13.1045695,3 14,3.88772964 14,5 C14,6.1045695 13.1122704,7 12,7 C10.8954305,7 10,6.11227036 10,5 Z M10,19 C10,17.8954305 10.8877296,17 12,17 C13.1045695,17 14,17.8877296 14,19 C14,20.1045695 13.1122704,21 12,21 C10.8954305,21 10,20.1122704 10,19 Z" id="Combined-Shape"/>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
+++ /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 stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <g transform="translate(-444.000000, -115.000000)" fill="#000000">
- <g id="10" transform="translate(444.000000, 115.000000)">
- <path d="M10,12 C10,10.8954305 10.8877296,10 12,10 C13.1045695,10 14,10.8877296 14,12 C14,13.1045695 13.1122704,14 12,14 C10.8954305,14 10,13.1122704 10,12 Z M17,12 C17,10.8954305 17.8877296,10 19,10 C20.1045695,10 21,10.8877296 21,12 C21,13.1045695 20.1122704,14 19,14 C17.8954305,14 17,13.1122704 17,12 Z M3,12 C3,10.8954305 3.88772964,10 5,10 C6.1045695,10 7,10.8877296 7,12 C7,13.1045695 6.11227036,14 5,14 C3.8954305,14 3,13.1122704 3,12 Z" id="Combined-Shape"></path>
- </g>
- </g>
- </g>
-</svg>
$play-overlay-height: 26px;
$play-overlay-width: 18px;
-@mixin miniature-thumbnail {
+@mixin miniature-thumbnail($width: $video-thumbnail-width, $height: $video-thumbnail-height) {
@include disable-outline;
display: inline-block;
position: relative;
border-radius: 3px;
overflow: hidden;
- width: $video-thumbnail-width;
- height: $video-thumbnail-height;
+ width: $width;
+ height: $height;
background-color: #ececec;
transition: filter $play-overlay-transition;
right: 0;
bottom: 0;
- width: $video-thumbnail-width;
- height: $video-thumbnail-height;
+ width: inherit;
+ height: inherit;
opacity: 0;
background-color: rgba(0, 0, 0, 0.7);
}
img {
- width: $video-thumbnail-width;
- height: $video-thumbnail-height;
+ width: inherit;
+ height: inherit;
&.blur-filter {
filter: blur(5px);
align-items: center;
}
}
+
+@mixin dropdown-with-icon-item {
+ padding: 6px 24px;
+
+ my-global-icon {
+ width: 24px;
+
+ margin-right: 10px;
+ position: relative;
+ top: -2px;
+ }
+}
tr {
&:hover {
background-color: var(--submenuColor) !important;
- }
- &:not(:hover) {
- .action-cell * {
- display: none !important;
+ .action-cell .dropdown-root {
+ display: block !important;
}
}
padding: 0 !important;
text-align: center;
+ .dropdown-root {
+ display: none !important;
+
+ &.show {
+ display: block !important;
+ }
+ }
+
my-edit-button + my-delete-button {
margin-left: 5px;
}
fps: number
}
+export interface PlaylistElement {
+ position: number
+ startTimestamp: number
+ stopTimestamp: number
+}
+
export interface Video {
id: number
uuid: string
currentTime: number
}
- playlistElement?: {
- position: number
- startTimestamp: number
- stopTimestamp: number
- }
+ playlistElement?: PlaylistElement
}
export interface VideoDetails extends Video {