client/src/app/shared/shared-share-modal/video-share.component.ts
import { NgClass, NgFor, NgIf } from '@angular/common'
import { Component, ElementRef, Input, ViewChild } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
import { RouterLink } from '@angular/router'
import { HooksService, ServerService } from '@app/core'
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
import {
NgbCollapse,
NgbModal,
NgbNav,
NgbNavContent,
NgbNavItem,
NgbNavLink,
NgbNavLinkBase,
NgbNavOutlet
} from '@ng-bootstrap/ng-bootstrap'
import { buildPlaylistLink, buildVideoLink, decoratePlaylistLink, decorateVideoLink } from '@peertube/peertube-core-utils'
import { VideoCaption, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models'
import { buildVideoOrPlaylistEmbed } from '@root-helpers/video'
import { QRCodeModule } from 'angularx-qrcode'
import { InputTextComponent } from '../shared-forms/input-text.component'
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
import { TimestampInputComponent } from '../shared-forms/timestamp-input.component'
import { GlobalIconComponent } from '../shared-icons/global-icon.component'
import { AlertComponent } from '../shared-main/common/alert.component'
import { PluginPlaceholderComponent } from '../shared-main/plugins/plugin-placeholder.component'
import { VideoPlaylist } from '../shared-video-playlist/video-playlist.model'
type Customizations = {
startAtCheckbox: boolean
startAt: number
stopAtCheckbox: boolean
stopAt: number
subtitleCheckbox: boolean
subtitle: string
loop: boolean
originUrl: boolean
autoplay: boolean
muted: boolean
embedP2P: boolean
onlyEmbedUrl: boolean
title: boolean
warningTitle: boolean
controlBar: boolean
peertubeLink: boolean
responsive: boolean
includeVideoInPlaylist: boolean
}
type TabId = 'url' | 'qrcode' | 'embed'
@Component({
selector: 'my-video-share',
templateUrl: './video-share.component.html',
styleUrls: [ './video-share.component.scss' ],
standalone: true,
imports: [
GlobalIconComponent,
NgIf,
RouterLink,
NgbNav,
NgbNavItem,
NgbNavLink,
NgbNavLinkBase,
NgbNavContent,
InputTextComponent,
QRCodeModule,
NgbNavOutlet,
PeertubeCheckboxComponent,
FormsModule,
PluginPlaceholderComponent,
TimestampInputComponent,
NgClass,
NgFor,
NgbCollapse,
AlertComponent
]
})
export class VideoShareComponent {
@ViewChild('modal', { static: true }) modal: ElementRef
@Input() video: VideoDetails = null
@Input() videoCaptions: VideoCaption[] = []
@Input() playlist: VideoPlaylist = null
@Input() playlistPosition: number = null
activeVideoId: TabId = 'url'
activePlaylistId: TabId = 'url'
customizations: Customizations
isAdvancedCustomizationCollapsed = true
videoUrl: string
playlistUrl: string
videoEmbedUrl: string
playlistEmbedUrl: string
videoEmbedHTML: string
videoEmbedSafeHTML: SafeHtml
playlistEmbedHTML: string
playlistEmbedSafeHTML: SafeHtml
constructor (
private modalService: NgbModal,
private sanitizer: DomSanitizer,
private server: ServerService,
private hooks: HooksService
) { }
show (currentVideoTimestamp?: number, currentPlaylistPosition?: number) {
let subtitle: string
if (this.videoCaptions && this.videoCaptions.length !== 0) {
subtitle = this.videoCaptions[0].language.id
}
this.customizations = new Proxy({
startAtCheckbox: false,
startAt: currentVideoTimestamp ? Math.floor(currentVideoTimestamp) : 0,
stopAtCheckbox: false,
stopAt: this.video?.duration,
subtitleCheckbox: false,
subtitle,
loop: false,
originUrl: false,
autoplay: false,
muted: false,
// Embed options
embedP2P: this.server.getHTMLConfig().defaults.p2p.embed.enabled,
onlyEmbedUrl: false,
title: true,
warningTitle: true,
controlBar: true,
peertubeLink: true,
responsive: false,
includeVideoInPlaylist: false
}, {
set: (target, prop, value) => {
(target as any)[prop] = value
if (prop === 'embedP2P') {
// Auto enabled warning title if P2P is enabled
this.customizations.warningTitle = value
}
this.onUpdate()
return true
}
})
this.playlistPosition = currentPlaylistPosition
this.onUpdate()
this.modalService.open(this.modal, { centered: true }).shown.subscribe(() => {
this.hooks.runAction('action:modal.share.shown', 'video-watch', { video: this.video, playlist: this.playlist })
})
}
// ---------------------------------------------------------------------------
getVideoUrl () {
const url = this.customizations.originUrl
? this.video.url
: buildVideoLink(this.video, window.location.origin)
return this.hooks.wrapFun(
decorateVideoLink,
{ url, ...this.getVideoOptions(false) },
'video-watch',
'filter:share.video-url.build.params',
'filter:share.video-url.build.result'
)
}
getVideoEmbedUrl () {
return this.hooks.wrapFun(
decorateVideoLink,
{ url: this.video.embedUrl, ...this.getVideoOptions(true) },
'video-watch',
'filter:share.video-embed-url.build.params',
'filter:share.video-embed-url.build.result'
)
}
async getVideoEmbedCode (options: { responsive: boolean }) {
const { responsive } = options
return this.hooks.wrapFun(
buildVideoOrPlaylistEmbed,
{ embedUrl: await this.getVideoEmbedUrl(), embedTitle: this.video.name, responsive, aspectRatio: this.video.aspectRatio },
'video-watch',
'filter:share.video-embed-code.build.params',
'filter:share.video-embed-code.build.result'
)
}
// ---------------------------------------------------------------------------
getPlaylistUrl () {
const url = buildPlaylistLink(this.playlist)
return this.hooks.wrapFun(
decoratePlaylistLink,
{ url, ...this.getPlaylistOptions() },
'video-watch',
'filter:share.video-playlist-url.build.params',
'filter:share.video-playlist-url.build.result'
)
}
getPlaylistEmbedUrl () {
return this.hooks.wrapFun(
decoratePlaylistLink,
{ url: this.playlist.embedUrl, ...this.getPlaylistOptions() },
'video-watch',
'filter:share.video-playlist-embed-url.build.params',
'filter:share.video-playlist-embed-url.build.result'
)
}
async getPlaylistEmbedCode (options: { responsive: boolean }) {
const { responsive } = options
return this.hooks.wrapFun(
buildVideoOrPlaylistEmbed,
{
embedUrl: await this.getPlaylistEmbedUrl(),
embedTitle: this.playlist.displayName,
responsive,
aspectRatio: this.video?.aspectRatio
},
'video-watch',
'filter:share.video-playlist-embed-code.build.params',
'filter:share.video-playlist-embed-code.build.result'
)
}
// ---------------------------------------------------------------------------
async onUpdate () {
if (this.playlist) {
this.playlistUrl = await this.getPlaylistUrl()
this.playlistEmbedUrl = await this.getPlaylistEmbedUrl()
this.playlistEmbedHTML = await this.getPlaylistEmbedCode({ responsive: this.customizations.responsive })
this.playlistEmbedSafeHTML = this.sanitizer.bypassSecurityTrustHtml(await this.getPlaylistEmbedCode({ responsive: false }))
}
if (this.video) {
this.videoUrl = await this.getVideoUrl()
this.videoEmbedUrl = await this.getVideoEmbedUrl()
this.videoEmbedHTML = await this.getVideoEmbedCode({ responsive: this.customizations.responsive })
this.videoEmbedSafeHTML = this.sanitizer.bypassSecurityTrustHtml(await this.getVideoEmbedCode({ responsive: false }))
}
}
notSecure () {
return window.location.protocol === 'http:'
}
isInVideoEmbedTab () {
return this.activeVideoId === 'embed'
}
isInPlaylistEmbedTab () {
return this.activePlaylistId === 'embed'
}
isLocalVideo () {
return this.video.isLocal
}
isPrivateVideo () {
return this.video.privacy.id === VideoPrivacy.PRIVATE
}
isPrivatePlaylist () {
return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE
}
isPasswordProtectedVideo () {
return this.video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED
}
private getPlaylistOptions (baseUrl?: string) {
return {
baseUrl,
playlistPosition: this.playlistPosition && this.customizations.includeVideoInPlaylist
? this.playlistPosition
: undefined
}
}
private getVideoOptions (forEmbed: boolean) {
const embedOptions = forEmbed
? {
title: this.customizations.title,
warningTitle: this.customizations.warningTitle,
controlBar: this.customizations.controlBar,
peertubeLink: this.customizations.peertubeLink,
// If using default value, we don't need to specify it
p2p: this.customizations.embedP2P === this.server.getHTMLConfig().defaults.p2p.embed.enabled
? undefined
: this.customizations.embedP2P
}
: {}
return {
startTime: this.customizations.startAtCheckbox ? this.customizations.startAt : undefined,
stopTime: this.customizations.stopAtCheckbox ? this.customizations.stopAt : undefined,
subtitle: this.customizations.subtitleCheckbox ? this.customizations.subtitle : undefined,
loop: this.customizations.loop,
autoplay: this.customizations.autoplay,
muted: this.customizations.muted,
...embedOptions
}
}
}