Playlist support in watch page
This commit is contained in:
parent
15e9d5ca39
commit
e2f01c47e0
20 changed files with 668 additions and 294 deletions
|
@ -5,60 +5,7 @@
|
|||
cdkDropList (cdkDropListDropped)="drop($event)"
|
||||
>
|
||||
<div class="video" *ngFor="let video of videos" cdkDrag (cdkDragMoved)="onDragMove($event)">
|
||||
<div class="position">{{ video.playlistElement.position }}</div>
|
||||
|
||||
<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>
|
||||
<my-video-playlist-element-miniature [video]="video" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)">
|
||||
</my-video-playlist-element-miniature>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,100 +2,6 @@
|
|||
@import '_mixins';
|
||||
@import '_miniature';
|
||||
|
||||
.video, .cdk-drag-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--mainBackgroundColor);
|
||||
cursor: pointer;
|
||||
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;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Thanks Angular CDK <3 https://material.angular.io/cdk/drag-drop/examples
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { AuthService } from '../../core/auth'
|
||||
import { ConfirmService } from '../../core/confirm'
|
||||
|
@ -10,9 +10,6 @@ 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'
|
||||
import { CdkDragDrop, CdkDragMove } from '@angular/cdk/drag-drop'
|
||||
import { throttleTime } from 'rxjs/operators'
|
||||
|
||||
|
@ -22,8 +19,6 @@ import { throttleTime } from 'rxjs/operators'
|
|||
styleUrls: [ './my-account-video-playlist-elements.component.scss' ]
|
||||
})
|
||||
export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('moreDropdown') moreDropdown: NgbDropdown
|
||||
|
||||
videos: Video[] = []
|
||||
playlist: VideoPlaylist
|
||||
|
||||
|
@ -33,15 +28,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
|
|||
totalItems: null
|
||||
}
|
||||
|
||||
displayTimestampOptions = false
|
||||
|
||||
timestampOptions: {
|
||||
startTimestampEnabled: boolean
|
||||
startTimestamp: number
|
||||
stopTimestampEnabled: boolean
|
||||
stopTimestamp: number
|
||||
} = {} as any
|
||||
|
||||
private videoPlaylistId: string | number
|
||||
private paramsSub: Subscription
|
||||
private dragMoveSubject = new Subject<number>()
|
||||
|
@ -124,45 +110,9 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
|
|||
// }
|
||||
}
|
||||
|
||||
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 }))
|
||||
|
||||
onElementRemoved (video: Video) {
|
||||
this.videos = this.videos.filter(v => v.id !== video.id)
|
||||
this.reorderClientPositions()
|
||||
},
|
||||
|
||||
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 () {
|
||||
|
@ -173,50 +123,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
|
|||
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 }) => {
|
||||
|
|
|
@ -27,7 +27,8 @@ const icons = {
|
|||
'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')
|
||||
'playlist-add': require('../../../assets/images/video/playlist-add.html'),
|
||||
'play': require('../../../assets/images/global/play.html')
|
||||
}
|
||||
|
||||
export type GlobalIconName = keyof typeof icons
|
||||
|
|
|
@ -77,6 +77,7 @@ import { GlobalIconComponent } from '@app/shared/images/global-icon.component'
|
|||
import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component'
|
||||
import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component'
|
||||
import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.component'
|
||||
import { VideoPlaylistElementMiniatureComponent } from '@app/shared/video-playlist/video-playlist-element-miniature.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -105,6 +106,7 @@ import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.compo
|
|||
VideoMiniatureComponent,
|
||||
VideoPlaylistMiniatureComponent,
|
||||
VideoAddToPlaylistComponent,
|
||||
VideoPlaylistElementMiniatureComponent,
|
||||
|
||||
FeedComponent,
|
||||
|
||||
|
@ -163,6 +165,7 @@ import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.compo
|
|||
VideoMiniatureComponent,
|
||||
VideoPlaylistMiniatureComponent,
|
||||
VideoAddToPlaylistComponent,
|
||||
VideoPlaylistElementMiniatureComponent,
|
||||
|
||||
FeedComponent,
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
<div class="video" [ngClass]="{ playing: playing }">
|
||||
<a [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()">
|
||||
<div class="position">
|
||||
<my-global-icon *ngIf="playing" iconName="play"></my-global-icon>
|
||||
<ng-container *ngIf="!playing">{{ video.playlistElement.position }}</ng-container>
|
||||
</div>
|
||||
|
||||
<my-video-thumbnail
|
||||
[video]="video" [nsfw]="isVideoBlur(video)"
|
||||
[routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
|
||||
></my-video-thumbnail>
|
||||
|
||||
<div class="video-info">
|
||||
<a tabindex="-1" class="video-info-name"
|
||||
[routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
|
||||
[attr.title]="video.name"
|
||||
>{{ video.name }}</a>
|
||||
|
||||
<a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
|
||||
<span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ video.byAccount }}</span>
|
||||
|
||||
<span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(video)}}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div *ngIf="owned" class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom-right" (openChange)="onDropdownOpenChange()"
|
||||
autoClose="outside">
|
||||
<my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></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>
|
|
@ -0,0 +1,124 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
@import '_miniature';
|
||||
|
||||
.video {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--mainBackgroundColor);
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid $separator-border-color;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
|
||||
.more {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.playing {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
a {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
flex-grow: 1;
|
||||
|
||||
.position {
|
||||
font-weight: $font-semibold;
|
||||
margin-right: 10px;
|
||||
color: $grey-foreground-color;
|
||||
min-width: 20px;
|
||||
|
||||
my-global-icon {
|
||||
@include apply-svg-color($grey-foreground-color);
|
||||
|
||||
width: 17px;
|
||||
position: relative;
|
||||
left: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
my-video-thumbnail {
|
||||
@include thumbnail-size-component(130px, 72px);
|
||||
|
||||
display: flex; // Avoids an issue with line-height that adds space below the element
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.video-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
|
||||
a {
|
||||
color: var(--mainForegroundColor);
|
||||
width: fit-content;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
}
|
||||
|
||||
.video-info-name {
|
||||
font-size: 18px;
|
||||
font-weight: $font-semibold;
|
||||
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.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);
|
||||
|
||||
display: flex;
|
||||
|
||||
&::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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
|
||||
import { Video } from '@app/shared/video/video.model'
|
||||
import { VideoPlaylistElementUpdate } from '@shared/models'
|
||||
import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { VideoService } from '@app/shared/video/video.service'
|
||||
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
||||
import { secondsToTime } from '../../../assets/player/utils'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-playlist-element-miniature',
|
||||
styleUrls: [ './video-playlist-element-miniature.component.scss' ],
|
||||
templateUrl: './video-playlist-element-miniature.component.html'
|
||||
})
|
||||
export class VideoPlaylistElementMiniatureComponent {
|
||||
@ViewChild('moreDropdown') moreDropdown: NgbDropdown
|
||||
|
||||
@Input() playlist: VideoPlaylist
|
||||
@Input() video: Video
|
||||
@Input() owned = false
|
||||
@Input() playing = false
|
||||
@Input() rowLink = false
|
||||
@Input() accountLink = true
|
||||
|
||||
@Output() elementRemoved = new EventEmitter<Video>()
|
||||
|
||||
displayTimestampOptions = false
|
||||
|
||||
timestampOptions: {
|
||||
startTimestampEnabled: boolean
|
||||
startTimestamp: number
|
||||
stopTimestampEnabled: boolean
|
||||
stopTimestamp: number
|
||||
} = {} as any
|
||||
|
||||
constructor (
|
||||
private authService: AuthService,
|
||||
private serverService: ServerService,
|
||||
private notifier: Notifier,
|
||||
private confirmService: ConfirmService,
|
||||
private route: ActivatedRoute,
|
||||
private i18n: I18n,
|
||||
private videoService: VideoService,
|
||||
private videoPlaylistService: VideoPlaylistService
|
||||
) {}
|
||||
|
||||
buildRouterLink () {
|
||||
if (!this.playlist) return null
|
||||
|
||||
return [ '/videos/watch/playlist', this.playlist.uuid ]
|
||||
}
|
||||
|
||||
buildRouterQuery () {
|
||||
if (!this.video) return {}
|
||||
|
||||
return {
|
||||
videoId: this.video.uuid,
|
||||
start: this.video.playlistElement.startTimestamp,
|
||||
stop: this.video.playlistElement.stopTimestamp
|
||||
}
|
||||
}
|
||||
|
||||
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.elementRemoved.emit(this.video)
|
||||
},
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
|
|||
@Input() firstLoadedPage = 1
|
||||
@Input() percentLimit = 70
|
||||
@Input() autoInit = false
|
||||
@Input() container = document.body
|
||||
|
||||
@Output() nearOfBottom = new EventEmitter<void>()
|
||||
@Output() nearOfTop = new EventEmitter<void>()
|
||||
|
@ -48,7 +49,7 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
|
|||
.pipe(
|
||||
startWith(null),
|
||||
throttleTime(200, undefined, throttleOptions),
|
||||
map(() => ({ current: window.scrollY, maximumScroll: document.body.clientHeight - window.innerHeight })),
|
||||
map(() => ({ current: window.scrollY, maximumScroll: this.container.clientHeight - window.innerHeight })),
|
||||
distinctUntilChanged((o1, o2) => o1.current === o2.current),
|
||||
share()
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<a
|
||||
[routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name"
|
||||
[routerLink]="getVideoRouterLink()" [queryParams]="queryParams" [attr.title]="video.name"
|
||||
class="video-thumbnail"
|
||||
>
|
||||
<img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
|
||||
|
|
|
@ -10,8 +10,11 @@ import { ScreenService } from '@app/shared/misc/screen.service'
|
|||
export class VideoThumbnailComponent {
|
||||
@Input() video: Video
|
||||
@Input() nsfw = false
|
||||
@Input() routerLink: any[]
|
||||
@Input() queryParams: any[]
|
||||
|
||||
constructor (private screenService: ScreenService) {}
|
||||
constructor (private screenService: ScreenService) {
|
||||
}
|
||||
|
||||
getImageUrl () {
|
||||
if (!this.video) return ''
|
||||
|
@ -30,4 +33,10 @@ export class VideoThumbnailComponent {
|
|||
|
||||
return (currentTime / this.video.duration) * 100
|
||||
}
|
||||
|
||||
getVideoRouterLink () {
|
||||
if (this.routerLink) return this.routerLink
|
||||
|
||||
return [ '/videos/watch', this.video.uuid ]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,16 +7,16 @@ import { VideoWatchComponent } from './video-watch.component'
|
|||
|
||||
const videoWatchRoutes: Routes = [
|
||||
{
|
||||
path: 'playlist/:uuid',
|
||||
path: 'playlist/:playlistId',
|
||||
component: VideoWatchComponent,
|
||||
canActivate: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: ':uuid/comments/:commentId',
|
||||
redirectTo: ':uuid'
|
||||
path: ':videoId/comments/:commentId',
|
||||
redirectTo: ':videoId'
|
||||
},
|
||||
{
|
||||
path: ':uuid',
|
||||
path: ':videoId',
|
||||
component: VideoWatchComponent,
|
||||
canActivate: [ MetaGuard ]
|
||||
}
|
||||
|
|
|
@ -1,11 +1,39 @@
|
|||
<div class="root-row row">
|
||||
<!-- We need the video container for videojs so we just hide it -->
|
||||
<div id="video-element-wrapper">
|
||||
<div id="video-wrapper">
|
||||
<div *ngIf="remoteServerDown" class="remote-server-down">
|
||||
Sorry, but this video is not available because the remote instance is not responding.
|
||||
<br />
|
||||
Please try again later.
|
||||
</div>
|
||||
|
||||
<div id="videojs-wrapper"></div>
|
||||
|
||||
<div *ngIf="playlist && video" class="playlist">
|
||||
<div class="playlist-info">
|
||||
<div class="playlist-display-name">
|
||||
{{ playlist.displayName }}
|
||||
|
||||
<span *ngIf="isUnlistedPlaylist()" class="badge badge-warning" i18n>Unlisted</span>
|
||||
<span *ngIf="isPrivatePlaylist()" class="badge badge-danger" i18n>Private</span>
|
||||
<span *ngIf="isPublicPlaylist()" class="badge badge-info" i18n>Public</span>
|
||||
</div>
|
||||
|
||||
<div class="playlist-by-index">
|
||||
<div class="playlist-by">{{ playlist.ownerBy }}</div>
|
||||
<div class="playlist-index">
|
||||
<span>{{currentPlaylistPosition}}</span><span>{{playlistPagination.totalItems}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngFor="let playlistVideo of playlistVideos" myInfiniteScroller [autoInit]="true" #elem [container]="elem" (nearOfBottom)="onPlaylistVideosNearOfBottom()">
|
||||
<my-video-playlist-element-miniature
|
||||
[video]="playlistVideo" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)"
|
||||
[playing]="currentPlaylistPosition === playlistVideo.playlistElement.position" [accountLink]="false"
|
||||
></my-video-playlist-element-miniature>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div i18n class="alert alert-warning" *ngIf="isVideoToImport()">
|
||||
|
@ -20,6 +48,10 @@
|
|||
This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}.
|
||||
</div>
|
||||
|
||||
<div i18n class="alert alert-info" *ngIf="noPlaylistVideos">
|
||||
This playlist does not have videos.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" *ngIf="video?.blacklisted">
|
||||
<div class="blacklisted-label" i18n>This video is blacklisted.</div>
|
||||
{{ video.blacklistedReason }}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
@import '_bootstrap-variables';
|
||||
@import '_miniature';
|
||||
|
||||
$other-videos-width: 260px;
|
||||
|
||||
|
@ -12,7 +13,7 @@ $other-videos-width: 260px;
|
|||
font-weight: $font-semibold;
|
||||
}
|
||||
|
||||
#video-element-wrapper {
|
||||
#video-wrapper {
|
||||
background-color: #000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -39,6 +40,57 @@ $other-videos-width: 260px;
|
|||
}
|
||||
}
|
||||
|
||||
.playlist {
|
||||
width: 400px;
|
||||
height: 66vh;
|
||||
background-color: #e4e4e4;
|
||||
overflow-y: auto;
|
||||
|
||||
.playlist-info {
|
||||
padding: 5px 30px;
|
||||
|
||||
.playlist-display-name {
|
||||
font-size: 18px;
|
||||
font-weight: $font-semibold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.playlist-by-index {
|
||||
color: $grey-foreground-color;
|
||||
display: flex;
|
||||
|
||||
.playlist-by {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.playlist-index span:first-child::after {
|
||||
content: '/';
|
||||
margin: 0 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
my-video-playlist-element-miniature {
|
||||
/deep/ {
|
||||
.video {
|
||||
.position {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.video-info {
|
||||
.video-info-name {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
my-video-thumbnail {
|
||||
@include thumbnail-size-component(90px, 50px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .video-js {
|
||||
width: calc(66vh * 1.77);
|
||||
height: 66vh;
|
||||
|
|
|
@ -8,7 +8,7 @@ import { MetaService } from '@ngx-meta/core'
|
|||
import { Notifier, ServerService } from '@app/core'
|
||||
import { forkJoin, Subscription } from 'rxjs'
|
||||
import { Hotkey, HotkeysService } from 'angular2-hotkeys'
|
||||
import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
|
||||
import { UserVideoRateType, VideoCaption, VideoPlaylistPrivacy, VideoPrivacy, VideoState } from '../../../../../shared'
|
||||
import { AuthService, ConfirmService } from '../../core'
|
||||
import { RestExtractor, VideoBlacklistService } from '../../shared'
|
||||
import { VideoDetails } from '../../shared/video/video-details.model'
|
||||
|
@ -28,6 +28,10 @@ import {
|
|||
PeertubePlayerManagerOptions,
|
||||
PlayerMode
|
||||
} from '../../../assets/player/peertube-player-manager'
|
||||
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
||||
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||
import { Video } from '@app/shared/video/video.model'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-watch',
|
||||
|
@ -50,6 +54,16 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
video: VideoDetails = null
|
||||
descriptionLoading = false
|
||||
|
||||
playlist: VideoPlaylist = null
|
||||
playlistVideos: Video[] = []
|
||||
playlistPagination: ComponentPagination = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 10,
|
||||
totalItems: null
|
||||
}
|
||||
noPlaylistVideos = false
|
||||
currentPlaylistPosition = 1
|
||||
|
||||
completeDescriptionShown = false
|
||||
completeVideoDescription: string
|
||||
shortVideoDescription: string
|
||||
|
@ -61,6 +75,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
|
||||
private currentTime: number
|
||||
private paramsSub: Subscription
|
||||
private queryParamsSub: Subscription
|
||||
|
||||
constructor (
|
||||
private elementRef: ElementRef,
|
||||
|
@ -68,6 +83,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private videoService: VideoService,
|
||||
private playlistService: VideoPlaylistService,
|
||||
private videoBlacklistService: VideoBlacklistService,
|
||||
private confirmService: ConfirmService,
|
||||
private metaService: MetaService,
|
||||
|
@ -97,31 +113,16 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.paramsSub = this.route.params.subscribe(routeParams => {
|
||||
const uuid = routeParams[ 'uuid' ]
|
||||
const videoId = routeParams[ 'videoId' ]
|
||||
if (videoId) this.loadVideo(videoId)
|
||||
|
||||
// Video did not change
|
||||
if (this.video && this.video.uuid === uuid) return
|
||||
|
||||
if (this.player) this.player.pause()
|
||||
|
||||
// Video did change
|
||||
forkJoin(
|
||||
this.videoService.getVideo(uuid),
|
||||
this.videoCaptionService.listCaptions(uuid)
|
||||
)
|
||||
.pipe(
|
||||
// If 401, the video is private or blacklisted so redirect to 404
|
||||
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
|
||||
)
|
||||
.subscribe(([ video, captionsResult ]) => {
|
||||
const startTime = this.route.snapshot.queryParams.start
|
||||
const stopTime = this.route.snapshot.queryParams.stop
|
||||
const subtitle = this.route.snapshot.queryParams.subtitle
|
||||
const playerMode = this.route.snapshot.queryParams.mode
|
||||
|
||||
this.onVideoFetched(video, captionsResult.data, { startTime, stopTime, subtitle, playerMode })
|
||||
.catch(err => this.handleError(err))
|
||||
const playlistId = routeParams[ 'playlistId' ]
|
||||
if (playlistId) this.loadPlaylist(playlistId)
|
||||
})
|
||||
|
||||
this.queryParamsSub = this.route.queryParams.subscribe(queryParams => {
|
||||
const videoId = queryParams[ 'videoId' ]
|
||||
if (videoId) this.loadVideo(videoId)
|
||||
})
|
||||
|
||||
this.hotkeys = [
|
||||
|
@ -147,7 +148,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.flushPlayer()
|
||||
|
||||
// Unsubscribe subscriptions
|
||||
this.paramsSub.unsubscribe()
|
||||
if (this.paramsSub) this.paramsSub.unsubscribe()
|
||||
if (this.queryParamsSub) this.queryParamsSub.unsubscribe()
|
||||
|
||||
// Unbind hotkeys
|
||||
if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys)
|
||||
|
@ -219,8 +221,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
showShareModal () {
|
||||
const currentTime = this.player ? this.player.currentTime() : undefined
|
||||
|
||||
this.videoShareModal.show(this.currentTime)
|
||||
}
|
||||
|
||||
|
@ -322,6 +322,107 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
return this.video && this.video.scheduledUpdate !== undefined
|
||||
}
|
||||
|
||||
isVideoBlur (video: Video) {
|
||||
return video.isVideoNSFWForUser(this.user, this.serverService.getConfig())
|
||||
}
|
||||
|
||||
isPlaylistOwned () {
|
||||
return this.playlist.isLocal === true && this.playlist.ownerAccount.name === this.user.username
|
||||
}
|
||||
|
||||
isUnlistedPlaylist () {
|
||||
return this.playlist.privacy.id === VideoPlaylistPrivacy.UNLISTED
|
||||
}
|
||||
|
||||
isPrivatePlaylist () {
|
||||
return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE
|
||||
}
|
||||
|
||||
isPublicPlaylist () {
|
||||
return this.playlist.privacy.id === VideoPlaylistPrivacy.PUBLIC
|
||||
}
|
||||
|
||||
onPlaylistVideosNearOfBottom () {
|
||||
// Last page
|
||||
if (this.playlistPagination.totalItems <= (this.playlistPagination.currentPage * this.playlistPagination.itemsPerPage)) return
|
||||
|
||||
this.playlistPagination.currentPage += 1
|
||||
this.loadPlaylistElements(false)
|
||||
}
|
||||
|
||||
onElementRemoved (video: Video) {
|
||||
this.playlistVideos = this.playlistVideos.filter(v => v.id !== video.id)
|
||||
|
||||
this.playlistPagination.totalItems--
|
||||
}
|
||||
|
||||
private loadVideo (videoId: string) {
|
||||
// Video did not change
|
||||
if (this.video && this.video.uuid === videoId) return
|
||||
|
||||
if (this.player) this.player.pause()
|
||||
|
||||
// Video did change
|
||||
forkJoin(
|
||||
this.videoService.getVideo(videoId),
|
||||
this.videoCaptionService.listCaptions(videoId)
|
||||
)
|
||||
.pipe(
|
||||
// If 401, the video is private or blacklisted so redirect to 404
|
||||
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
|
||||
)
|
||||
.subscribe(([ video, captionsResult ]) => {
|
||||
const queryParams = this.route.snapshot.queryParams
|
||||
const startTime = queryParams.start
|
||||
const stopTime = queryParams.stop
|
||||
const subtitle = queryParams.subtitle
|
||||
const playerMode = queryParams.mode
|
||||
|
||||
this.onVideoFetched(video, captionsResult.data, { startTime, stopTime, subtitle, playerMode })
|
||||
.catch(err => this.handleError(err))
|
||||
})
|
||||
}
|
||||
|
||||
private loadPlaylist (playlistId: string) {
|
||||
// Playlist did not change
|
||||
if (this.playlist && this.playlist.uuid === playlistId) return
|
||||
|
||||
this.playlistService.getVideoPlaylist(playlistId)
|
||||
.pipe(
|
||||
// If 401, the video is private or blacklisted so redirect to 404
|
||||
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
|
||||
)
|
||||
.subscribe(playlist => {
|
||||
this.playlist = playlist
|
||||
|
||||
const videoId = this.route.snapshot.queryParams['videoId']
|
||||
this.loadPlaylistElements(!videoId)
|
||||
})
|
||||
}
|
||||
|
||||
private loadPlaylistElements (redirectToFirst = false) {
|
||||
this.videoService.getPlaylistVideos(this.playlist.id, this.playlistPagination)
|
||||
.subscribe(({ totalVideos, videos }) => {
|
||||
this.playlistVideos = this.playlistVideos.concat(videos)
|
||||
this.playlistPagination.totalItems = totalVideos
|
||||
|
||||
if (totalVideos === 0) {
|
||||
this.noPlaylistVideos = true
|
||||
return
|
||||
}
|
||||
|
||||
this.updatePlaylistIndex()
|
||||
|
||||
if (redirectToFirst) {
|
||||
const extras = {
|
||||
queryParams: { videoId: this.playlistVideos[ 0 ].uuid },
|
||||
replaceUrl: true
|
||||
}
|
||||
this.router.navigate([], extras)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private updateVideoDescription (description: string) {
|
||||
this.video.description = description
|
||||
this.setVideoDescriptionHTML()
|
||||
|
@ -383,11 +484,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.remoteServerDown = false
|
||||
this.currentTime = undefined
|
||||
|
||||
this.updatePlaylistIndex()
|
||||
|
||||
let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0)
|
||||
// If we are at the end of the video, reset the timer
|
||||
if (this.video.duration - startTime <= 1) startTime = 0
|
||||
|
||||
if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) {
|
||||
if (this.isVideoBlur(this.video)) {
|
||||
const res = await this.confirmService.confirm(
|
||||
this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'),
|
||||
this.i18n('Mature or explicit content')
|
||||
|
@ -399,7 +502,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.flushPlayer()
|
||||
|
||||
// Build video element, because videojs remove it on dispose
|
||||
const playerElementWrapper = this.elementRef.nativeElement.querySelector('#video-element-wrapper')
|
||||
const playerElementWrapper = this.elementRef.nativeElement.querySelector('#videojs-wrapper')
|
||||
this.playerElement = document.createElement('video')
|
||||
this.playerElement.className = 'video-js vjs-peertube-skin'
|
||||
this.playerElement.setAttribute('playsinline', 'true')
|
||||
|
@ -474,6 +577,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.player.on('timeupdate', () => {
|
||||
this.currentTime = Math.floor(this.player.currentTime())
|
||||
})
|
||||
|
||||
this.player.one('ended', () => {
|
||||
if (this.playlist) {
|
||||
this.zone.run(() => this.navigateToNextPlaylistVideo())
|
||||
}
|
||||
})
|
||||
|
||||
this.player.one('stopped', () => {
|
||||
if (this.playlist) {
|
||||
this.zone.run(() => this.navigateToNextPlaylistVideo())
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.setVideoDescriptionHTML()
|
||||
|
@ -528,6 +643,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.setVideoLikesBarTooltipText()
|
||||
}
|
||||
|
||||
private updatePlaylistIndex () {
|
||||
if (this.playlistVideos.length === 0 || !this.video) return
|
||||
|
||||
for (const video of this.playlistVideos) {
|
||||
if (video.id === this.video.id) {
|
||||
this.currentPlaylistPosition = video.playlistElement.position
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Load more videos to find our video
|
||||
this.onPlaylistVideosNearOfBottom()
|
||||
}
|
||||
|
||||
private setOpenGraphTags () {
|
||||
this.metaService.setTitle(this.video.name)
|
||||
|
||||
|
@ -567,4 +696,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.player = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private navigateToNextPlaylistVideo () {
|
||||
if (this.currentPlaylistPosition < this.playlistPagination.totalItems) {
|
||||
const next = this.playlistVideos.find(v => v.playlistElement.position === this.currentPlaylistPosition + 1)
|
||||
|
||||
const start = next.playlistElement.startTimestamp
|
||||
const stop = next.playlistElement.stopTimestamp
|
||||
this.router.navigate([],{ queryParams: { videoId: next.uuid, start, stop } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
9
client/src/assets/images/global/play.html
Normal file
9
client/src/assets/images/global/play.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<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" stroke-linejoin="round">
|
||||
<g id="Artboard-4" transform="translate(-532.000000, -115.000000)" stroke-width="2" stroke="#000000">
|
||||
<g id="12" transform="translate(532.000000, 115.000000)">
|
||||
<polygon id="Triangle-1" points="5 21 5 3 21 12" fill="#000000"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 528 B |
|
@ -4,6 +4,7 @@ import * as videojs from 'video.js'
|
|||
import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings'
|
||||
import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
|
||||
import { Events } from 'p2p-media-loader-core'
|
||||
import { timeToInt } from '../utils'
|
||||
|
||||
// videojs-hlsjs-plugin needs videojs in window
|
||||
window['videojs'] = videojs
|
||||
|
@ -32,6 +33,7 @@ class P2pMediaLoaderPlugin extends Plugin {
|
|||
totalDownload: 0,
|
||||
totalUpload: 0
|
||||
}
|
||||
private startTime: number
|
||||
|
||||
private networkInfoInterval: any
|
||||
|
||||
|
@ -54,12 +56,14 @@ class P2pMediaLoaderPlugin extends Plugin {
|
|||
|
||||
initVideoJsContribHlsJsPlayer(player)
|
||||
|
||||
this.startTime = timeToInt(options.startTime)
|
||||
|
||||
player.src({
|
||||
type: options.type,
|
||||
src: options.src
|
||||
})
|
||||
|
||||
player.on('play', () => {
|
||||
player.one('play', () => {
|
||||
player.addClass('vjs-has-big-play-button-clicked')
|
||||
})
|
||||
|
||||
|
@ -92,6 +96,12 @@ class P2pMediaLoaderPlugin extends Plugin {
|
|||
this.statsP2PBytes.numPeers = 1 + this.options.redundancyBaseUrls.length
|
||||
|
||||
this.runStats()
|
||||
|
||||
this.hlsjs.on('hlsLevelLoaded', () => {
|
||||
if (this.startTime) this.player.currentTime(this.startTime)
|
||||
|
||||
this.hlsjs.off('hlsLevelLoaded', this)
|
||||
})
|
||||
}
|
||||
|
||||
private runStats () {
|
||||
|
|
|
@ -83,9 +83,15 @@ class PeerTubePlugin extends Plugin {
|
|||
|
||||
if (options.stopTime) {
|
||||
const stopTime = timeToInt(options.stopTime)
|
||||
const self = this
|
||||
|
||||
this.player.on('timeupdate', () => {
|
||||
if (this.player.currentTime() > stopTime) this.player.pause()
|
||||
this.player.on('timeupdate', function onTimeUpdate () {
|
||||
if (self.player.currentTime() > stopTime) {
|
||||
self.player.pause()
|
||||
self.player.trigger('stopped')
|
||||
|
||||
self.player.off('timeupdate', onTimeUpdate)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -28,15 +28,15 @@ $play-overlay-transition: 0.2s ease;
|
|||
$play-overlay-height: 26px;
|
||||
$play-overlay-width: 18px;
|
||||
|
||||
@mixin miniature-thumbnail($width: $video-thumbnail-width, $height: $video-thumbnail-height) {
|
||||
@mixin miniature-thumbnail {
|
||||
@include disable-outline;
|
||||
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
width: $width;
|
||||
height: $height;
|
||||
width: $video-thumbnail-width;
|
||||
height: $video-thumbnail-height;
|
||||
background-color: #ececec;
|
||||
transition: filter $play-overlay-transition;
|
||||
|
||||
|
@ -97,6 +97,13 @@ $play-overlay-width: 18px;
|
|||
}
|
||||
}
|
||||
|
||||
@mixin thumbnail-size-component ($width, $height) {
|
||||
/deep/ .video-thumbnail {
|
||||
width: $width;
|
||||
height: $height;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin static-thumbnail-overlay {
|
||||
display: inline-block;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
|
|
|
@ -63,11 +63,11 @@
|
|||
|
||||
@mixin apply-svg-color ($color) {
|
||||
/deep/ svg {
|
||||
path[fill="#000000"], g[fill="#000000"], rect[fill="#000000"], circle[fill="#000000"] {
|
||||
path[fill="#000000"], g[fill="#000000"], rect[fill="#000000"], circle[fill="#000000"], polygon[fill="#000000"] {
|
||||
fill: $color;
|
||||
}
|
||||
|
||||
path[stroke="#000000"], g[stroke="#000000"], rect[stroke="#000000"], circle[stroke="#000000"] {
|
||||
path[stroke="#000000"], g[stroke="#000000"], rect[stroke="#000000"], circle[stroke="#000000"], polygon[stroke="#000000"] {
|
||||
stroke: $color;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue