diff --git a/client/src/app/+video-watch/shared/premiere/index.ts b/client/src/app/+video-watch/shared/premiere/index.ts new file mode 100644 index 00000000000..6800a0a5430 --- /dev/null +++ b/client/src/app/+video-watch/shared/premiere/index.ts @@ -0,0 +1 @@ +export * from './video-premiere.component' diff --git a/client/src/app/+video-watch/shared/premiere/video-premiere.component.html b/client/src/app/+video-watch/shared/premiere/video-premiere.component.html new file mode 100644 index 00000000000..15df273ae0f --- /dev/null +++ b/client/src/app/+video-watch/shared/premiere/video-premiere.component.html @@ -0,0 +1,40 @@ +
\ No newline at end of file diff --git a/client/src/app/+video-watch/shared/premiere/video-premiere.component.scss b/client/src/app/+video-watch/shared/premiere/video-premiere.component.scss new file mode 100644 index 00000000000..06e96d56b5b --- /dev/null +++ b/client/src/app/+video-watch/shared/premiere/video-premiere.component.scss @@ -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; + } + } + } + } + } + } +} \ No newline at end of file diff --git a/client/src/app/+video-watch/shared/premiere/video-premiere.component.ts b/client/src/app/+video-watch/shared/premiere/video-premiere.component.ts new file mode 100644 index 00000000000..6faecf9d42a --- /dev/null +++ b/client/src/app/+video-watch/shared/premiere/video-premiere.component.ts @@ -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 + } +} diff --git a/client/src/app/+video-watch/video-watch.component.html b/client/src/app/+video-watch/video-watch.component.html index 0fb189f3a5c..a5e69f55ef4 100644 --- a/client/src/app/+video-watch/video-watch.component.html +++ b/client/src/app/+video-watch/video-watch.component.html @@ -10,9 +10,23 @@