Media viewer improvements:

Resizing content
Mobile video player
Minor fixes
This commit is contained in:
morethanwords 2020-09-01 15:53:46 +03:00
parent 5c5cd2d1f4
commit 2d6b25a0a4
7 changed files with 219 additions and 66 deletions

View File

@ -144,7 +144,6 @@
<div class="overlays"> <div class="overlays">
<div class="media-viewer"> <div class="media-viewer">
<div class="media-viewer-author"> <div class="media-viewer-author">
<div class="btn-icon tgico-close only-handhelds menu-mobile-close rp"></div>
<avatar-element class="media-viewer-userpic"></avatar-element> <avatar-element class="media-viewer-userpic"></avatar-element>
<div class="media-viewer-name"></div> <div class="media-viewer-name"></div>
<div class="media-viewer-date"></div> <div class="media-viewer-date"></div>
@ -164,6 +163,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="btn-icon tgico-close only-handhelds menu-mobile-close rp"></div>
<div class="btn-icon tgico-more rp btn-menu-toggle only-handhelds"> <div class="btn-icon tgico-more rp btn-menu-toggle only-handhelds">
<div class="btn-menu bottom-left"> <div class="btn-menu bottom-left">
<div class="btn-menu-item menu-menu-forward tgico-forward rp">Forward</div> <div class="btn-menu-item menu-menu-forward tgico-forward rp">Forward</div>

View File

@ -18,6 +18,7 @@ import appMediaPlaybackController from "../../components/appMediaPlaybackControl
// TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию // TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию
// TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода) // TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода)
// TODO: видео в мобильной вёрстке, если показываются элементы управления: если свайпнуть в сторону, то элементы вернутся на место, т.е. прыгнут - это не ок, надо бы замаскировать
class SwipeHandler { class SwipeHandler {
private xDown: number; private xDown: number;
@ -205,6 +206,9 @@ export class AppMediaViewer {
if(touchSupport) { if(touchSupport) {
const swipeHandler = new SwipeHandler(this.wholeDiv, (xDiff, yDiff) => { const swipeHandler = new SwipeHandler(this.wholeDiv, (xDiff, yDiff) => {
if(VideoPlayer.isFullScreen()) {
return;
}
//console.log(xDiff, yDiff); //console.log(xDiff, yDiff);
const percents = Math.abs(xDiff) / appPhotosManager.windowW; const percents = Math.abs(xDiff) / appPhotosManager.windowW;
@ -358,7 +362,7 @@ export class AppMediaViewer {
} */ } */
let aspecter: HTMLDivElement; 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')) { if(mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter')) {
aspecter = mover.firstElementChild as HTMLDivElement; aspecter = mover.firstElementChild as HTMLDivElement;
@ -491,11 +495,21 @@ export class AppMediaViewer {
if(aspecter) { if(aspecter) {
aspecter.style.borderRadius = borderRadius; aspecter.style.borderRadius = borderRadius;
if(mediaElement) {
aspecter.append(mediaElement); aspecter.append(mediaElement);
} }
}
mediaElement = mover.querySelector('video, img'); mediaElement = mover.querySelector('video, img');
if(mediaElement instanceof HTMLImageElement && src) { if(mediaElement instanceof HTMLImageElement) {
mediaElement.classList.add('thumbnail');
if(!aspecter) {
mediaElement.style.width = containerRect.width + 'px';
mediaElement.style.height = containerRect.height + 'px';
}
if(src) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
mediaElement.addEventListener('load', resolve); mediaElement.addEventListener('load', resolve);
@ -503,6 +517,7 @@ export class AppMediaViewer {
mediaElement.src = src; mediaElement.src = src;
} }
}); });
}
}/* else if(mediaElement instanceof HTMLVideoElement && mediaElement.firstElementChild && ((mediaElement.firstElementChild as HTMLSourceElement).src || src)) { }/* else if(mediaElement instanceof HTMLVideoElement && mediaElement.firstElementChild && ((mediaElement.firstElementChild as HTMLSourceElement).src || src)) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
mediaElement.addEventListener('loadeddata', resolve); mediaElement.addEventListener('loadeddata', resolve);
@ -551,7 +566,7 @@ export class AppMediaViewer {
setTimeout(() => { setTimeout(() => {
mover.innerHTML = ''; mover.innerHTML = '';
mover.classList.remove('moving', 'active', 'hiding'); mover.classList.remove('moving', 'active', 'hiding');
mover.style.display = 'none'; mover.style.cssText = 'display: none;';
deferred.resolve(); deferred.resolve();
}, delay); }, delay);
@ -669,7 +684,8 @@ export class AppMediaViewer {
private removeCenterFromMover(mover: HTMLDivElement) { private removeCenterFromMover(mover: HTMLDivElement) {
if(mover.classList.contains('center')) { 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.style.transform = `translate(${rect.left}px,${rect.top}px)`;
mover.classList.remove('center'); mover.classList.remove('center');
void mover.offsetLeft; // reflow void mover.offsetLeft; // reflow
@ -818,7 +834,7 @@ export class AppMediaViewer {
this.log('openMedia doc:', message); this.log('openMedia doc:', message);
const media = message.media.photo || message.media.document || message.media.webpage.document || message.media.webpage.photo; 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; const isFirstOpen = !this.peerID;
if(isFirstOpen) { if(isFirstOpen) {
@ -946,7 +962,11 @@ export class AppMediaViewer {
//video.src = ''; //video.src = '';
video.setAttribute('playsinline', ''); video.setAttribute('playsinline', '');
if(isSafari) {
video.autoplay = true; video.autoplay = true;
}
if(media.type == 'gif') { if(media.type == 'gif') {
video.muted = true; video.muted = true;
video.autoplay = true; video.autoplay = true;
@ -958,7 +978,7 @@ export class AppMediaViewer {
} }
const canPlayThrough = new Promise((resolve) => { const canPlayThrough = new Promise((resolve) => {
video.addEventListener('canplaythrough', resolve, {once: true}); video.addEventListener('canplay', resolve, {once: true});
}); });
const createPlayer = () => { const createPlayer = () => {
@ -1039,29 +1059,9 @@ export class AppMediaViewer {
this.updateMediaSource(mover, url, 'video'); this.updateMediaSource(mover, url, 'video');
} else { } else {
//const promise = new Promise((resolve) => video.addEventListener('loadeddata', resolve, {once: true}));
renderImageFromUrl(video, url); 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(); createPlayer();
}); });
@ -1081,8 +1081,8 @@ export class AppMediaViewer {
//if(wasActive) return; //if(wasActive) return;
//return; //return;
let load = () => { const load = () => {
let cancellablePromise = appPhotosManager.preloadPhoto(media.id, size); const cancellablePromise = appPhotosManager.preloadPhoto(media.id, size);
onAnimationEnd.then(() => { onAnimationEnd.then(() => {
this.preloader.attach(mover, true, cancellablePromise); this.preloader.attach(mover, true, cancellablePromise);
}); });
@ -1094,12 +1094,19 @@ export class AppMediaViewer {
///////this.log('indochina', blob); ///////this.log('indochina', blob);
let url = media.url; const url = media.url;
if(target instanceof SVGSVGElement) { if(target instanceof SVGSVGElement) {
this.updateMediaSource(target, url, 'img'); this.updateMediaSource(target, url, 'img');
this.updateMediaSource(mover, url, 'img'); this.updateMediaSource(mover, url, 'img');
/* const imgs = mover.querySelectorAll('img');
if(imgs && imgs.length) {
imgs.forEach(img => {
img.classList.remove('thumbnail'); // может здесь это вообще не нужно
});
} */
} else { } 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; let image = div.firstElementChild as HTMLImageElement;
if(!image || image.tagName != 'IMG') { if(!image || image.tagName != 'IMG') {
image = new Image(); image = new Image();
@ -1108,6 +1115,7 @@ export class AppMediaViewer {
//this.log('will renderImageFromUrl:', image, div, target); //this.log('will renderImageFromUrl:', image, div, target);
renderImageFromUrl(image, url, () => { renderImageFromUrl(image, url, () => {
image.classList.remove('thumbnail'); // может здесь это вообще не нужно
div.append(image); div.append(image);
}); });
} }

View File

@ -226,20 +226,24 @@ export class AppSidebarRight extends SidebarSlider {
}); });
this.sharedMedia.contentMedia.addEventListener('click', (e) => { 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) { if(!messageID) {
this.log.warn('no messageID by click on target:', target); this.log.warn('no messageID by click on target:', target);
return; 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); const ids = Object.keys(this.mediaDivsByIDs).map(k => +k).sort((a, b) => a - b);
let idx = ids.findIndex(i => i == messageID); 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); appMediaViewer.openMedia(message, target, false, this.sidebarEl, targets.slice(idx + 1).reverse(), targets.slice(0, idx).reverse(), true);
}); });

View File

@ -2,6 +2,8 @@ import { cancelEvent } from "./utils";
import { touchSupport } from "./config"; import { touchSupport } from "./config";
import appMediaPlaybackController from "../components/appMediaPlaybackController"; import appMediaPlaybackController from "../components/appMediaPlaybackController";
type SUPEREVENT = MouseEvent | TouchEvent;
export class ProgressLine { export class ProgressLine {
public container: HTMLDivElement; public container: HTMLDivElement;
protected filled: HTMLDivElement; protected filled: HTMLDivElement;
@ -46,17 +48,17 @@ export class ProgressLine {
this.events = events; this.events = events;
} }
onMouseMove = (e: MouseEvent) => { onMouseMove = (e: SUPEREVENT) => {
this.mousedown && this.scrub(e); this.mousedown && this.scrub(e);
}; };
onMouseDown = (e: MouseEvent) => { onMouseDown = (e: SUPEREVENT) => {
this.scrub(e); this.scrub(e);
this.mousedown = true; this.mousedown = true;
this.events?.onMouseDown && this.events.onMouseDown(e); this.events?.onMouseDown && this.events.onMouseDown(e);
}; };
onMouseUp = (e: MouseEvent) => { onMouseUp = (e: SUPEREVENT) => {
this.mousedown = false; this.mousedown = false;
this.events?.onMouseUp && this.events.onMouseUp(e); this.events?.onMouseUp && this.events.onMouseUp(e);
}; };
@ -65,6 +67,12 @@ export class ProgressLine {
this.container.addEventListener('mousemove', this.onMouseMove); this.container.addEventListener('mousemove', this.onMouseMove);
this.container.addEventListener('mousedown', this.onMouseDown); this.container.addEventListener('mousedown', this.onMouseDown);
this.container.addEventListener('mouseup', this.onMouseUp); 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) { public setProgress(scrubTime: number) {
@ -78,8 +86,16 @@ export class ProgressLine {
this.filled.style.transform = 'scaleX(' + scaleX + ')'; this.filled.style.transform = 'scaleX(' + scaleX + ')';
} }
protected scrub(e: MouseEvent) { protected scrub(e: SUPEREVENT) {
const scrubTime = e.offsetX / this.container.offsetWidth * this.duration; 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); this.setFilled(scrubTime);
@ -92,6 +108,12 @@ export class ProgressLine {
this.container.removeEventListener('mousedown', this.onMouseDown); this.container.removeEventListener('mousedown', this.onMouseDown);
this.container.removeEventListener('mouseup', this.onMouseUp); 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 = {}; this.events = {};
} }
} }
@ -119,7 +141,7 @@ export class MediaProgressLine extends ProgressLine {
this.setSeekMax(); this.setSeekMax();
this.setListeners(); this.setListeners();
this.setHandlers({ this.setHandlers({
onMouseDown: (e: MouseEvent) => { onMouseDown: (e: SUPEREVENT) => {
//super.onMouseDown(e); //super.onMouseDown(e);
//Таймер для того, чтобы стопать видео, если зажал мышку и не отпустил клик //Таймер для того, чтобы стопать видео, если зажал мышку и не отпустил клик
@ -133,7 +155,7 @@ export class MediaProgressLine extends ProgressLine {
}, 150); }, 150);
}, },
onMouseUp: (e: MouseEvent) => { onMouseUp: (e: SUPEREVENT) => {
//super.onMouseUp(e); //super.onMouseUp(e);
if(this.stopAndScrubTimeout) { if(this.stopAndScrubTimeout) {
@ -379,9 +401,42 @@ export default class VideoPlayer {
video.addEventListener('click', () => { video.addEventListener('click', () => {
if(!touchSupport) { if(!touchSupport) {
this.togglePlay(); 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) => { /* player.addEventListener('click', (e) => {
if(e.target != player) { if(e.target != player) {
return; return;
@ -398,6 +453,10 @@ export default class VideoPlayer {
}); });
video.addEventListener('dblclick', () => { video.addEventListener('dblclick', () => {
if(touchSupport) {
return;
}
return this.toggleFullScreen(fullScreenButton); return this.toggleFullScreen(fullScreenButton);
}) })
@ -541,12 +600,16 @@ export default class VideoPlayer {
} }
} }
public static isFullScreen(): boolean {
// @ts-ignore
return !!(document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
}
public toggleFullScreen(fullScreenButton: HTMLElement) { public toggleFullScreen(fullScreenButton: HTMLElement) {
// alternative standard method // alternative standard method
const player = this.wrapper; const player = this.wrapper;
// @ts-ignore if(!VideoPlayer.isFullScreen()) {
if(!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) {
player.classList.add('ckin__fullscreen'); player.classList.add('ckin__fullscreen');
/* const videoParent = this.video.parentElement; /* const videoParent = this.video.parentElement;

View File

@ -17,8 +17,10 @@
display: flex; display: flex;
video { video {
max-height: none; width: 100%;
max-width: none; height: 100%;
/* max-height: none;
max-width: none; */
object-fit: contain; object-fit: contain;
} }
} }
@ -45,6 +47,9 @@
//overflow: hidden; //overflow: hidden;
//border-radius: 5px; //border-radius: 5px;
cursor: pointer; cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
&:before { &:before {
content: ''; content: '';
@ -168,7 +173,8 @@
transform: translateY(50px); transform: translateY(50px);
} }
html.no-touch &:hover { html.no-touch &:hover,
&.show-controls {
.default__gradient-bottom { .default__gradient-bottom {
transform: translateY(0px); transform: translateY(0px);
} }
@ -205,6 +211,10 @@
display: flex; display: flex;
align-items: center; align-items: center;
@include respond-to(handhelds) {
margin-right: .75rem;
}
&__icon { &__icon {
fill: #fff; fill: #fff;
width: 24px; width: 24px;

View File

@ -196,8 +196,18 @@ $move-duration: .35s;
overflow: hidden; overflow: hidden;
//border-radius: 0; //border-radius: 0;
max-width: 100%; // эти значения должны быть такими же, как при установке maxWidth и maxHeight в openMedia!
max-height: 100%; //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 { .ckin__player {
width: 100%; width: 100%;
@ -215,8 +225,11 @@ $move-duration: .35s;
img, video { img, video {
width: 100%; width: 100%;
height: 100%; height: 100%;
max-width: 100%;
max-height: 100%;
user-select: none; user-select: none;
object-fit: cover; object-fit: cover;
//object-fit: contain;
opacity: 1; opacity: 1;
//&.thumbnail { //&.thumbnail {
@ -245,6 +258,55 @@ $move-duration: .35s;
left: 50% !important; left: 50% !important;
top: 50% !important; top: 50% !important;
transform: translate(-50%, -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 { &.hiding {
@ -262,6 +324,10 @@ $move-duration: .35s;
transform: scale(1); transform: scale(1);
//overflow: hidden; // WARNING //overflow: hidden; // WARNING
position: absolute; position: absolute;
display: flex;
align-items: center;
justify-content: center;
} }
&-mover.active &-aspecter { &-mover.active &-aspecter {
@ -283,7 +349,7 @@ $move-duration: .35s;
visibility: visible; visibility: visible;
transition-delay: 0s; transition-delay: 0s;
.overlays, .btn-menu-toggle { .overlays, > .btn-icon {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transition: opacity $open-duration 0s, visibility 0s 0s; transition: opacity $open-duration 0s, visibility 0s 0s;
@ -292,18 +358,19 @@ $move-duration: .35s;
@include respond-to(handhelds) { @include respond-to(handhelds) {
.menu-mobile-close { .menu-mobile-close {
position: absolute;
left: 20px; left: 20px;
top: 8px;
} }
.btn-menu-toggle { > .btn-icon {
position: fixed;
right: 8px;
top: 8px; top: 8px;
position: fixed;
z-index: 5; z-index: 5;
opacity: 0; opacity: 0;
transition: opacity $open-duration 0s, visibility 0s $open-duration; transition: opacity $open-duration 0s, visibility 0s $open-duration;
}
.btn-menu-toggle {
right: 8px;
&.menu-open { &.menu-open {
color: #fff; color: #fff;

View File

@ -1224,6 +1224,7 @@ input:focus, button:focus {
img.emoji { img.emoji {
width: 18px; width: 18px;
height: 18px; height: 18px;
margin: 0 .125rem;
} }
.btn-circle { .btn-circle {