Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/src/app/+video-watch/shared/premiere/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './video-premiere.component'
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<div class="premiere-widget">
<div class="premiere-info">
<div class="premiere-title">
<h2 i18n>This video will be available soon!</h2>
</div>

<div class="premiere-countdown">
@if (!countdown.isExpired) {
<div class="countdown-timer">
<div class="countdown-unit" *ngIf="countdown.days > 0">
<span class="countdown-number">{{ countdown.days }}</span>
<span class="countdown-label" i18n>{{ countdown.days === 1 ? 'day' : 'days' }}</span>
</div>
<div class="countdown-unit">
<span class="countdown-number">{{ countdown.hours.toString().padStart(2, '0') }}</span>
<span class="countdown-label" i18n>hours</span>
</div>
<div class="countdown-unit">
<span class="countdown-number">{{ countdown.minutes.toString().padStart(2, '0') }}</span>
<span class="countdown-label" i18n>minutes</span>
</div>
<div class="countdown-unit">
<span class="countdown-number">{{ countdown.seconds.toString().padStart(2, '0') }}</span>
<span class="countdown-label" i18n>seconds</span>
</div>
</div>
} @else {
<p class="countdown-expired" i18n>Premiere has started!</p>
}
</div>

<div class="premiere-actions">
<my-subscribe-button
[videoChannels]="[video.channel]"
size="normal"
class="premiere-subscribe-button">
</my-subscribe-button>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
@use '_variables' as *;
@use '_mixins' as *;
@use '_bootstrap-variables';

.premiere-widget {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
padding: 20px;
text-align: center;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
pointer-events: auto;

.premiere-info {
max-width: 500px;

.premiere-title {
margin-bottom: 20px;

h2 {
font-size: 1.8em;
font-weight: 600;
color: #fff;
margin: 0;
}
}

.premiere-countdown {
margin-bottom: 30px;

.countdown-timer {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;

.countdown-unit {
display: flex;
flex-direction: column;
align-items: center;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 12px 16px;
min-width: 70px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);

.countdown-number {
font-size: 2.2em;
font-weight: 700;
color: #fff;
line-height: 1;
margin-bottom: 5px;
font-family: 'Roboto Mono', monospace;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}

.countdown-label {
font-size: 0.85em;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
}

.countdown-expired {
font-size: 1.4em;
color: #ff6b6b;
font-weight: 600;
margin-bottom: 15px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
}

.premiere-actions {
.premiere-subscribe-button {
::ng-deep .btn {
padding: 10px 20px;
font-size: 1em;
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.3);
color: #fff;

&:hover {
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
}

my-global-icon {
margin-right: 5px;
}
}
}
}
}
}

// Mobile responsive styles
@media screen and (max-width: 600px) {
.premiere-widget {
padding: 15px;

.premiere-info {
max-width: 100%;

.premiere-title h2 {
font-size: 1.4em;
}

.premiere-countdown {
.countdown-timer {
gap: 8px;

.countdown-unit {
padding: 8px 12px;
min-width: 55px;

.countdown-number {
font-size: 1.8em;
}

.countdown-label {
font-size: 0.75em;
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Component, Input, OnInit, OnDestroy } from '@angular/core'
import { NgIf } from '@angular/common'
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription/subscribe-button.component'

interface CountdownTime {
days: number
hours: number
minutes: number
seconds: number
isExpired: boolean
}

@Component({
selector: 'my-video-premiere',
templateUrl: './video-premiere.component.html',
styleUrls: [ './video-premiere.component.scss' ],
standalone: true,
imports: [
NgIf,
SubscribeButtonComponent
]
})
export class VideoPremiereComponent implements OnInit, OnDestroy {
@Input() video: VideoDetails
@Input() theaterEnabled = false

countdown: CountdownTime = {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
isExpired: false
}

private countdownInterval: any

ngOnInit() {
this.startCountdown()
}

ngOnDestroy() {
if (this.countdownInterval) {
clearInterval(this.countdownInterval)
}
}

private startCountdown() {
this.updateCountdown()
this.countdownInterval = setInterval(() => {
this.updateCountdown()
}, 1000)
}

private updateCountdown() {
if (!this.video?.scheduledUpdate?.updateAt) {
this.countdown.isExpired = true
return
}

const premiereTime = new Date(this.video.scheduledUpdate.updateAt).getTime()
const now = new Date().getTime()
const timeDiff = premiereTime - now

if (timeDiff <= 0) {
this.countdown.isExpired = true
if (this.countdownInterval) {
clearInterval(this.countdownInterval)
}
return
}

this.countdown.days = Math.floor(timeDiff / (1000 * 60 * 60 * 24))
this.countdown.hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
this.countdown.minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60))
this.countdown.seconds = Math.floor((timeDiff % (1000 * 60)) / 1000)
this.countdown.isExpired = false
}
}
20 changes: 17 additions & 3 deletions client/src/app/+video-watch/video-watch.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,23 @@
<ng-container i18n>Please try refreshing the page, or try again later.</ng-container>
</div>

<div id="videojs-wrapper">
<video #playerElement class="video-js vjs-peertube-skin" playsinline="true"></video>
</div>
@if (video && video.privacy.id === VideoPrivacy.PREMIERE) {
<div id="premiere-thumbnail-wrapper">
<img
[src]="video.previewUrl || video.thumbnailUrl"
[alt]="video.name"
class="premiere-thumbnail"
/>
<my-video-premiere
[video]="video"
[theaterEnabled]="theaterEnabled">
</my-video-premiere>
</div>
} @else {
<div id="videojs-wrapper">
<video #playerElement class="video-js vjs-peertube-skin" playsinline="true"></video>
</div>
}

<div class="player-widget-component">
<my-video-watch-playlist
Expand Down
44 changes: 44 additions & 0 deletions client/src/app/+video-watch/video-watch.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,24 @@ $video-max-height: calc(100vh - #{pvar(--header-height)} - #{$theater-bottom-spa
justify-content: center;

#videojs-wrapper {
position: relative;
width: 100%;
height: $video-max-height;
}

#premiere-thumbnail-wrapper {
position: relative;
width: 100%;
height: $video-max-height;

.premiere-thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 3px;
}
}

::ng-deep .video-js {
--co-player-height: #{$video-max-height};
}
Expand All @@ -56,10 +70,26 @@ $video-max-height: calc(100vh - #{pvar(--header-height)} - #{$theater-bottom-spa
border-radius:5px;

#videojs-wrapper {
position: relative;
display: flex;
justify-content: center;
flex-grow: 1;
height: $video-default-height;
}

#premiere-thumbnail-wrapper {
position: relative;
display: flex;
justify-content: center;
flex-grow: 1;
height: $video-default-height;

.premiere-thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 3px;
}
}

::ng-deep .video-js {
Expand Down Expand Up @@ -249,6 +279,7 @@ my-video-comments {

@media screen and (max-width: 600px) {
#videojs-wrapper {
position: relative;
// Reset height
height: initial !important;

Expand All @@ -261,6 +292,19 @@ my-video-comments {
}
}

#premiere-thumbnail-wrapper {
position: relative;
// Reset height
height: initial !important;

.premiere-thumbnail {
width: 100vw;
height: calc(100vw / var(--co-player-ratio, #{math.div(16, 9)}));
max-height: calc(100vh - #{pvar(--header-height)} - #{$player-portrait-bottom-space});
object-fit: cover;
}
}

.video-bottom {
margin-top: 20px !important;
padding-bottom: 20px !important;
Expand Down
6 changes: 5 additions & 1 deletion client/src/app/+video-watch/video-watch.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { VideoDescriptionComponent } from './shared/metadata/video-description.c
import { VideoTranscriptionComponent } from './shared/player-widgets/video-transcription.component'
import { VideoWatchPlaylistComponent } from './shared/player-widgets/video-watch-playlist.component'
import { RecommendedVideosComponent } from './shared/recommendations/recommended-videos.component'
import { VideoPremiereComponent } from './shared/premiere/video-premiere.component'

const debugLogger = debug('peertube:watch:VideoWatchComponent')

Expand Down Expand Up @@ -119,10 +120,13 @@ type URLOptions = {
PrivacyConcernsComponent,
PlayerStylesComponent,
VideoWatchPlaylistComponent,
VideoTranscriptionComponent
VideoTranscriptionComponent,
VideoPremiereComponent
]
})
export class VideoWatchComponent implements OnInit, OnDestroy {
VideoPrivacy = VideoPrivacy // Make VideoPrivacy accessible in template

private route = inject(ActivatedRoute)
private router = inject(Router)
private videoService = inject(VideoService)
Expand Down
Loading