1
0
Fork 0

Add ability to share playlists in modal

This commit is contained in:
Chocobozzz 2020-08-07 13:43:48 +02:00
parent 4891e4c77b
commit 951b582f52
No known key found for this signature in database
GPG key ID: 583A612D890159BE
13 changed files with 143 additions and 43 deletions

View file

@ -7,7 +7,7 @@ import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
import { VideoBlockService } from '@app/shared/shared-moderation' import { VideoBlockService } from '@app/shared/shared-moderation'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { VideoBlacklist, VideoBlacklistType } from '@shared/models' import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' import { buildVideoOrPlaylistEmbed, buildVideoLink } from 'src/assets/player/utils'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
import { DomSanitizer } from '@angular/platform-browser' import { DomSanitizer } from '@angular/platform-browser'
@ -176,7 +176,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
} }
getVideoEmbed (entry: VideoBlacklist) { getVideoEmbed (entry: VideoBlacklist) {
return buildVideoEmbed( return buildVideoOrPlaylistEmbed(
buildVideoLink({ buildVideoLink({
baseUrl: `${environment.embedUrl}/videos/embed/${entry.video.uuid}`, baseUrl: `${environment.embedUrl}/videos/embed/${entry.video.uuid}`,
title: false, title: false,

View file

@ -135,7 +135,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
this.comment.account = null this.comment.account = null
} }
if (this.isUserLoggedIn() && this.authService.getUser().account.id !== this.comment.account.id) { if (this.isUserLoggedIn() && this.comment.isDeleted === false && this.authService.getUser().account.id !== this.comment.account.id) {
this.prependModerationActions = [ this.prependModerationActions = [
{ {
label: this.i18n('Report comment'), label: this.i18n('Report comment'),

View file

@ -6,18 +6,56 @@
<div class="modal-body"> <div class="modal-body">
<div class="playlist" *ngIf="hasPlaylist()"> <div class="playlist" *ngIf="hasPlaylist()">
<div class="title-page title-page-single" i18n>Share the playlist</div> <div class="title-page title-page-single" i18n>Share the playlist</div>
<my-input-readonly-copy [value]="getPlaylistUrl()"></my-input-readonly-copy> <div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activePlaylistId">
<ng-container ngbNavItem="url">
<a ngbNavLink i18n>URL</a>
<ng-template ngbNavContent>
<div class="nav-content">
<my-input-readonly-copy [value]="getPlaylistUrl()"></my-input-readonly-copy>
</div>
</ng-template>
</ng-container>
<ng-container ngbNavItem="qrcode">
<a ngbNavLink i18n>QR-Code</a>
<ng-template ngbNavContent>
<div class="nav-content">
<qrcode [qrdata]="getPlaylistUrl()" [size]="256" level="Q"></qrcode>
</div>
</ng-template>
</ng-container>
<ng-container ngbNavItem="embed">
<a ngbNavLink i18n>Embed</a>
<ng-template ngbNavContent>
<div class="nav-content">
<my-input-readonly-copy [value]="getPlaylistIframeCode()"></my-input-readonly-copy>
<div i18n *ngIf="notSecure()" class="alert alert-warning">
The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS websites).
</div>
</div>
</ng-template>
</ng-container>
</div>
<div [ngbNavOutlet]="nav"></div>
<div class="filters"> <div class="filters">
<div class="form-group"> <div class="form-group">
<my-peertube-checkbox <my-peertube-checkbox inputName="includeVideoInPlaylist" [(ngModel)]="includeVideoInPlaylist" i18n-labelText
inputName="includeVideoInPlaylist" [(ngModel)]="includeVideoInPlaylist" labelText="Share the playlist at this video position"></my-peertube-checkbox>
i18n-labelText labelText="Share the playlist at this video position"
></my-peertube-checkbox>
</div> </div>
</div> </div>
@ -27,7 +65,7 @@
<div class="video"> <div class="video">
<div class="title-page title-page-single" *ngIf="hasPlaylist()" i18n>Share the video</div> <div class="title-page title-page-single" *ngIf="hasPlaylist()" i18n>Share the video</div>
<div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activeId"> <div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activeVideoId">
<ng-container ngbNavItem="url"> <ng-container ngbNavItem="url">
<a ngbNavLink i18n>URL</a> <a ngbNavLink i18n>URL</a>
@ -137,7 +175,7 @@
</div> </div>
</div> </div>
<ng-container *ngIf="isInEmbedTab()"> <ng-container *ngIf="isVideoInEmbedTab()">
<div class="form-group"> <div class="form-group">
<my-peertube-checkbox <my-peertube-checkbox
inputName="title" [(ngModel)]="customizations.title" inputName="title" [(ngModel)]="customizations.title"

View file

@ -1,5 +1,5 @@
import { Component, ElementRef, Input, ViewChild } from '@angular/core' import { Component, ElementRef, Input, ViewChild } from '@angular/core'
import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' import { buildVideoOrPlaylistEmbed, buildVideoLink, buildPlaylistLink } from '../../../../assets/player/utils'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { VideoCaption } from '@shared/models' import { VideoCaption } from '@shared/models'
import { VideoDetails } from '@app/shared/shared-main' import { VideoDetails } from '@app/shared/shared-main'
@ -24,6 +24,8 @@ type Customizations = {
peertubeLink: boolean peertubeLink: boolean
} }
type TabId = 'url' | 'qrcode' | 'embed'
@Component({ @Component({
selector: 'my-video-share', selector: 'my-video-share',
templateUrl: './video-share.component.html', templateUrl: './video-share.component.html',
@ -36,14 +38,18 @@ export class VideoShareComponent {
@Input() videoCaptions: VideoCaption[] = [] @Input() videoCaptions: VideoCaption[] = []
@Input() playlist: VideoPlaylist = null @Input() playlist: VideoPlaylist = null
activeId: 'url' | 'qrcode' | 'embed' = 'url' activeVideoId: TabId = 'url'
activePlaylistId: TabId = 'url'
customizations: Customizations customizations: Customizations
isAdvancedCustomizationCollapsed = true isAdvancedCustomizationCollapsed = true
includeVideoInPlaylist = false includeVideoInPlaylist = false
private playlistPosition: number = null
constructor (private modalService: NgbModal) { } constructor (private modalService: NgbModal) { }
show (currentVideoTimestamp?: number) { show (currentVideoTimestamp?: number, currentPlaylistPosition?: number) {
let subtitle: string let subtitle: string
if (this.videoCaptions.length !== 0) { if (this.videoCaptions.length !== 0) {
subtitle = this.videoCaptions[0].language.id subtitle = this.videoCaptions[0].language.id
@ -70,19 +76,28 @@ export class VideoShareComponent {
peertubeLink: true peertubeLink: true
} }
this.playlistPosition = currentPlaylistPosition
this.modalService.open(this.modal, { centered: true }) this.modalService.open(this.modal, { centered: true })
} }
getVideoIframeCode () { getVideoIframeCode () {
const options = this.getOptions(this.video.embedUrl) const options = this.getVideoOptions(this.video.embedUrl)
const embedUrl = buildVideoLink(options) const embedUrl = buildVideoLink(options)
return buildVideoEmbed(embedUrl) return buildVideoOrPlaylistEmbed(embedUrl)
}
getPlaylistIframeCode () {
const options = this.getPlaylistOptions(this.playlist.embedUrl)
const embedUrl = buildPlaylistLink(options)
return buildVideoOrPlaylistEmbed(embedUrl)
} }
getVideoUrl () { getVideoUrl () {
const baseUrl = window.location.origin + '/videos/watch/' + this.video.uuid const baseUrl = window.location.origin + '/videos/watch/' + this.video.uuid
const options = this.getOptions(baseUrl) const options = this.getVideoOptions(baseUrl)
return buildVideoLink(options) return buildVideoLink(options)
} }
@ -99,15 +114,23 @@ export class VideoShareComponent {
return window.location.protocol === 'http:' return window.location.protocol === 'http:'
} }
isInEmbedTab () { isVideoInEmbedTab () {
return this.activeId === 'embed' return this.activeVideoId === 'embed'
} }
hasPlaylist () { hasPlaylist () {
return !!this.playlist return !!this.playlist
} }
private getOptions (baseUrl?: string) { private getPlaylistOptions (baseUrl?: string) {
return {
baseUrl,
playlistPosition: this.playlistPosition || undefined
}
}
private getVideoOptions (baseUrl?: string) {
return { return {
baseUrl, baseUrl,

View file

@ -244,7 +244,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
showShareModal () { showShareModal () {
this.pausePlayer() this.pausePlayer()
this.videoShareModal.show(this.currentTime) this.videoShareModal.show(this.currentTime, this.videoWatchPlaylist.currentPlaylistPosition)
} }
isUserLoggedIn () { isUserLoggedIn () {

View file

@ -1,7 +1,7 @@
import * as debug from 'debug' import * as debug from 'debug'
import truncate from 'lodash-es/truncate' import truncate from 'lodash-es/truncate'
import { SortMeta } from 'primeng/api' import { SortMeta } from 'primeng/api'
import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' import { buildVideoOrPlaylistEmbed, buildVideoLink } from 'src/assets/player/utils'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
import { AfterViewInit, Component, OnInit, ViewChild, Input } from '@angular/core' import { AfterViewInit, Component, OnInit, ViewChild, Input } from '@angular/core'
import { DomSanitizer } from '@angular/platform-browser' import { DomSanitizer } from '@angular/platform-browser'
@ -141,7 +141,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
} }
getVideoEmbed (abuse: AdminAbuse) { getVideoEmbed (abuse: AdminAbuse) {
return buildVideoEmbed( return buildVideoOrPlaylistEmbed(
buildVideoLink({ buildVideoLink({
baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`, baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
title: false, title: false,

View file

@ -1,5 +1,5 @@
import { mapValues, pickBy } from 'lodash-es' import { mapValues, pickBy } from 'lodash-es'
import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' import { buildVideoOrPlaylistEmbed, buildVideoLink } from 'src/assets/player/utils'
import { Component, Input, OnInit, ViewChild } from '@angular/core' import { Component, Input, OnInit, ViewChild } from '@angular/core'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser' import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
import { Notifier } from '@app/core' import { Notifier } from '@app/core'
@ -58,7 +58,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
getVideoEmbed () { getVideoEmbed () {
return this.sanitizer.bypassSecurityTrustHtml( return this.sanitizer.bypassSecurityTrustHtml(
buildVideoEmbed( buildVideoOrPlaylistEmbed(
buildVideoLink({ buildVideoLink({
baseUrl: this.video.embedUrl, baseUrl: this.video.embedUrl,
title: false, title: false,

View file

@ -1,4 +1,4 @@
import { getAbsoluteAPIUrl } from '@app/helpers' import { getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
import { Actor } from '@app/shared/shared-main' import { Actor } from '@app/shared/shared-main'
import { peertubeTranslate } from '@shared/core-utils/i18n' import { peertubeTranslate } from '@shared/core-utils/i18n'
import { import {
@ -33,6 +33,9 @@ export class VideoPlaylist implements ServerVideoPlaylist {
thumbnailUrl: string thumbnailUrl: string
embedPath: string
embedUrl: string
ownerBy: string ownerBy: string
ownerAvatarUrl: string ownerAvatarUrl: string
@ -63,6 +66,9 @@ export class VideoPlaylist implements ServerVideoPlaylist {
this.thumbnailUrl = window.location.origin + '/client/assets/images/default-playlist.jpg' this.thumbnailUrl = window.location.origin + '/client/assets/images/default-playlist.jpg'
} }
this.embedPath = hash.embedPath
this.embedUrl = getAbsoluteEmbedUrl() + hash.embedPath
this.videosLength = hash.videosLength this.videosLength = hash.videosLength
this.type = hash.type this.type = hash.type

View file

@ -35,7 +35,7 @@ import {
VideoJSPluginOptions VideoJSPluginOptions
} from './peertube-videojs-typings' } from './peertube-videojs-typings'
import { TranslationsManager } from './translations-manager' import { TranslationsManager } from './translations-manager'
import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig, isIOS, isSafari } from './utils' import { buildVideoOrPlaylistEmbed, buildVideoLink, copyToClipboard, getRtcConfig, isIOS, isSafari } from './utils'
// Change 'Playback Rate' to 'Speed' (smaller for our settings menu) // Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
(videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
@ -492,7 +492,7 @@ export class PeertubePlayerManager {
{ {
label: player.localize('Copy embed code'), label: player.localize('Copy embed code'),
listener: () => { listener: () => {
copyToClipboard(buildVideoEmbed(videoEmbedUrl)) copyToClipboard(buildVideoOrPlaylistEmbed(videoEmbedUrl))
} }
} }
] ]

View file

@ -43,20 +43,20 @@ function isMobile () {
} }
function buildVideoLink (options: { function buildVideoLink (options: {
baseUrl?: string, baseUrl?: string
startTime?: number, startTime?: number
stopTime?: number, stopTime?: number
subtitle?: string, subtitle?: string
loop?: boolean, loop?: boolean
autoplay?: boolean, autoplay?: boolean
muted?: boolean, muted?: boolean
// Embed options // Embed options
title?: boolean, title?: boolean
warningTitle?: boolean, warningTitle?: boolean
controls?: boolean controls?: boolean
peertubeLink?: boolean peertubeLink?: boolean
} = {}) { } = {}) {
@ -66,10 +66,7 @@ function buildVideoLink (options: {
? baseUrl ? baseUrl
: window.location.origin + window.location.pathname.replace('/embed/', '/watch/') : window.location.origin + window.location.pathname.replace('/embed/', '/watch/')
const params = new URLSearchParams(window.location.search) const params = generateParams(window.location.search)
// Remove these unused parameters when we are on a playlist page
params.delete('videoId')
params.delete('resume')
if (options.startTime) { if (options.startTime) {
const startTimeInt = Math.floor(options.startTime) const startTimeInt = Math.floor(options.startTime)
@ -91,6 +88,28 @@ function buildVideoLink (options: {
if (options.controls === false) params.set('controls', '0') if (options.controls === false) params.set('controls', '0')
if (options.peertubeLink === false) params.set('peertubeLink', '0') if (options.peertubeLink === false) params.set('peertubeLink', '0')
return buildUrl(url, params)
}
function buildPlaylistLink (options: {
baseUrl?: string
playlistPosition: number
}) {
const { baseUrl } = options
const url = baseUrl
? baseUrl
: window.location.origin + window.location.pathname.replace('/video-playlists/embed/', '/videos/watch/playlist/')
const params = generateParams(window.location.search)
if (options.playlistPosition) params.set('playlistPosition', '' + options.playlistPosition)
return buildUrl(url, params)
}
function buildUrl (url: string, params: URLSearchParams) {
let hasParams = false let hasParams = false
params.forEach(() => hasParams = true) params.forEach(() => hasParams = true)
@ -99,6 +118,15 @@ function buildVideoLink (options: {
return url return url
} }
function generateParams (url: string) {
const params = new URLSearchParams(window.location.search)
// Unused parameters in embed
params.delete('videoId')
params.delete('resume')
return params
}
function timeToInt (time: number | string) { function timeToInt (time: number | string) {
if (!time) return 0 if (!time) return 0
if (typeof time === 'number') return time if (typeof time === 'number') return time
@ -140,7 +168,7 @@ function secondsToTime (seconds: number, full = false, symbol?: string) {
return time return time
} }
function buildVideoEmbed (embedUrl: string) { function buildVideoOrPlaylistEmbed (embedUrl: string) {
return '<iframe width="560" height="315" ' + return '<iframe width="560" height="315" ' +
'sandbox="allow-same-origin allow-scripts allow-popups" ' + 'sandbox="allow-same-origin allow-scripts allow-popups" ' +
'src="' + embedUrl + '" ' + 'src="' + embedUrl + '" ' +
@ -203,8 +231,9 @@ export {
timeToInt, timeToInt,
secondsToTime, secondsToTime,
isWebRTCDisabled, isWebRTCDisabled,
buildPlaylistLink,
buildVideoLink, buildVideoLink,
buildVideoEmbed, buildVideoOrPlaylistEmbed,
videoFileMaxByResolution, videoFileMaxByResolution,
videoFileMinByResolution, videoFileMinByResolution,
copyToClipboard, copyToClipboard,

View file

@ -528,6 +528,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
}, },
thumbnailPath: this.getThumbnailStaticPath(), thumbnailPath: this.getThumbnailStaticPath(),
embedPath: this.getEmbedStaticPath(),
type: { type: {
id: this.type, id: this.type,

View file

@ -236,7 +236,7 @@ describe('Test video playlists', function () {
const playlistFromList = res.body.data[0] as VideoPlaylist const playlistFromList = res.body.data[0] as VideoPlaylist
const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid) const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid)
const playlistFromGet = res2.body const playlistFromGet = res2.body as VideoPlaylist
for (const playlist of [ playlistFromGet, playlistFromList ]) { for (const playlist of [ playlistFromGet, playlistFromList ]) {
expect(playlist.id).to.be.a('number') expect(playlist.id).to.be.a('number')
@ -250,6 +250,7 @@ describe('Test video playlists', function () {
expect(playlist.privacy.label).to.equal('Public') expect(playlist.privacy.label).to.equal('Public')
expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR) expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
expect(playlist.type.label).to.equal('Regular') expect(playlist.type.label).to.equal('Regular')
expect(playlist.embedPath).to.equal('/video-playlists/embed/' + playlist.uuid)
expect(playlist.videosLength).to.equal(0) expect(playlist.videosLength).to.equal(0)

View file

@ -18,6 +18,8 @@ export interface VideoPlaylist {
type: VideoConstant<VideoPlaylistType> type: VideoConstant<VideoPlaylistType>
embedPath: string
createdAt: Date | string createdAt: Date | string
updatedAt: Date | string updatedAt: Date | string