From 2d6b25a0a4785d73c530438dbbc72ce5f60304c5 Mon Sep 17 00:00:00 2001 From: morethanwords Date: Tue, 1 Sep 2020 15:53:46 +0300 Subject: [PATCH] Media viewer improvements: Resizing content Mobile video player Minor fixes --- src/index.hbs | 2 +- src/lib/appManagers/appMediaViewer.ts | 84 +++++++++++++------------ src/lib/appManagers/appSidebarRight.ts | 16 +++-- src/lib/mediaPlayer.ts | 81 +++++++++++++++++++++--- src/scss/partials/_ckin.scss | 16 ++++- src/scss/partials/_mediaViewer.scss | 85 +++++++++++++++++++++++--- src/scss/style.scss | 1 + 7 files changed, 219 insertions(+), 66 deletions(-) diff --git a/src/index.hbs b/src/index.hbs index 071e0147..10588436 100644 --- a/src/index.hbs +++ b/src/index.hbs @@ -144,7 +144,6 @@
-
@@ -164,6 +163,7 @@
+
diff --git a/src/lib/appManagers/appMediaViewer.ts b/src/lib/appManagers/appMediaViewer.ts index 442edd76..7efe5676 100644 --- a/src/lib/appManagers/appMediaViewer.ts +++ b/src/lib/appManagers/appMediaViewer.ts @@ -18,6 +18,7 @@ import appMediaPlaybackController from "../../components/appMediaPlaybackControl // TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию // TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода) +// TODO: видео в мобильной вёрстке, если показываются элементы управления: если свайпнуть в сторону, то элементы вернутся на место, т.е. прыгнут - это не ок, надо бы замаскировать class SwipeHandler { private xDown: number; @@ -205,6 +206,9 @@ export class AppMediaViewer { if(touchSupport) { const swipeHandler = new SwipeHandler(this.wholeDiv, (xDiff, yDiff) => { + if(VideoPlayer.isFullScreen()) { + return; + } //console.log(xDiff, yDiff); const percents = Math.abs(xDiff) / appPhotosManager.windowW; @@ -358,7 +362,7 @@ export class AppMediaViewer { } */ let aspecter: HTMLDivElement; - if(target instanceof HTMLImageElement || target instanceof HTMLVideoElement) { + if(target instanceof HTMLImageElement || target instanceof HTMLVideoElement || target.tagName == 'DIV') { if(mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter')) { aspecter = mover.firstElementChild as HTMLDivElement; @@ -491,18 +495,29 @@ export class AppMediaViewer { if(aspecter) { aspecter.style.borderRadius = borderRadius; - aspecter.append(mediaElement); + + if(mediaElement) { + aspecter.append(mediaElement); + } } mediaElement = mover.querySelector('video, img'); - if(mediaElement instanceof HTMLImageElement && src) { - await new Promise((resolve, reject) => { - mediaElement.addEventListener('load', resolve); + if(mediaElement instanceof HTMLImageElement) { + mediaElement.classList.add('thumbnail'); + if(!aspecter) { + mediaElement.style.width = containerRect.width + 'px'; + mediaElement.style.height = containerRect.height + 'px'; + } - if(src) { - mediaElement.src = src; - } - }); + if(src) { + await new Promise((resolve, reject) => { + mediaElement.addEventListener('load', resolve); + + if(src) { + mediaElement.src = src; + } + }); + } }/* else if(mediaElement instanceof HTMLVideoElement && mediaElement.firstElementChild && ((mediaElement.firstElementChild as HTMLSourceElement).src || src)) { await new Promise((resolve, reject) => { mediaElement.addEventListener('loadeddata', resolve); @@ -551,7 +566,7 @@ export class AppMediaViewer { setTimeout(() => { mover.innerHTML = ''; mover.classList.remove('moving', 'active', 'hiding'); - mover.style.display = 'none'; + mover.style.cssText = 'display: none;'; deferred.resolve(); }, delay); @@ -669,7 +684,8 @@ export class AppMediaViewer { private removeCenterFromMover(mover: HTMLDivElement) { if(mover.classList.contains('center')) { - const rect = mover.getBoundingClientRect(); + //const rect = mover.getBoundingClientRect(); + const rect = this.content.container.getBoundingClientRect(); mover.style.transform = `translate(${rect.left}px,${rect.top}px)`; mover.classList.remove('center'); void mover.offsetLeft; // reflow @@ -818,7 +834,7 @@ export class AppMediaViewer { this.log('openMedia doc:', message); const media = message.media.photo || message.media.document || message.media.webpage.document || message.media.webpage.photo; - const isVideo = (media as MTDocument).type == 'video'; + const isVideo = (media as MTDocument).type == 'video' || (media as MTDocument).type == 'gif'; const isFirstOpen = !this.peerID; if(isFirstOpen) { @@ -946,7 +962,11 @@ export class AppMediaViewer { //video.src = ''; video.setAttribute('playsinline', ''); - video.autoplay = true; + + if(isSafari) { + video.autoplay = true; + } + if(media.type == 'gif') { video.muted = true; video.autoplay = true; @@ -958,7 +978,7 @@ export class AppMediaViewer { } const canPlayThrough = new Promise((resolve) => { - video.addEventListener('canplaythrough', resolve, {once: true}); + video.addEventListener('canplay', resolve, {once: true}); }); const createPlayer = () => { @@ -1039,29 +1059,9 @@ export class AppMediaViewer { this.updateMediaSource(mover, url, 'video'); } else { - //const promise = new Promise((resolve) => video.addEventListener('loadeddata', resolve, {once: true})); renderImageFromUrl(video, url); - - //await promise; - /* const first = div.firstElementChild as HTMLImageElement; - if(!(first instanceof HTMLVideoElement) && first) { - first.remove(); - } - - if(!video.parentElement) { - div.prepend(video); - } */ } - // я хз что это такое, видео появляются просто чёрными и не включаются без этого кода снизу - /* source.remove(); - window.requestAnimationFrame(() => { - window.requestAnimationFrame(() => { - //parent.append(video); - video.append(source); - }); - }); */ - createPlayer(); }); @@ -1081,8 +1081,8 @@ export class AppMediaViewer { //if(wasActive) return; //return; - let load = () => { - let cancellablePromise = appPhotosManager.preloadPhoto(media.id, size); + const load = () => { + const cancellablePromise = appPhotosManager.preloadPhoto(media.id, size); onAnimationEnd.then(() => { this.preloader.attach(mover, true, cancellablePromise); }); @@ -1094,12 +1094,19 @@ export class AppMediaViewer { ///////this.log('indochina', blob); - let url = media.url; + const url = media.url; if(target instanceof SVGSVGElement) { this.updateMediaSource(target, url, 'img'); this.updateMediaSource(mover, url, 'img'); + + /* const imgs = mover.querySelectorAll('img'); + if(imgs && imgs.length) { + imgs.forEach(img => { + img.classList.remove('thumbnail'); // может здесь это вообще не нужно + }); + } */ } else { - let div = mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter') ? mover.firstElementChild : mover; + const div = mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter') ? mover.firstElementChild : mover; let image = div.firstElementChild as HTMLImageElement; if(!image || image.tagName != 'IMG') { image = new Image(); @@ -1108,6 +1115,7 @@ export class AppMediaViewer { //this.log('will renderImageFromUrl:', image, div, target); renderImageFromUrl(image, url, () => { + image.classList.remove('thumbnail'); // может здесь это вообще не нужно div.append(image); }); } diff --git a/src/lib/appManagers/appSidebarRight.ts b/src/lib/appManagers/appSidebarRight.ts index 5deca80f..cdfbb79b 100644 --- a/src/lib/appManagers/appSidebarRight.ts +++ b/src/lib/appManagers/appSidebarRight.ts @@ -226,20 +226,24 @@ export class AppSidebarRight extends SidebarSlider { }); this.sharedMedia.contentMedia.addEventListener('click', (e) => { - let target = e.target as HTMLDivElement; + const target = e.target as HTMLDivElement; - let messageID = +target.dataset.mid; + const messageID = +target.dataset.mid; if(!messageID) { this.log.warn('no messageID by click on target:', target); return; } - let message = appMessagesManager.getMessage(messageID); + const message = appMessagesManager.getMessage(messageID); - let ids = Object.keys(this.mediaDivsByIDs).map(k => +k).sort((a, b) => a - b); - let idx = ids.findIndex(i => i == messageID); + const ids = Object.keys(this.mediaDivsByIDs).map(k => +k).sort((a, b) => a - b); + const idx = ids.findIndex(i => i == messageID); - let targets = ids.map(id => ({element: this.mediaDivsByIDs[id]/* .firstElementChild */ as HTMLElement, mid: id})); + const targets = ids.map(id => { + const element = this.mediaDivsByIDs[id] as HTMLElement; + //element = element.querySelector('img') || element; + return {element, mid: id}; + }); appMediaViewer.openMedia(message, target, false, this.sidebarEl, targets.slice(idx + 1).reverse(), targets.slice(0, idx).reverse(), true); }); diff --git a/src/lib/mediaPlayer.ts b/src/lib/mediaPlayer.ts index a2243707..179acc4c 100644 --- a/src/lib/mediaPlayer.ts +++ b/src/lib/mediaPlayer.ts @@ -2,6 +2,8 @@ import { cancelEvent } from "./utils"; import { touchSupport } from "./config"; import appMediaPlaybackController from "../components/appMediaPlaybackController"; +type SUPEREVENT = MouseEvent | TouchEvent; + export class ProgressLine { public container: HTMLDivElement; protected filled: HTMLDivElement; @@ -46,17 +48,17 @@ export class ProgressLine { this.events = events; } - onMouseMove = (e: MouseEvent) => { + onMouseMove = (e: SUPEREVENT) => { this.mousedown && this.scrub(e); }; - onMouseDown = (e: MouseEvent) => { + onMouseDown = (e: SUPEREVENT) => { this.scrub(e); this.mousedown = true; this.events?.onMouseDown && this.events.onMouseDown(e); }; - onMouseUp = (e: MouseEvent) => { + onMouseUp = (e: SUPEREVENT) => { this.mousedown = false; this.events?.onMouseUp && this.events.onMouseUp(e); }; @@ -65,6 +67,12 @@ export class ProgressLine { this.container.addEventListener('mousemove', this.onMouseMove); this.container.addEventListener('mousedown', this.onMouseDown); this.container.addEventListener('mouseup', this.onMouseUp); + + if(touchSupport) { + this.container.addEventListener('touchmove', this.onMouseMove); + this.container.addEventListener('touchstart', this.onMouseDown); + this.container.addEventListener('touchend', this.onMouseUp); + } } public setProgress(scrubTime: number) { @@ -78,8 +86,16 @@ export class ProgressLine { this.filled.style.transform = 'scaleX(' + scaleX + ')'; } - protected scrub(e: MouseEvent) { - const scrubTime = e.offsetX / this.container.offsetWidth * this.duration; + protected scrub(e: SUPEREVENT) { + let offsetX: number; + if(e instanceof TouchEvent) { + const rect = (e.target as HTMLElement).getBoundingClientRect(); + offsetX = e.targetTouches[0].pageX - rect.left; + } else { + offsetX = e.offsetX; + } + + const scrubTime = offsetX / this.container.offsetWidth * this.duration; this.setFilled(scrubTime); @@ -92,6 +108,12 @@ export class ProgressLine { this.container.removeEventListener('mousedown', this.onMouseDown); this.container.removeEventListener('mouseup', this.onMouseUp); + if(touchSupport) { + this.container.removeEventListener('touchmove', this.onMouseMove); + this.container.removeEventListener('touchstart', this.onMouseDown); + this.container.removeEventListener('touchend', this.onMouseUp); + } + this.events = {}; } } @@ -119,7 +141,7 @@ export class MediaProgressLine extends ProgressLine { this.setSeekMax(); this.setListeners(); this.setHandlers({ - onMouseDown: (e: MouseEvent) => { + onMouseDown: (e: SUPEREVENT) => { //super.onMouseDown(e); //Таймер для того, чтобы стопать видео, если зажал мышку и не отпустил клик @@ -133,7 +155,7 @@ export class MediaProgressLine extends ProgressLine { }, 150); }, - onMouseUp: (e: MouseEvent) => { + onMouseUp: (e: SUPEREVENT) => { //super.onMouseUp(e); if(this.stopAndScrubTimeout) { @@ -379,8 +401,41 @@ export default class VideoPlayer { video.addEventListener('click', () => { if(!touchSupport) { this.togglePlay(); + return; } }); + + if(touchSupport) { + let showControlsTimeout = 0; + + const t = () => { + showControlsTimeout = setTimeout(() => { + showControlsTimeout = 0; + player.classList.remove('show-controls'); + }, 3e3); + }; + + player.addEventListener('click', () => { + if(showControlsTimeout) { + clearTimeout(showControlsTimeout); + } else { + player.classList.add('show-controls'); + } + + t(); + }); + + player.addEventListener('touchstart', () => { + player.classList.add('show-controls'); + clearTimeout(showControlsTimeout); + }); + + player.addEventListener('touchend', () => { + if(player.classList.contains('is-playing')) { + t(); + } + }); + } /* player.addEventListener('click', (e) => { if(e.target != player) { @@ -398,6 +453,10 @@ export default class VideoPlayer { }); video.addEventListener('dblclick', () => { + if(touchSupport) { + return; + } + return this.toggleFullScreen(fullScreenButton); }) @@ -540,13 +599,17 @@ export default class VideoPlayer { `; } } + + public static isFullScreen(): boolean { + // @ts-ignore + return !!(document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement); + } public toggleFullScreen(fullScreenButton: HTMLElement) { // alternative standard method const player = this.wrapper; - // @ts-ignore - if(!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) { + if(!VideoPlayer.isFullScreen()) { player.classList.add('ckin__fullscreen'); /* const videoParent = this.video.parentElement; diff --git a/src/scss/partials/_ckin.scss b/src/scss/partials/_ckin.scss index 9fb188c6..b24ae45c 100644 --- a/src/scss/partials/_ckin.scss +++ b/src/scss/partials/_ckin.scss @@ -17,8 +17,10 @@ display: flex; video { - max-height: none; - max-width: none; + width: 100%; + height: 100%; + /* max-height: none; + max-width: none; */ object-fit: contain; } } @@ -45,6 +47,9 @@ //overflow: hidden; //border-radius: 5px; cursor: pointer; + display: flex; + align-items: center; + justify-content: center; &:before { content: ''; @@ -168,7 +173,8 @@ transform: translateY(50px); } - html.no-touch &:hover { + html.no-touch &:hover, + &.show-controls { .default__gradient-bottom { transform: translateY(0px); } @@ -204,6 +210,10 @@ margin: -3px 2px 0 10px; display: flex; align-items: center; + + @include respond-to(handhelds) { + margin-right: .75rem; + } &__icon { fill: #fff; diff --git a/src/scss/partials/_mediaViewer.scss b/src/scss/partials/_mediaViewer.scss index 0b97d2db..01b38db8 100644 --- a/src/scss/partials/_mediaViewer.scss +++ b/src/scss/partials/_mediaViewer.scss @@ -196,8 +196,18 @@ $move-duration: .35s; overflow: hidden; //border-radius: 0; - max-width: 100%; - max-height: 100%; + // эти значения должны быть такими же, как при установке maxWidth и maxHeight в openMedia! + //max-width: 100%; + max-width: calc(100% - 16px); + //max-height: 100%; + max-height: calc(100% - 100px); + + // эти значения должны быть такими же, как при установке maxWidth и maxHeight в openMedia! + @include respond-to(handhelds) { + overflow: visible; + //max-height: 100% !important; + max-width: 100% !important; + } .ckin__player { width: 100%; @@ -215,8 +225,11 @@ $move-duration: .35s; img, video { width: 100%; height: 100%; + max-width: 100%; + max-height: 100%; user-select: none; object-fit: cover; + //object-fit: contain; opacity: 1; //&.thumbnail { @@ -245,6 +258,55 @@ $move-duration: .35s; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; + + @include respond-to(handhelds) { + width: 100% !important; + height: 100% !important; + //height: calc(100% - 100px) !important; + /* height: calc(100% - 50px) !important; + top: calc(50% + 25px) !important; + + img, video { + margin-top: -25px; + } */ + + /* img, video { + max-height: calc(100% - 100px); + } */ + + .ckin__player:not(.ckin__fullscreen) { + .default__controls { + bottom: -50px; + } + + .default__gradient-bottom { + bottom: -50px; + } + } + } + + /* @include respond-to(handhelds) { + &.moving { + .ckin__player { + .default__controls, .default__gradient-bottom { + display: none; + } + } + } + } */ + + img:not(.thumbnail), video { + height: auto; + width: auto; + object-fit: contain; + //max-height: calc(100% - 100px); + } + + img.thumbnail { + width: auto; + object-fit: contain; + //height: auto; + } } &.hiding { @@ -262,6 +324,10 @@ $move-duration: .35s; transform: scale(1); //overflow: hidden; // WARNING position: absolute; + + display: flex; + align-items: center; + justify-content: center; } &-mover.active &-aspecter { @@ -283,7 +349,7 @@ $move-duration: .35s; visibility: visible; transition-delay: 0s; - .overlays, .btn-menu-toggle { + .overlays, > .btn-icon { opacity: 1; visibility: visible; transition: opacity $open-duration 0s, visibility 0s 0s; @@ -292,18 +358,19 @@ $move-duration: .35s; @include respond-to(handhelds) { .menu-mobile-close { - position: absolute; left: 20px; - top: 8px; } - - .btn-menu-toggle { - position: fixed; - right: 8px; + + > .btn-icon { top: 8px; + position: fixed; z-index: 5; opacity: 0; transition: opacity $open-duration 0s, visibility 0s $open-duration; + } + + .btn-menu-toggle { + right: 8px; &.menu-open { color: #fff; diff --git a/src/scss/style.scss b/src/scss/style.scss index ffe9c326..2827a51f 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -1224,6 +1224,7 @@ input:focus, button:focus { img.emoji { width: 18px; height: 18px; + margin: 0 .125rem; } .btn-circle {