From c1965f0f400fa22dac77ebe283ee39f14b8a48e0 Mon Sep 17 00:00:00 2001 From: Olivia Madrid <oliviajmadrid@gmail.com> Date: Sun, 11 Aug 2019 23:48:35 -0600 Subject: [PATCH] (WIP): Media modal - handle video and more --- .../marketing/marketing.component.scss | 2 +- .../components/cards/activity/activity.html | 7 +- .../components/cards/activity/activity.ts | 18 +- .../video/players/direct-http.component.ts | 38 +-- .../video/players/torrent.component.ts | 29 +- .../components/video/video.component.html | 19 +- .../components/video/video.component.scss | 15 +- .../media/components/video/video.component.ts | 54 +++- .../volume-slider.component.html | 1 + .../volume-slider.component.scss | 11 + .../modules/media/modal/modal.component.html | 24 +- .../modules/media/modal/modal.component.scss | 72 +++-- .../modules/media/modal/modal.component.ts | 263 +++++++++++------- 13 files changed, 356 insertions(+), 197 deletions(-) diff --git a/src/app/modules/blockchain/marketing/marketing.component.scss b/src/app/modules/blockchain/marketing/marketing.component.scss index 2f70958536..e0bc9f29d7 100644 --- a/src/app/modules/blockchain/marketing/marketing.component.scss +++ b/src/app/modules/blockchain/marketing/marketing.component.scss @@ -155,7 +155,7 @@ m-blockchain--marketing { m-video .minds-video-play-icon { @include m-theme(){ - text-shadow: 0 0 3px themed($m-grey-900); + text-shadow: 0 0 3px rgba(themed($m-black-always), 0.6); } } diff --git a/src/app/modules/legacy/components/cards/activity/activity.html b/src/app/modules/legacy/components/cards/activity/activity.html index 91aaedd653..afc1607ec1 100644 --- a/src/app/modules/legacy/components/cards/activity/activity.html +++ b/src/app/modules/legacy/components/cards/activity/activity.html @@ -173,7 +173,8 @@ [guid]="activity.custom_data.guid" [playCount]="activity['play:count']" [torrent]="[{ res: '360', key: activity.custom_data.guid + '/360.mp4' }]" - (triggerMediaModal)="showMediaModal('video')" + (requestedMediaModal)="showMediaModal()" + (videoMetadataLoaded)="setVideoDimensions($event)" #player> <video-ads [player]="player" *ngIf="activity.monetized"></video-ads> </m-video> @@ -190,7 +191,7 @@ </span> </div> - <a class="m-activity--image-link" (click)="showMediaModal('image')"> + <a class="m-activity--image-link"> <img [src]="activity.thumbnail_src" (error)="activity.thumbnail_src = null"> </a> </div> @@ -205,7 +206,7 @@ <span i18n="@@M__COMMON__CONFIRM_18">Click to confirm your are 18+</span> </span> </div> - <a class="m-activity--image-link" (click)="showMediaModal('image')"> + <a class="m-activity--image-link" (click)="showMediaModal()"> <img [src]="activity.custom_data[0].src" style="width:100%" (error)="activity.custom_data[0].src = minds.cdn_assets_url + 'assets/logos/placeholder-bulb.jpg'" *ngIf="activity.custom_data" diff --git a/src/app/modules/legacy/components/cards/activity/activity.ts b/src/app/modules/legacy/components/cards/activity/activity.ts index 19d514da1d..c3a8505a50 100644 --- a/src/app/modules/legacy/components/cards/activity/activity.ts +++ b/src/app/modules/legacy/components/cards/activity/activity.ts @@ -11,7 +11,6 @@ import { Injector, } from '@angular/core'; -import { Location } from '@angular/common'; import { Client } from '../../../../../services/api'; import { Session } from '../../../../../services/session'; import { AttachmentService } from '../../../../../services/attachment'; @@ -113,7 +112,6 @@ export class Activity implements OnInit { private cd: ChangeDetectorRef, private entitiesService: EntitiesService, private router: Router, - private location: Location, protected blockListService: BlockListService, protected activityAnalyticsOnViewService: ActivityAnalyticsOnViewService, protected newsfeedService: NewsfeedService, @@ -436,12 +434,11 @@ export class Activity implements OnInit { this.activity.mature_visibility = !this.activity.mature_visibility; } - detectChanges() { - this.cd.markForCheck(); - this.cd.detectChanges(); + setVideoDimensions($event) { + this.activity.custom_data.dimensions = $event.dimensions; } - showMediaModal(subtype: string) { + showMediaModal() { // Mobile users go to media page instead of modal if (isMobile()) { this.router.navigate([`/media/${this.activity.entity_guid}`]); @@ -449,11 +446,14 @@ export class Activity implements OnInit { this.activity.modal_source_url = this.router.url; - // 'image' or 'video' - this.activity.modal_subtype = subtype; - this.overlayModal.create(MediaModalComponent, this.activity, { class: 'm-overlayModal--media' }).present(); + + } + + detectChanges() { + this.cd.markForCheck(); + this.cd.detectChanges(); } } diff --git a/src/app/modules/media/components/video/players/direct-http.component.ts b/src/app/modules/media/components/video/players/direct-http.component.ts index 83fb773833..87e7780afe 100644 --- a/src/app/modules/media/components/video/players/direct-http.component.ts +++ b/src/app/modules/media/components/video/players/direct-http.component.ts @@ -2,9 +2,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; -import { Router } from '@angular/router'; import { MindsPlayerInterface } from './player.interface'; -import isMobile from '../../../../../helpers/is-mobile'; @Component({ moduleId: module.id, @@ -38,19 +36,24 @@ export class MindsVideoDirectHttpPlayer implements OnInit, OnDestroy, MindsPlaye @Output() onPause: EventEmitter<HTMLVideoElement> = new EventEmitter(); @Output() onEnd: EventEmitter<HTMLVideoElement> = new EventEmitter(); @Output() onError: EventEmitter<{ player: HTMLVideoElement, e }> = new EventEmitter(); - @Output() triggerMediaModal: EventEmitter<any> = new EventEmitter(); + @Output() onCanPlay: EventEmitter<any> = new EventEmitter(); + @Output() onLoadedMetadata: EventEmitter<any> = new EventEmitter(); + @Output() onLoadedData: EventEmitter<any> = new EventEmitter(); + @Output() requestedMediaModal: EventEmitter<any> = new EventEmitter(); loading: boolean = false; - isModal: boolean = false; constructor( protected cd: ChangeDetectorRef, - private router: Router, ) { } + protected _emitPlay = () => this.onPlay.emit(this.getPlayer()); protected _emitPause = () => this.onPause.emit(this.getPlayer()); protected _emitEnd = () => this.onEnd.emit(this.getPlayer()); protected _emitError = e => this.onError.emit({ player: this.getPlayer(), e }); + protected _emitCanPlay = () => this.onCanPlay.emit(this.getPlayer()); + protected _emitLoadedMetadata = () => this.onLoadedMetadata.emit(this.getPlayer()); + protected _emitLoadedData = () => this.onLoadedData.emit(this.getPlayer()); protected _canPlayThrough = () => { this.loading = false; @@ -58,7 +61,7 @@ export class MindsVideoDirectHttpPlayer implements OnInit, OnDestroy, MindsPlaye }; protected _dblClick = () => { - this.requestFullScreen(); + this.requestedMediaModal.emit(); }; protected _onPlayerError = e => { @@ -81,9 +84,11 @@ export class MindsVideoDirectHttpPlayer implements OnInit, OnDestroy, MindsPlaye player.addEventListener('ended', this._emitEnd); player.addEventListener('error', this._onPlayerError); player.addEventListener('canplaythrough', this._canPlayThrough); + player.addEventListener('canplay', this._emitCanPlay); + player.addEventListener('loadedmetadata', this._emitLoadedMetadata); + player.addEventListener('loadeddata', this._emitLoadedData); this.loading = true; - this.isModal = document.body.classList.contains('m-overlay-modal--shown'); } ngOnDestroy() { @@ -96,6 +101,9 @@ export class MindsVideoDirectHttpPlayer implements OnInit, OnDestroy, MindsPlaye player.removeEventListener('ended', this._emitEnd); player.removeEventListener('error', this._onPlayerError); player.removeEventListener('canplaythrough', this._canPlayThrough); + player.removeEventListener('canplay', this._emitCanPlay); + player.removeEventListener('loadedmetadata', this._emitLoadedMetadata); + player.removeEventListener('loadeddata', this._emitLoadedData); } } @@ -172,21 +180,13 @@ export class MindsVideoDirectHttpPlayer implements OnInit, OnDestroy, MindsPlaye return {}; } + requestMediaModal() { + this.requestedMediaModal.emit(); + } + detectChanges() { this.cd.markForCheck(); this.cd.detectChanges(); } - requestMediaModal() { - // Don't reopen modal if you're already on it - if ( this.isModal ) { - this.toggle(); - } - - // Mobile users go to media page instead of modal - if (isMobile()) { - this.router.navigate([`/media/${this.guid}`]); - } - this.triggerMediaModal.emit(); - } } diff --git a/src/app/modules/media/components/video/players/torrent.component.ts b/src/app/modules/media/components/video/players/torrent.component.ts index 10600aa56e..0b2d7313ab 100644 --- a/src/app/modules/media/components/video/players/torrent.component.ts +++ b/src/app/modules/media/components/video/players/torrent.component.ts @@ -3,12 +3,10 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; -import { Router } from '@angular/router'; import { MindsPlayerInterface } from './player.interface'; import { WebtorrentService } from '../../../../webtorrent/webtorrent.service'; import { Client } from '../../../../../services/api/client'; import base64ToBlob from '../../../../../helpers/base64-to-blob'; -import isMobile from '../../../../../helpers/is-mobile'; @Component({ moduleId: module.id, @@ -41,7 +39,10 @@ export class MindsVideoTorrentPlayer implements OnInit, AfterViewInit, OnDestroy @Output() onPause: EventEmitter<HTMLVideoElement> = new EventEmitter(); @Output() onEnd: EventEmitter<HTMLVideoElement> = new EventEmitter(); @Output() onError: EventEmitter<{ player, e }> = new EventEmitter(); - @Output() triggerMediaModal: EventEmitter<any> = new EventEmitter(); + @Output() onCanPlay: EventEmitter<any> = new EventEmitter(); + @Output() onLoadedMetadata: EventEmitter<any> = new EventEmitter(); + @Output() onLoadedData: EventEmitter<any> = new EventEmitter(); + @Output() requestedMediaModal: EventEmitter<any> = new EventEmitter(); initialized: boolean = false; loading: boolean = false; @@ -66,13 +67,15 @@ export class MindsVideoTorrentPlayer implements OnInit, AfterViewInit, OnDestroy protected cd: ChangeDetectorRef, protected client: Client, protected webtorrent: WebtorrentService, - private router: Router, ) { } protected _emitPlay = () => this.onPlay.emit(this.getPlayer()); protected _emitPause = () => this.onPause.emit(this.getPlayer()); protected _emitEnd = () => this.onEnd.emit(this.getPlayer()); protected _emitError = e => this.onError.emit({ player: this.getPlayer(), e}); + protected _emitCanPlay = () => this.onCanPlay.emit(this.getPlayer()); + protected _emitLoadedMetadata = () => this.onLoadedMetadata.emit(this.getPlayer()); + protected _emitLoadedData = () => this.onLoadedData.emit(this.getPlayer()); protected _canPlayThrough = () => { this.loading = false; @@ -136,6 +139,9 @@ export class MindsVideoTorrentPlayer implements OnInit, AfterViewInit, OnDestroy player.addEventListener('ended', this._emitEnd); player.addEventListener('error', this._onPlayerError); player.addEventListener('canplaythrough', this._canPlayThrough); + player.addEventListener('canplay', this._emitCanPlay); + player.addEventListener('loadedmetadata', this._emitLoadedMetadata); + player.addEventListener('loadeddata', this._emitLoadedData); this.infoTimer$ = setInterval(this._refreshInfo, 1000); this.isModal = document.body.classList.contains('m-overlay-modal--shown'); @@ -165,6 +171,9 @@ export class MindsVideoTorrentPlayer implements OnInit, AfterViewInit, OnDestroy player.removeEventListener('ended', this._emitEnd); player.removeEventListener('error', this._onPlayerError); player.removeEventListener('canplaythrough', this._canPlayThrough); + player.removeEventListener('canplay', this._emitCanPlay); + player.removeEventListener('loadedmetadata', this._emitLoadedMetadata); + player.removeEventListener('loadeddata', this._emitLoadedData); } } @@ -360,16 +369,8 @@ export class MindsVideoTorrentPlayer implements OnInit, AfterViewInit, OnDestroy this.torrentReady = false; } } - requestMediaModal() { - // Don't reopen modal if you're already on it - if ( this.isModal ) { - this.toggle(); - } - // Mobile users go to media page instead of modal - if (isMobile()) { - this.router.navigate([`/media/${this.guid}`]); - } - this.triggerMediaModal.emit(); + requestMediaModal() { + this.requestedMediaModal.emit(); } } diff --git a/src/app/modules/media/components/video/video.component.html b/src/app/modules/media/components/video/video.component.html index 61e415fa49..4ef2bd8cbc 100644 --- a/src/app/modules/media/components/video/video.component.html +++ b/src/app/modules/media/components/video/video.component.html @@ -10,6 +10,10 @@ (onPause)="onPause()" (onEnd)="onEnd()" (onError)="onError()" + (onCanPlay)="onCanPlay()" + (onLoadedMetadata)="loadedMetadata()" + (onLoadedData)="loadedData()" + (requestedMediaModal)="requestMediaModal()" #player ></m-video--direct-http-player> @@ -24,15 +28,18 @@ (onPause)="onPause()" (onEnd)="onEnd()" (onError)="onError()" + (onCanPlay)="onCanPlay()" + (onLoadedMetadata)="loadedMetadata()" + (onLoadedData)="loadedData()" + (requestedMediaModal)="requestMediaModal()" #player ></m-video--torrent-player> <ng-container *ngIf="playerRef"> <i *ngIf="!playerRef.isPlaying() && !playerRef.isLoading()" class="material-icons minds-video-play-icon" - (click)="playerRef.play()" + (click)="requestMediaModal()" >play_circle_outline</i> - <ng-content></ng-content> <div *ngIf="transcoding" class="minds-video-bar-top"> @@ -44,9 +51,9 @@ </div> <div class="minds-video-bar-full"> - <i class="material-icons" *ngIf="!playerRef.isLoading(); else loadingSpinner" + <i class="material-icons" (click)="playerRef.toggle()" - >{{ playerRef.isPlaying() ? 'pause' : 'play_arrow' }}</i> + >{{ playerRef.isPlaying() || playerRef.isLoading() ? 'pause' : 'play_arrow' }}</i> <ng-template #loadingSpinner> <div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div> </ng-template> @@ -55,7 +62,7 @@ <m-video--volume-slider #volumeSlider [player]="playerRef"></m-video--volume-slider> <a class="material-icons m-video-full-page minds-video--open-new" - *ngIf="guid" + *ngIf="guid && !isModal" [routerLink]="['/media', guid]" target="_blank" (click)="playerRef.pause()"> @@ -92,7 +99,7 @@ (select)="selectedQuality($event)" ></m-video--quality-selector> - <i class="material-icons" (click)="playerRef.requestFullScreen()">tv</i> + <i *ngIf="!isModal" class="material-icons" (click)="playerRef.requestFullScreen()">tv</i> </div> <div class="m-video--torrent-info" *ngIf="torrentInfo && current.type === 'torrent'"> diff --git a/src/app/modules/media/components/video/video.component.scss b/src/app/modules/media/components/video/video.component.scss index f22ec9e4dd..49c3292744 100644 --- a/src/app/modules/media/components/video/video.component.scss +++ b/src/app/modules/media/components/video/video.component.scss @@ -5,6 +5,12 @@ m-video{ position: relative; display:block; + cursor:pointer; + &:hover { + .minds-video-play-icon { + opacity: 1; + } + } video{ width:100%; @@ -43,6 +49,7 @@ } } .minds-video-play-icon{ + opacity: 0.8; display: block; text-align: center; top: 50%; @@ -51,8 +58,10 @@ position: absolute; cursor: pointer; width: 100%; + transition: opacity 0.3s cubic-bezier(.23, 1, .32, 1); @include m-theme(){ color: themed($m-white-always); + text-shadow: 0 0 3px rgba(themed($m-black-always), 0.6); } } .minds-video-bar-full{ @@ -62,7 +71,6 @@ left: 0; width: 100%; box-sizing: border-box; - //padding: $minds-padding; text-align: center; align-items: center; @include m-theme(){ @@ -75,7 +83,7 @@ color:themed($m-white-always); } } - + .mdl-spinner { margin: 0 8px; } @@ -91,9 +99,6 @@ .minds-video-bar-min{ display: none; } - .minds-video-bar-full{ - display: flex; - } } .m-video--torrent-info { diff --git a/src/app/modules/media/components/video/video.component.ts b/src/app/modules/media/components/video/video.component.ts index 4e946284de..5859c2774b 100644 --- a/src/app/modules/media/components/video/video.component.ts +++ b/src/app/modules/media/components/video/video.component.ts @@ -1,5 +1,5 @@ import { Component, ElementRef, Input, Output, EventEmitter, ViewChild, ChangeDetectorRef } from '@angular/core'; -// import { Router } from '@angular/router'; +import { Router } from '@angular/router'; import { MindsVideoProgressBar } from './progress-bar/progress-bar.component'; import { MindsVideoVolumeSlider } from './volume-slider/volume-slider.component'; @@ -8,7 +8,7 @@ import { ScrollService } from '../../../../services/ux/scroll'; import { MindsPlayerInterface } from './players/player.interface'; import { WebtorrentService } from '../../../webtorrent/webtorrent.service'; import { SOURCE_CANDIDATE_PICK_ZIGZAG, SourceCandidates } from './source-candidates'; -// import isMobile from '../../../../helpers/is-mobile'; +import isMobile from '../../../../helpers/is-mobile'; @Component({ selector: 'm-video', @@ -24,9 +24,14 @@ export class MindsVideoComponent { @Input() log: string | number; @Input() muted: boolean = false; @Input() poster: string = ''; + @Input() isModal: boolean = false; @Output('finished') finished: EventEmitter<any> = new EventEmitter(); - // @Output() triggerMediaModal: EventEmitter<any> = new EventEmitter(); + + @Output() videoMetadataLoaded: EventEmitter<any> = new EventEmitter(); + @Output() videoLoaded: EventEmitter<any> = new EventEmitter(); + @Output() videoCanPlay: EventEmitter<any> = new EventEmitter(); + @Output() requestedMediaModal: EventEmitter<any> = new EventEmitter(); @ViewChild('progressBar', { static: false }) progressBar: MindsVideoProgressBar; @ViewChild('volumeSlider', { static: false }) volumeSlider: MindsVideoVolumeSlider; @@ -55,6 +60,7 @@ export class MindsVideoComponent { playedOnce: boolean = false; playCount: number = -1; playCountDisabled: boolean = false; + modalHover: boolean = false; current: { type: 'torrent' | 'direct-http', src: string }; protected candidates: SourceCandidates = new SourceCandidates(); @@ -73,7 +79,7 @@ export class MindsVideoComponent { public client: Client, protected webtorrent: WebtorrentService, protected cd: ChangeDetectorRef, - // private router: Router, + private router: Router, ) { } ngOnInit() { @@ -163,6 +169,9 @@ export class MindsVideoComponent { } onMouseLeave() { + if (this.modalHover) { + return; + } this.progressBar.stopSeeker(); this.progressBar.disableKeyControls(); } @@ -228,6 +237,23 @@ export class MindsVideoComponent { this.playerRef.toggle(); } + loadedMetadata() { + const dimensions = { + 'width' : this.playerRef.getPlayer().videoWidth, + 'height' : this.playerRef.getPlayer().videoHeight + }; + + this.videoMetadataLoaded.emit({dimensions: dimensions}); + } + + loadedData() { + this.videoLoaded.emit(); + } + + onCanPlay() { + this.videoCanPlay.emit(); + } + // Sources async fallback() { @@ -327,21 +353,23 @@ export class MindsVideoComponent { } } - // requestMediaModal() { - // // this.playerRef.pause(); //no need anymore - // // Mobile users go to media page instead of modal - // if (isMobile()) { - // this.router.navigate([`/media/${this.guid}`]); - // } + requestMediaModal() { + // Don't reopen modal if you're already on it + if (this.isModal) { + this.toggle(); + } + // Mobile users go to media page instead of modal + if (isMobile()) { + this.router.navigate([`/media/${this.guid}`]); + } - // this.triggerMediaModal.emit(); - // } + this.requestedMediaModal.emit(); + } detectChanges() { this.cd.markForCheck(); this.cd.detectChanges(); } - } export { VideoAds } from './ads.component'; diff --git a/src/app/modules/media/components/video/volume-slider/volume-slider.component.html b/src/app/modules/media/components/video/volume-slider/volume-slider.component.html index e02d9aec99..cbf99b8979 100644 --- a/src/app/modules/media/components/video/volume-slider/volume-slider.component.html +++ b/src/app/modules/media/components/video/volume-slider/volume-slider.component.html @@ -5,6 +5,7 @@ <i class="material-icons" *ngIf="element.volume >= 0.9 && !element.muted" (click)="element.muted = true">volume_up</i> <div class="m-video--volume-control"> + <div class="m-video--volume-control--background"></div> <input type="range" [(ngModel)]="element.volume" class="m-video--volume-control-selector" min="0" max="1" step="0.05" /> </div> </div> diff --git a/src/app/modules/media/components/video/volume-slider/volume-slider.component.scss b/src/app/modules/media/components/video/volume-slider/volume-slider.component.scss index e79ab7ccb3..84c21c189b 100644 --- a/src/app/modules/media/components/video/volume-slider/volume-slider.component.scss +++ b/src/app/modules/media/components/video/volume-slider/volume-slider.component.scss @@ -61,6 +61,9 @@ m-video--volume-slider{ position: absolute; top: 35px; right: -18px; + @include m-theme(){ + background-color: themed($m-white-always); + } } .m-video--volume-control{ @@ -72,6 +75,14 @@ m-video--volume-slider{ height: 80px; position: absolute; margin: 0; + } + + .m-video--volume-control--background { + width:40px; + height:72px; + position: absolute; + left: calc(50% - 20px); + bottom: 16px; @include m-theme(){ background-color: rgba(themed($m-black-always), 0.4); } diff --git a/src/app/modules/media/modal/modal.component.html b/src/app/modules/media/modal/modal.component.html index 4025d827bd..5bd11485f7 100644 --- a/src/app/modules/media/modal/modal.component.html +++ b/src/app/modules/media/modal/modal.component.html @@ -1,15 +1,17 @@ -<div class="m-mediaModal__exoWrapper"> - <div class="m-mediaModal__wrapper" +<div class="m-mediaModal__wrapper"> + <div class="m-mediaModal__theater" (click)="clickedModal($event)" > <div class="m-mediaModal m-mediaModal__clearFix" [style.width]="modalWidth + 'px'" [style.height]="stageHeight + 'px'" > - <!-- This is the element that goes into fullscreen --> + <!-- The stageWrapper is the element that goes into fullscreen --> <div class="m-mediaModal__stageWrapper" [style.width]="stageWidth + 'px'" [style.line-height]="stageHeight + 'px'" + (mouseenter)="onMouseEnterStage()" + (mouseleave)="onMouseLeaveStage()" > <!-- LOADING PANEL --> <div class="m-mediaModal__loadingPanel" *ngIf="inProgress"> @@ -22,28 +24,28 @@ > <!-- MEDIA: IMAGE --> - <div class="m-mediaModal__imageWrapper" - *ngIf="entity.modal_subtype === 'image'" + <div class="m-mediaModal__mediaWrapper m-mediaModal__mediaWrapper--image" + *ngIf="entity.custom_type === 'batch'" [style.width]="mediaWidth + 'px'" [style.height]="mediaHeight + 'px'" > <img [src]="thumbnail" - (load)="inProgress = !inProgress" - + (load)="this.inProgress = false" [style.height]="entity.height + 'px'" [style.width]="entity.width + 'px'" /> </div> <!-- MEDIA: VIDEO --> - <div class="m-mediaModal__videoWrapper" - *ngIf="entity.modal_subtype === 'video'" + <div class="m-mediaModal__mediaWrapper m-mediaModal__mediaWrapper--video" + *ngIf="entity.custom_type === 'video'" [style.width]="mediaWidth + 'px'" [style.height]="mediaHeight + 'px'" > <m-video [style.height]="entity.height + 'px'" [style.width]="entity.width + 'px'" + [isModal]="true" [autoplay]="true" [muted]="false" [poster]="entity.custom_data.thumbnail_src" @@ -51,7 +53,7 @@ [guid]="entity.custom_data.guid" [playCount]="entity['play:count']" [torrent]="[{ res: '360', key: entity.custom_data.guid + '/360.mp4' }]" - #player + (videoCanPlay)="this.inProgress = false" > <video-ads [player]="player" *ngIf="entity.monetized"></video-ads> </m-video> @@ -67,7 +69,7 @@ <!-- TITLE: FULLSCREEN --> <span class="m-mediaModal__overlayTitle m-mediaModal__overlayTitle--fullscreen" *ngIf="isFullscreen"> <a [routerLink]="['/', entity.ownerObj.username]"> - <img class="avatar" [hovercard]="entity.ownerObj.guid" [src]="minds.cdn_url + 'icon/' + entity.ownerObj.guid + '/small/' + getOwnerIconTime()" class="mdl-shadow--2dp"/> + <img class="avatar" [src]="minds.cdn_url + 'icon/' + entity.ownerObj.guid + '/small/' + getOwnerIconTime()" class="mdl-shadow--2dp"/> <span title={{entity.ownerObj.name}}>{{entity.ownerObj.name}}</span> </a> <div class="m-mediaModal__overlayTitleSeparator"></div> diff --git a/src/app/modules/media/modal/modal.component.scss b/src/app/modules/media/modal/modal.component.scss index 1223a0dc8b..ed1fb0d717 100644 --- a/src/app/modules/media/modal/modal.component.scss +++ b/src/app/modules/media/modal/modal.component.scss @@ -9,9 +9,9 @@ m-overlay-modal { } .m-overlay-modal.m-overlayModal--media { - min-width: 1060px; // ! Need to output this to parent + // min-width: 1060px; // should output this to parent min-height: 100%; - position: fixed; //should be absolute? + position: fixed; //? should be absolute? top: 0; right: 0; left: 0; @@ -48,28 +48,25 @@ m-overlay-modal { vertical-align: middle; width: 100%; - .m-mediaModal__exoWrapper { + .m-mediaModal__wrapper { position: static; margin: 20px; display:inline-block; text-align: left; - .m-mediaModal__wrapper { + .m-mediaModal__theater { position: relative; @include m-theme(){ box-shadow: 0 12px 24px rgba(themed($m-black-always), 0.3); } - // .m-mediaModal { // has inline width/height - - // } + // .m-mediaModal {} // has inline width/height } } } } } -.m-mediaModal__loadingPanel { - .mdl-spinner { +.m-mediaModal__loadingPanel .mdl-spinner { position: absolute; top: unquote("-webkit-calc(50% - 14px)"); left: unquote("-webkit-calc(50% - 14px)"); @@ -77,7 +74,6 @@ m-overlay-modal { left: unquote("-moz-calc(50% - 14px)"); top: unquote("calc(50% - 14px)"); left: unquote("calc(50% - 14px)"); - } } .m-mediaModal__stageWrapper { // Has inline width/line-height @@ -95,13 +91,17 @@ m-overlay-modal { .m-mediaModal__overlayContainer { opacity: 1; } + .minds-video-bar-full { + visibility: visible; + opacity: 1; + } } } .m-mediaModal__stage { display: flex; align-items:center; - font-size:0; + font-size: 0; height: 100%; min-height: 402px; position: relative; @@ -109,27 +109,55 @@ m-overlay-modal { width: 100%; } -.m-mediaModal__imageWrapper { // Has inline width/height +.m-mediaModal__mediaWrapper { // Has inline width/height display: inline-block; margin: 0 auto; vertical-align: middle; - img { // Has inline width/height + img, m-video { // Has inline width/height display: inline-block; max-height: 100%; max-width: 100%; vertical-align: top; } -} + m-video { + position: static; + + .minds-video-bar-full { + visibility: hidden; + display: flex; + opacity: 0; + transition: opacity .3s; + + .m-video--progress-bar { + padding-right: 0; + } + + .m-video--volume-control-wrapper { + margin-right: 16px; + } + } + .minds-video-play-icon { + transform: none; + width: auto; + top: unquote("-webkit-calc(50% - 50px)"); + left: unquote("-webkit-calc(50% - 50px)"); + top: unquote("-moz-calc(50% - 50px)"); + left: unquote("-moz-calc(50% - 50px)"); + top: unquote("calc(50% - 50px)"); + left: unquote("calc(50% - 50px)"); + } + m-video--progress-bar { + .seeker-ball { + top: 4px; + } + .progress-bar { + margin-right: 8px; + } + } + } -.m-mediaModal__videoWrapper { - // TODO - this was from fb - // bottom: 40px; - // overflow: hidden; - // position: absolute; - // top: 40px; - // width: 100%; } .m-mediaModal__overlayContainer { @@ -155,13 +183,13 @@ m-overlay-modal { overflow: hidden; text-overflow: ellipsis; margin-right: 36px; - cursor: pointer; @include m-theme(){ color: themed($m-white-always); } .m-mediaModal__overlayTitle { text-decoration: none; + cursor: pointer; > * { &:not(.m-mediaModal__overlayTitleSeparator) { diff --git a/src/app/modules/media/modal/modal.component.ts b/src/app/modules/media/modal/modal.component.ts index 18d9ccc966..a467d98698 100644 --- a/src/app/modules/media/modal/modal.component.ts +++ b/src/app/modules/media/modal/modal.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, Input, HostListener } from '@angular/core'; +import { Component, OnInit, OnDestroy, Input, HostListener, ViewChild } from '@angular/core'; import { Location } from '@angular/common'; import { Router, Event, NavigationStart } from '@angular/router'; import { trigger, state, style, animate, transition } from '@angular/animations'; @@ -6,15 +6,13 @@ import { Subscription } from 'rxjs'; import { Session } from '../../../services/session'; import { OverlayModalService } from '../../../services/ux/overlay-modal'; import { AnalyticsService } from '../../../services/analytics'; -import isMobile from '../../../helpers/is-mobile'; -import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet'; - +import { MindsVideoComponent } from '../components/video/video.component'; @Component({ selector: 'm-media--modal', templateUrl: 'modal.component.html', animations: [ - // Fade in image when it's done loading + // Fade in media when done loading trigger('simpleFadeAnimation', [ state('in', style({opacity: 1})), transition(':enter', [ @@ -22,18 +20,17 @@ import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet'; animate(800) ]) ]) -] + ] }) - export class MediaModalComponent implements OnInit, OnDestroy { minds = window.Minds; entity: any = {}; inProgress: boolean = true; + isFullscreen: boolean = false; navigatedAway: boolean = false; fullscreenHovering: boolean = false; // Used for fullscreen button transformation - isTablet: boolean = false; screenWidth: number; screenHeight: number; @@ -47,48 +44,54 @@ export class MediaModalComponent implements OnInit, OnDestroy { maxStageWidth: number; maxHeight: number; minStageHeight: number; + minStageWidth: number; title: string = ''; thumbnail: string = ''; boosted: boolean = false; - isOpen: boolean = false; // Used for backdrop click detection hack - isOpenTimeout: any = null; // Used for backdrop click detection hack + // Used for backdrop click detection hack + isOpen: boolean = false; + isOpenTimeout: any = null; routerSubscription: Subscription; @Input('entity') set data(entity) { this.entity = entity; + this.entity.width = 0; + this.entity.height = 0; } + + @ViewChild( MindsVideoComponent, { static: false }) videoComponent: MindsVideoComponent; + constructor( public session: Session, + public analyticsService: AnalyticsService, private overlayModal: OverlayModalService, private router: Router, private location: Location, - public analyticsService: AnalyticsService ) { } ngOnInit() { + // Prevent dismissal of modal when it's just been opened this.isOpenTimeout = setTimeout(() => this.isOpen = true, 50); this.analyticsService.send('pageview', {url: `/media/${this.entity.entity_guid}?ismodal=true`}); - // this.isTablet = isMobileOrTablet() && !isMobile(); - - this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.entity_guid}/xlarge`; this.boosted = this.entity.boosted || this.entity.p2p_boosted; // Set title if ( !this.entity.title ) { if ( !this.entity.message ) { - // ? is there ever a case where there is a message but no title? this.title = `${this.entity.ownerObj.name}'s post`; } else { this.title = this.entity.message; } + } else { + this.title = this.entity.title; } // Change the url to point to media page so user can easily share link @@ -108,119 +111,168 @@ export class MediaModalComponent implements OnInit, OnDestroy { // Go to the intended destination this.router.navigate([event.url]); - this.overlayModal.dismiss(); + this.dismissModal(); } } }); - // Dimensions are in px - this.entity.width = this.entity.custom_data[0].width; - this.entity.height = this.entity.custom_data[0].height; - this.entity.aspectRatio = this.entity.width / this.entity.height; - this.entity.isPortrait = this.entity.aspectRatio < 1; - console.log(this.entity); + // TO DO? handle giant screens by setting max on screenwidth and height and window size? + // Set fixed dimensions (i.e. those that don't change when window changes) this.screenWidth = screen.width; this.screenHeight = screen.height; - this.contentWidth = Math.round(this.screenWidth * 0.25); - this.minStageHeight = Math.round((this.screenHeight * 0.6) - 20); // 520px - this.maxHeight = Math.round(this.screenHeight * 0.8); // ~723px - this.stageWidthPaddingThreshold = this.screenWidth * 0.4 + 44; // 620 + this.contentWidth = Math.max(Math.round(this.screenWidth * 0.25), 300); + this.minStageHeight = Math.round((this.screenHeight * 0.6) - 20); + this.maxHeight = Math.round(this.screenHeight * 0.8); + this.stageWidthPaddingThreshold = this.screenWidth * 0.4 + 44; + this.minStageWidth = this.stageWidthPaddingThreshold + 40; + + if (this.entity.custom_type === 'batch') { + // Image + this.entity.width = this.entity.custom_data[0].width; + this.entity.height = this.entity.custom_data[0].height; + this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.entity_guid}/xlarge`; + } else { + // Video + this.entity.width = this.entity.custom_data.dimensions.width; + this.entity.height = this.entity.custom_data.dimensions.height; + this.thumbnail = this.entity.custom_data.thumbnail_src; // Not currently used + } + this.entity.aspectRatio = this.entity.width / this.entity.height; this.calculateDimensions(); } - // Change media containers' height/width on window resize + // Recalculate height/width when window resizes @HostListener('window:resize', ['$resizeEvent']) onResize(resizeEvent) { this.calculateDimensions(); } calculateDimensions() { - // This can be simplified but it works const windowWidth: number = window.innerWidth; const windowHeight: number = window.innerHeight; if ( !this.isFullscreen ) { - // Set stageHeight as % of windowHeight - this.stageHeight = Math.round(windowHeight * 0.94); - - // Ensure stageHeight is between min and max (max. is % of screenHeight) - if ( this.stageHeight > this.maxHeight ) { - this.stageHeight = this.maxHeight; - } else if ( this.stageHeight < this.minStageHeight ) { - // Stage height should be no lower than minimum - this.stageHeight = this.minStageHeight; - } - - // Set image height as hight as stageHeight but no larger than intrinsic height - if (this.entity.height >= this.stageHeight) { - this.mediaHeight = this.stageHeight; - } else { - this.mediaHeight = this.entity.height; - this.stageHeight = this.minStageHeight; - } - this.mediaWidth = Math.round(this.mediaHeight * this.entity.aspectRatio); - - // If width is lower than threshold, add extra padding - const minStageWidth = this.stageWidthPaddingThreshold + 40; // 660 - // TODO rename minStageWidth. Here it's 660. but technically also it goes down to 621 - - let maxStageWidth: number = windowWidth - 400; - // Don't let maxStageWidth go below minStageWidth - if ( maxStageWidth <= minStageWidth ) { - maxStageWidth = minStageWidth; - } - - // Reset mediaWidth and stageWidth if needed - // If image is too wide, set width to max and rescale imageHeight and stageHeight - if ( this.mediaWidth >= maxStageWidth ) { - this.mediaWidth = maxStageWidth; - this.stageWidth = maxStageWidth; - - // Rescale media based on new mediaWidth - this.mediaHeight = Math.round(this.mediaWidth / this.entity.aspectRatio); - if ( this.mediaHeight >= this.minStageHeight ) { - this.stageHeight = this.mediaHeight; - } else { - this.stageHeight = this.minStageHeight; - } - } else if ( this.mediaWidth > this.stageWidthPaddingThreshold ) { + // Set heights as tall as possible + this.setStageHeightWithinRange(window.innerHeight); + this.setHeights(); + + // After heights are set, check that scaled width isn't too wide or narrow + this.maxStageWidth = Math.max(windowWidth - this.contentWidth - 40, this.minStageWidth); + if ( this.mediaWidth >= this.maxStageWidth ) { + // Too wide :( + this.rescaleHeightsForMaxWidth(); + } else if ( this.mediaWidth > this.stageWidthPaddingThreshold ) { + // Not too wide and not too narrow :) this.stageWidth = this.mediaWidth; } else { - // When stageWidth goes below threshold, increase horizontal padding up to 20px minimum - this.stageWidth = minStageWidth; + // Too narrow :( + // If black stage background is visible on left/right, each strip should be at least 20px wide + this.stageWidth = this.minStageWidth; + + // Continue to resize height after reaching min width + this.handleNarrowWindow(window.innerWidth); } - } else { // Fullscreen + // If black stage background is visible on top/bottom, each strip should be at least 20px high + const heightDiff = this.stageHeight - this.mediaHeight; + if ( 0 < heightDiff && heightDiff <= 40){ + this.stageHeight += 40; + } + + } else { // isFullscreen this.stageWidth = windowWidth; this.stageHeight = windowHeight; // Set mediaHeight as tall as possible but not taller than instrinsic height this.mediaHeight = this.entity.height < windowHeight ? this.entity.height : windowHeight; - this.mediaWidth = Math.round(this.mediaHeight * this.entity.aspectRatio); + this.mediaWidth = this.scaleWidth(); - // Width was too wide, need to rescale if ( this.mediaWidth > windowWidth ) { + // Width was too wide, need to rescale heights so width fits this.mediaWidth = windowWidth; - this.mediaHeight = Math.round(this.mediaWidth / this.entity.aspectRatio); + this.mediaHeight = this.scaleHeight(); } } - this.modalWidth = this.stageWidth + this.contentWidth; } - getOwnerIconTime() { - const session = this.session.getLoggedInUser(); - if (session && session.guid === this.entity.ownerObj.guid) { - return session.icontime; + + setStageHeightWithinRange(windowHeight) { + // Initialize stageHeight as % of windowHeight + this.stageHeight = Math.round(windowHeight * 0.94); + + // Ensure stageHeight is not taller than max (a % of screenHeight) + this.stageHeight = Math.min(this.stageHeight, this.maxHeight); + + // Ensure stageHeight is not shorter than min (a % of screenHeight) + this.stageHeight = Math.max(this.stageHeight, this.minStageHeight); + + // Scale width according to aspect ratio + this.mediaWidth = this.scaleWidth(); + } + + + setHeights() { + // Set mediaHeight as tall as stage but no larger than intrinsic height + if (this.entity.height >= this.stageHeight) { + // Fit media inside stage + this.mediaHeight = this.stageHeight; } else { - return this.entity.ownerObj.icontime; + // Media is shorter than stage + this.mediaHeight = this.entity.height; + this.stageHeight = this.minStageHeight; + } + + // Scale width according to aspect ratio + this.mediaWidth = this.scaleWidth(); + } + + rescaleHeightsForMaxWidth() { + // Media is too wide, set width to max and rescale heights + this.mediaWidth = this.maxStageWidth; + this.stageWidth = this.maxStageWidth; + + this.mediaHeight = this.scaleHeight(); + this.stageHeight = Math.max(this.mediaHeight, this.minStageHeight); + } + + handleNarrowWindow(windowWidth) { + // When at minStageWidth and windowWidth falls below threshold, + // shrink vertically until it hits minStageHeight + + // When window is narrower than this, start to shrink height + const verticalShrinkWidthThreshold = this.mediaWidth + this.contentWidth + 82; + + const widthDiff = verticalShrinkWidthThreshold - windowWidth; + // Is window narrow enough to start shrinking vertically? + if ( widthDiff >= 1 ) { + // What mediaHeight would be if it shrunk proportionally to difference in width? + const mediaHeightPreview = Math.round((this.mediaWidth - widthDiff) / this.entity.aspectRatio); + + // Shrink media if mediaHeight is still above min + if (mediaHeightPreview > this.minStageHeight) { + this.mediaWidth -= widthDiff; + this.mediaHeight = this.scaleHeight(); + this.stageHeight = this.mediaHeight; + } else { + this.stageHeight = this.minStageHeight; + this.mediaHeight = Math.min(this.minStageHeight, this.entity.height); + this.mediaWidth = this.scaleWidth(); + } } } + scaleHeight() { + return Math.round(this.mediaWidth / this.entity.aspectRatio); + } + scaleWidth() { + return Math.round(this.mediaHeight * this.entity.aspectRatio); + } + // Listen for fullscreen change event in case user enters/exits full screen without clicking button @HostListener('document:fullscreenchange', ['$event']) @HostListener('document:webkitfullscreenchange', ['$event']) @@ -228,7 +280,10 @@ export class MediaModalComponent implements OnInit, OnDestroy { @HostListener('document:MSFullscreenChange', ['$event']) onFullscreenChange(event) { this.calculateDimensions(); - if (!document.fullscreenElement && !document['webkitFullScreenElement'] && !document['mozFullScreenElement'] && !document['msFullscreenElement']) { + if ( !document.fullscreenElement && + !document['webkitFullScreenElement'] && + !document['mozFullScreenElement'] && + !document['msFullscreenElement'] ) { this.isFullscreen = false; } else { this.isFullscreen = true; @@ -241,7 +296,10 @@ export class MediaModalComponent implements OnInit, OnDestroy { this.calculateDimensions(); // If fullscreen is not already enabled - if (!document['fullscreenElement'] && !document['webkitFullScreenElement'] && !document['mozFullScreenElement'] && !document['msFullscreenElement']) { + if ( !document['fullscreenElement'] && + !document['webkitFullScreenElement'] && + !document['mozFullScreenElement'] && + !document['msFullscreenElement'] ) { // Request full screen if (elem.requestFullscreen) { elem.requestFullscreen(); @@ -257,7 +315,7 @@ export class MediaModalComponent implements OnInit, OnDestroy { } // If fullscreen is already enabled, exit it - if (document.exitFullscreen) { + if ( document.exitFullscreen ) { document.exitFullscreen(); } else if (document['webkitExitFullscreen']) { document['webkitExitFullscreen'](); @@ -269,21 +327,41 @@ export class MediaModalComponent implements OnInit, OnDestroy { this.isFullscreen = false; } + // Make sure progress bar updates when video controls are visible + onMouseEnterStage() { + this.videoComponent.modalHover = true; + this.videoComponent.onMouseEnter(); + } + + // Stop updating progress bar when controls aren't visible + onMouseLeaveStage() { + this.videoComponent.modalHover = false; + this.videoComponent.onMouseLeave(); + } + + getOwnerIconTime() { + const session = this.session.getLoggedInUser(); + if (session && session.guid === this.entity.ownerObj.guid) { + return session.icontime; + } else { + return this.entity.ownerObj.icontime; + } + } + // Dismiss modal when backdrop is clicked and modal is open @HostListener('document:click', ['$event']) clickedBackdrop($event){ if (this.isOpen) { - this.dismiss(); + this.dismissModal(); } } - // Don't dismiss modal when modal is clicked + // Don't dismiss modal when clicking somewhere other than backdrop clickedModal($event) { $event.preventDefault(); $event.stopPropagation(); - console.log('***modal clicked...dont dismiss'); } - dismiss() { + dismissModal() { this.overlayModal.dismiss(); } @@ -299,7 +377,4 @@ export class MediaModalComponent implements OnInit, OnDestroy { this.location.replaceState(this.entity.modal_source_url); } } - - } - -- GitLab