tweb/src/lib/appManagers/appMediaViewer.ts

1034 lines
36 KiB
TypeScript
Raw Normal View History

2020-02-06 16:43:07 +01:00
import appPeersManager from "./appPeersManager";
import appPhotosManager from "./appPhotosManager";
import appMessagesManager from "./appMessagesManager";
import { RichTextProcessor } from "../richtextprocessor";
2020-06-21 14:25:17 +02:00
import { logger } from "../logger";
import ProgressivePreloader from "../../components/preloader";
import { findUpClassName, $rootScope, generatePathData, fillPropertyValue, cancelEvent } from "../utils";
2020-02-15 20:03:27 +01:00
import appDocsManager from "./appDocsManager";
2020-04-25 03:17:50 +02:00
import VideoPlayer from "../mediaPlayer";
import { renderImageFromUrl, parseMenuButtonsTo } from "../../components/misc";
import AvatarElement from "../../components/avatar";
import LazyLoadQueue from "../../components/lazyLoadQueue";
import appForward from "../../components/appForward";
2020-06-21 14:25:17 +02:00
import { isSafari, mediaSizes } from "../config";
2020-08-26 13:59:32 +02:00
import appAudio from "../../components/appAudio";
2020-08-27 20:25:47 +02:00
import { deferredPromise } from "../polyfill";
import { MTDocument } from "../../types";
// TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию
// TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода)
2020-02-06 16:43:07 +01:00
2020-02-07 07:38:55 +01:00
export class AppMediaViewer {
public wholeDiv = document.querySelector('.media-viewer-whole') as HTMLDivElement;
private overlaysDiv = this.wholeDiv.firstElementChild as HTMLDivElement;
2020-02-06 16:43:07 +01:00
private author = {
avatarEl: this.overlaysDiv.querySelector('.media-viewer-userpic') as AvatarElement,
2020-02-06 16:43:07 +01:00
nameEl: this.overlaysDiv.querySelector('.media-viewer-name') as HTMLDivElement,
date: this.overlaysDiv.querySelector('.media-viewer-date') as HTMLDivElement
};
public buttons: {[k in 'delete' | 'forward' | 'download' | 'close' | 'prev' | 'next' |
'menu-delete' | 'menu-forward' | 'menu-download' | 'mobile-close']: HTMLElement} = {} as any;
private content: {[k in 'container' | 'caption' | 'mover']: HTMLDivElement} = {
container: this.overlaysDiv.querySelector('.media-viewer-media'),
caption: this.overlaysDiv.querySelector('.media-viewer-caption'),
mover: null
2020-02-06 16:43:07 +01:00
};
2020-02-07 07:38:55 +01:00
public currentMessageID = 0;
2020-02-06 16:43:07 +01:00
private preloader: ProgressivePreloader = null;
2020-08-27 20:25:47 +02:00
private preloaderStreamable: ProgressivePreloader = null;
2020-02-07 07:38:55 +01:00
private lastTarget: HTMLElement = null;
2020-04-08 17:46:43 +02:00
private prevTargets: {
element: HTMLElement,
mid: number
}[] = [];
private nextTargets: AppMediaViewer['prevTargets'] = [];
//private targetContainer: HTMLElement = null;
//private loadMore: () => void = null;
2020-02-07 07:38:55 +01:00
public log: ReturnType<typeof logger>;
2020-04-08 17:46:43 +02:00
private peerID = 0;
private loadMediaPromiseUp: Promise<void> = null;
private loadMediaPromiseDown: Promise<void> = null;
private loadedAllMediaUp = false;
private loadedAllMediaDown = false;
private reverse = false; // reverse means next = higher msgid
2020-05-01 23:28:40 +02:00
private needLoadMore = true;
private pageEl = document.getElementById('page-chats') as HTMLDivElement;
private setMoverPromise: Promise<void>;
2020-08-27 20:25:47 +02:00
private setMoverAnimationPromise: Promise<void>;
private lazyLoadQueue: LazyLoadQueue;
2020-02-06 16:43:07 +01:00
constructor() {
2020-02-07 07:38:55 +01:00
this.log = logger('AMV');
2020-02-06 16:43:07 +01:00
this.preloader = new ProgressivePreloader();
2020-08-27 20:25:47 +02:00
2020-08-28 14:37:15 +02:00
this.preloaderStreamable = new ProgressivePreloader(undefined, false, true);
2020-08-27 20:25:47 +02:00
2020-06-16 22:48:08 +02:00
this.lazyLoadQueue = new LazyLoadQueue(undefined, true);
2020-02-15 12:14:58 +01:00
parseMenuButtonsTo(this.buttons, this.wholeDiv.querySelectorAll(`[class*='menu']`) as NodeListOf<HTMLElement>);
const close = (e: MouseEvent) => {
cancelEvent(e);
//this.overlaysDiv.classList.remove('active');
2020-02-06 16:43:07 +01:00
this.content.container.innerHTML = '';
/* if(this.content.container.firstElementChild) {
2020-03-02 18:15:11 +01:00
URL.revokeObjectURL((this.content.container.firstElementChild as HTMLImageElement).src);
} */
2020-03-02 18:15:11 +01:00
2020-04-08 17:46:43 +02:00
this.peerID = 0;
2020-02-07 07:38:55 +01:00
this.currentMessageID = 0;
this.lazyLoadQueue.clear();
this.setMoverToTarget(this.lastTarget, true);
2020-02-15 12:14:58 +01:00
this.lastTarget = null;
2020-04-08 17:46:43 +02:00
this.prevTargets = [];
this.nextTargets = [];
this.loadedAllMediaUp = this.loadedAllMediaDown = false;
this.loadMediaPromiseUp = this.loadMediaPromiseDown = null;
this.setMoverPromise = null;
2020-02-15 12:14:58 +01:00
if(appForward.container.classList.contains('active')) {
setTimeout(() => {
appForward.close();
}, 200);
}
2020-08-27 20:25:47 +02:00
window.removeEventListener('keydown', this.onKeyDown);
};
2020-08-27 20:25:47 +02:00
[this.buttons.close, this.buttons["mobile-close"], this.preloaderStreamable.preloader].forEach(el => {
el.addEventListener('click', close);
2020-02-06 16:43:07 +01:00
});
this.buttons.prev.addEventListener('click', (e) => {
cancelEvent(e);
if(this.setMoverPromise) return;
2020-04-08 17:46:43 +02:00
let target = this.prevTargets.pop();
if(target) {
2020-04-08 17:46:43 +02:00
this.nextTargets.unshift({element: this.lastTarget, mid: this.currentMessageID});
this.openMedia(appMessagesManager.getMessage(target.mid), target.element);
} else {
this.buttons.prev.style.display = 'none';
}
});
this.buttons.next.addEventListener('click', (e) => {
cancelEvent(e);
if(this.setMoverPromise) return;
2020-04-08 17:46:43 +02:00
let target = this.nextTargets.shift();
if(target) {
2020-04-08 17:46:43 +02:00
this.prevTargets.push({element: this.lastTarget, mid: this.currentMessageID});
this.openMedia(appMessagesManager.getMessage(target.mid), target.element);
} else {
this.buttons.next.style.display = 'none';
}
2020-02-06 16:43:07 +01:00
});
[this.buttons.download, this.buttons["menu-download"]].forEach(el => {
2020-08-27 20:25:47 +02:00
el.addEventListener('click', this.onClickDownload);
});
2020-06-21 14:25:17 +02:00
const forward = (e: MouseEvent) => {
appForward.init([this.currentMessageID]);
};
[this.buttons.forward, this.buttons["menu-forward"]].forEach(el => {
el.addEventListener('click', forward);
});
2020-08-27 20:25:47 +02:00
this.wholeDiv.addEventListener('click', this.onClick);
//this.content.mover.addEventListener('click', this.onClickBinded);
2020-02-15 12:14:58 +01:00
//this.content.mover.append(this.buttons.prev, this.buttons.next);
this.setNewMover();
2020-02-06 16:43:07 +01:00
}
2020-08-27 20:25:47 +02:00
onClickDownload = (e: MouseEvent) => {
const message = appMessagesManager.getMessage(this.currentMessageID);
if(message.media.photo) {
appPhotosManager.savePhotoFile(message.media.photo);
} else {
let document: MTDocument = null;
if(message.media.webpage) document = message.media.webpage.document;
else document = message.media.document;
if(document) {
//console.log('will save document:', document);
appDocsManager.saveDocFile(document);
}
}
};
onClick = (e: MouseEvent) => {
if(this.setMoverAnimationPromise) return;
const target = e.target as HTMLElement;
if(target.tagName == 'A') return;
cancelEvent(e);
let mover: HTMLElement = null;
['media-viewer-mover', 'media-viewer-buttons', 'media-viewer-author'].find(s => {
try {
mover = findUpClassName(target, s);
if(mover) return true;
} catch(err) {return false;}
});
if(/* target == this.mediaViewerDiv */!mover || target.tagName == 'IMG' || target.tagName == 'image') {
this.buttons.close.click();
}
};
onKeyDown = (e: KeyboardEvent) => {
2020-02-15 12:14:58 +01:00
//this.log('onKeyDown', e);
if(e.key == 'ArrowRight') {
this.buttons.next.click();
} else if(e.key == 'ArrowLeft') {
this.buttons.prev.click();
}
2020-08-27 20:25:47 +02:00
};
2020-02-15 12:14:58 +01:00
2020-08-27 20:25:47 +02:00
private async setMoverToTarget(target: HTMLElement, closing = false, fromRight = 0) {
2020-08-26 22:31:07 +02:00
const mover = this.content.mover;
if(!closing) {
mover.innerHTML = '';
//mover.append(this.buttons.prev, this.buttons.next);
}
2020-08-27 20:25:47 +02:00
this.removeCenterFromMover(mover);
2020-08-26 22:31:07 +02:00
const wasActive = fromRight !== 0;
2020-02-15 12:14:58 +01:00
2020-08-26 22:31:07 +02:00
const delay = wasActive ? 350 : 200;
2020-04-08 17:46:43 +02:00
//let delay = wasActive ? 350 : 10000;
2020-02-15 12:14:58 +01:00
/* if(wasActive) {
this.moveTheMover(mover);
mover = this.setNewMover();
} */
this.log('setMoverToTarget', target, closing, wasActive, fromRight);
2020-02-15 12:14:58 +01:00
2020-08-19 16:11:25 +02:00
let realParent: HTMLElement;
2020-04-08 17:46:43 +02:00
let rect: DOMRect;
if(target) {
if(target instanceof SVGImageElement || target.parentElement instanceof SVGForeignObjectElement) {
realParent = findUpClassName(target, 'attachment');
rect = realParent.getBoundingClientRect();
} else {
2020-08-19 16:11:25 +02:00
realParent = target.parentElement as HTMLElement;
2020-04-08 17:46:43 +02:00
rect = target.getBoundingClientRect();
}
}
2020-08-26 22:31:07 +02:00
const containerRect = this.content.container.getBoundingClientRect();
2020-02-15 12:14:58 +01:00
let transform = '';
let left: number;
let top: number;
if(wasActive) {
left = fromRight === 1 ? appPhotosManager.windowW : -containerRect.width;
top = containerRect.top;
} else {
left = rect.left;
top = rect.top;
}
2020-08-27 20:25:47 +02:00
transform += `translate(${left}px,${top}px) `;
/* if(wasActive) {
left = fromRight === 1 ? appPhotosManager.windowW / 2 : -(containerRect.width + appPhotosManager.windowW / 2);
transform += `translate(${left}px,-50%) `;
} else {
left = rect.left - (appPhotosManager.windowW / 2);
top = rect.top - (appPhotosManager.windowH / 2);
transform += `translate(${left}px,${top}px) `;
} */
let aspecter: HTMLDivElement;
if(target instanceof HTMLImageElement || target instanceof HTMLVideoElement) {
if(mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter')) {
aspecter = mover.firstElementChild as HTMLDivElement;
2020-08-26 22:31:07 +02:00
const player = aspecter.querySelector('.ckin__player');
if(player) {
2020-08-26 22:31:07 +02:00
const video = player.firstElementChild as HTMLVideoElement;
aspecter.append(video);
player.remove();
}
if(!aspecter.style.cssText) { // всё из-за видео, элементы управления скейлятся, так бы можно было этого не делать
mover.classList.remove('active');
this.setFullAspect(aspecter, containerRect, rect);
void mover.offsetLeft; // reflow
mover.classList.add('active');
}
} else {
aspecter = document.createElement('div');
2020-08-27 20:25:47 +02:00
aspecter.classList.add('media-viewer-aspecter'/* , 'disable-hover' */);
mover.prepend(aspecter);
}
aspecter.style.cssText = `width: ${rect.width}px; height: ${rect.height}px; transform: scale(${containerRect.width / rect.width}, ${containerRect.height / rect.height});`;
}
mover.style.width = containerRect.width + 'px';
mover.style.height = containerRect.height + 'px';
2020-08-26 22:31:07 +02:00
const scaleX = rect.width / containerRect.width;
const scaleY = rect.height / containerRect.height;
2020-02-15 12:14:58 +01:00
if(!wasActive) {
transform += `scale(${scaleX},${scaleY}) `;
2020-04-08 17:46:43 +02:00
}
2020-02-15 12:14:58 +01:00
2020-04-08 17:46:43 +02:00
let borderRadius = window.getComputedStyle(realParent).getPropertyValue('border-radius');
2020-08-26 22:31:07 +02:00
const brSplitted = fillPropertyValue(borderRadius) as string[];
2020-04-08 17:46:43 +02:00
borderRadius = brSplitted.map(r => (parseInt(r) / scaleX) + 'px').join(' ');
if(!wasActive) {
2020-02-15 12:14:58 +01:00
mover.style.borderRadius = borderRadius;
}
mover.style.transform = transform;
2020-02-17 13:18:06 +01:00
/* if(wasActive) {
2020-02-15 12:14:58 +01:00
this.log('setMoverToTarget', mover.style.transform);
2020-02-17 13:18:06 +01:00
} */
2020-04-08 17:46:43 +02:00
let path: SVGPathElement;
2020-08-26 22:31:07 +02:00
const isOut = target.classList.contains('is-out');
2020-04-08 17:46:43 +02:00
2020-08-27 20:25:47 +02:00
const deferred = this.setMoverAnimationPromise = deferredPromise<void>();
const ret = {onAnimationEnd: deferred};
this.setMoverAnimationPromise.then(() => {
this.setMoverAnimationPromise = null;
});
if(!closing) {
let mediaElement: HTMLImageElement | HTMLVideoElement;
let src: string;
if(target.tagName == 'DIV') { // useContainerAsTarget
if(target.firstElementChild) {
mediaElement = new Image();
src = (target.firstElementChild as HTMLImageElement).src;
mover.append(mediaElement);
}
/* mediaElement = new Image();
src = target.style.backgroundImage.slice(5, -2); */
} else if(target instanceof HTMLImageElement) {
mediaElement = new Image();
src = target.src;
} else if(target instanceof HTMLVideoElement) {
2020-08-26 22:31:07 +02:00
const video = mediaElement = document.createElement('video');
video.src = target?.src;
2020-04-08 17:46:43 +02:00
} else if(target instanceof SVGSVGElement) {
2020-08-26 22:31:07 +02:00
const clipID = target.dataset.clipID;
const newClipID = clipID + '-mv';
2020-04-08 17:46:43 +02:00
2020-08-26 22:31:07 +02:00
const {width, height} = containerRect;
2020-04-08 17:46:43 +02:00
2020-08-26 22:31:07 +02:00
const newSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2020-04-08 17:46:43 +02:00
newSvg.setAttributeNS(null, 'width', '' + width);
newSvg.setAttributeNS(null, 'height', '' + height);
2020-08-27 20:25:47 +02:00
// нижние два свойства для масштабирования
newSvg.setAttributeNS(null, 'viewBox', `0 0 ${width} ${height}`);
newSvg.setAttributeNS(null, 'preserveAspectRatio', 'xMidYMid meet');
2020-04-08 17:46:43 +02:00
newSvg.insertAdjacentHTML('beforeend', target.firstElementChild.outerHTML.replace(clipID, newClipID));
newSvg.insertAdjacentHTML('beforeend', target.lastElementChild.outerHTML.replace(clipID, newClipID));
// теперь надо выставить новую позицию для хвостика
2020-08-26 22:31:07 +02:00
const defs = newSvg.firstElementChild;
const use = defs.firstElementChild.firstElementChild as SVGUseElement;
2020-04-08 17:46:43 +02:00
if(use instanceof SVGUseElement) {
let transform = use.getAttributeNS(null, 'transform');
transform = transform.replace(/translate\((.+?), (.+?)\) scale\((.+?), (.+?)\)/, (match, x, y, sX, sY) => {
x = +x;
if(x != 2) {
x = width - (2 / scaleX);
} else {
x = 2 / scaleX;
}
y = height;
return `translate(${x}, ${y}) scale(${+sX / scaleX}, ${+sY / scaleY})`;
});
use.setAttributeNS(null, 'transform', transform);
// и новый RECT
path = defs.firstElementChild.lastElementChild as SVGPathElement;
// код ниже нужен только чтобы скрыть моргание до момента как сработает таймаут
let d: string;
2020-08-26 22:31:07 +02:00
const br: [number, number, number, number] = borderRadius.split(' ').map(v => parseInt(v)) as any;
2020-04-08 17:46:43 +02:00
if(isOut) d = generatePathData(0, 0, width - 9 / scaleX, height, ...br);
else d = generatePathData(9 / scaleX, 0, width - 9 / scaleX, height, ...br);
path.setAttributeNS(null, 'd', d);
}
2020-08-26 22:31:07 +02:00
const foreignObject = newSvg.lastElementChild;
foreignObject.setAttributeNS(null, 'width', '' + containerRect.width);
foreignObject.setAttributeNS(null, 'height', '' + containerRect.height);
2020-04-08 17:46:43 +02:00
mover.prepend(newSvg);
}
if(aspecter) {
aspecter.style.borderRadius = borderRadius;
aspecter.append(mediaElement);
}
mediaElement = mover.querySelector('video, img');
2020-06-21 14:25:17 +02:00
if(mediaElement instanceof HTMLImageElement && src) {
await new Promise((resolve, reject) => {
mediaElement.addEventListener('load', resolve);
if(src) {
mediaElement.src = src;
}
});
2020-06-16 22:48:08 +02:00
}/* else if(mediaElement instanceof HTMLVideoElement && mediaElement.firstElementChild && ((mediaElement.firstElementChild as HTMLSourceElement).src || src)) {
await new Promise((resolve, reject) => {
mediaElement.addEventListener('loadeddata', resolve);
if(src) {
(mediaElement.firstElementChild as HTMLSourceElement).src = src;
}
});
2020-06-16 22:48:08 +02:00
} */
mover.style.display = '';
2020-02-15 12:14:58 +01:00
window.requestAnimationFrame(() => {
2020-02-15 12:14:58 +01:00
mover.classList.add(wasActive ? 'moving' : 'active');
});
} else {
2020-08-27 20:25:47 +02:00
/* if(mover.classList.contains('center')) {
mover.classList.remove('center');
void mover.offsetLeft; // reflow
} */
2020-04-08 17:46:43 +02:00
if(target instanceof SVGSVGElement) {
path = mover.querySelector('path');
if(path) {
this.sizeTailPath(path, containerRect, scaleX, delay, false, isOut, borderRadius);
}
}
if(target.classList.contains('media-viewer-media')) {
mover.classList.add('hiding');
}
setTimeout(() => {
this.wholeDiv.classList.remove('active');
2020-02-15 12:14:58 +01:00
}, 0);
setTimeout(() => {
mover.style.borderRadius = borderRadius;
if(mover.firstElementChild) {
(mover.firstElementChild as HTMLElement).style.borderRadius = borderRadius;
}
}, delay / 2);
2020-02-15 12:14:58 +01:00
setTimeout(() => {
mover.innerHTML = '';
mover.classList.remove('moving', 'active', 'hiding');
mover.style.display = 'none';
2020-08-27 20:25:47 +02:00
deferred.resolve();
2020-02-15 12:14:58 +01:00
}, delay);
2020-08-27 20:25:47 +02:00
return ret;
}
//await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => window.requestAnimationFrame(resolve));
2020-08-27 20:25:47 +02:00
// чтобы проверить установленную позицию - раскомментировать
//throw '';
mover.style.transform = `translate(${containerRect.left}px,${containerRect.top}px) scale(1,1)`;
2020-08-27 20:25:47 +02:00
//mover.style.transform = `translate(-50%,-50%) scale(1,1)`;
2020-02-15 12:14:58 +01:00
if(aspecter) {
this.setFullAspect(aspecter, containerRect, rect);
}
2020-04-08 17:46:43 +02:00
setTimeout(() => {
mover.style.borderRadius = '';
if(mover.firstElementChild) {
(mover.firstElementChild as HTMLElement).style.borderRadius = '';
}
}, delay / 2);
mover.dataset.timeout = '' + setTimeout(() => {
mover.classList.remove('moving');
if(aspecter) { // всё из-за видео, элементы управления скейлятся, так бы можно было этого не делать
2020-08-27 20:25:47 +02:00
if(mover.querySelector('video') || true) {
mover.classList.remove('active');
aspecter.style.cssText = '';
void mover.offsetLeft; // reflow
}
2020-08-27 20:25:47 +02:00
//aspecter.classList.remove('disable-hover');
2020-04-08 17:46:43 +02:00
}
2020-08-27 20:25:47 +02:00
// эти строки нужны для установки центральной позиции, в случае ресайза это будет нужно
mover.classList.add('center', 'no-transition');
/* mover.style.left = mover.style.top = '50%';
mover.style.transform = 'translate(-50%, -50%)';
void mover.offsetLeft; // reflow */
// это уже нужно для будущих анимаций
mover.classList.add('active');
delete mover.dataset.timeout;
2020-08-27 20:25:47 +02:00
deferred.resolve();
}, delay);
if(path) {
this.sizeTailPath(path, containerRect, scaleX, delay, true, isOut, borderRadius);
}
2020-08-27 20:25:47 +02:00
return ret;
}
2020-02-15 12:14:58 +01:00
private setFullAspect(aspecter: HTMLDivElement, containerRect: DOMRect, rect: DOMRect) {
2020-06-20 03:11:24 +02:00
/* let media = aspecter.firstElementChild;
let proportion: number;
if(media instanceof HTMLImageElement) {
proportion = media.naturalWidth / media.naturalHeight;
} else if(media instanceof HTMLVideoElement) {
proportion = media.videoWidth / media.videoHeight;
2020-06-20 03:11:24 +02:00
} */
const proportion = containerRect.width / containerRect.height;
let {width, height} = rect;
/* if(proportion == 1) {
aspecter.style.cssText = '';
} else { */
if(proportion > 0) {
width = height * proportion;
} else {
height = width * proportion;
}
//this.log('will set style aspecter:', `width: ${width}px; height: ${height}px; transform: scale(${containerRect.width / width}, ${containerRect.height / height});`);
aspecter.style.cssText = `width: ${width}px; height: ${height}px; transform: scale(${containerRect.width / width}, ${containerRect.height / height});`;
//}
}
2020-08-27 20:25:47 +02:00
private sizeTailPath(path: SVGPathElement, rect: DOMRect, scaleX: number, delay: number, upscale: boolean, isOut: boolean, borderRadius: string) {
2020-08-26 22:31:07 +02:00
const start = Date.now();
const {width, height} = rect;
2020-04-08 17:46:43 +02:00
delay = delay / 2;
2020-08-26 22:31:07 +02:00
const br = borderRadius.split(' ').map(v => parseInt(v));
2020-04-08 17:46:43 +02:00
2020-08-26 22:31:07 +02:00
const step = () => {
const diff = Date.now() - start;
2020-04-08 17:46:43 +02:00
let progress = diff / delay;
if(progress > 1) progress = 1;
if(upscale) progress = 1 - progress;
2020-08-26 22:31:07 +02:00
const _br: [number, number, number, number] = br.map(v => v * progress) as any;
2020-04-08 17:46:43 +02:00
let d: string;
if(isOut) d = generatePathData(0, 0, width - (9 / scaleX * progress), height, ..._br);
else d = generatePathData(9 / scaleX * progress, 0, width/* width - (9 / scaleX * progress) */, height, ..._br);
path.setAttributeNS(null, 'd', d);
if(diff < delay) window.requestAnimationFrame(step);
};
//window.requestAnimationFrame(step);
step();
}
2020-08-27 20:25:47 +02:00
private removeCenterFromMover(mover: HTMLDivElement) {
if(mover.classList.contains('center')) {
const rect = mover.getBoundingClientRect();
mover.style.transform = `translate(${rect.left}px,${rect.top}px)`;
mover.classList.remove('center');
void mover.offsetLeft; // reflow
mover.classList.remove('no-transition');
}
}
private moveTheMover(mover: HTMLDivElement, toLeft = true) {
2020-08-26 22:31:07 +02:00
const windowW = appPhotosManager.windowW;
2020-02-15 12:14:58 +01:00
2020-08-27 20:25:47 +02:00
this.removeCenterFromMover(mover);
//mover.classList.remove('active');
2020-02-15 12:14:58 +01:00
mover.classList.add('moving');
if(mover.dataset.timeout) { // и это тоже всё из-за скейла видео, так бы это не нужно было
clearTimeout(+mover.dataset.timeout);
}
2020-08-26 22:31:07 +02:00
const rect = mover.getBoundingClientRect();
2020-02-15 12:14:58 +01:00
2020-08-26 22:31:07 +02:00
const newTransform = mover.style.transform.replace(/translate\((.+?),/, (match, p1) => {
2020-08-27 20:25:47 +02:00
const x = toLeft ? -rect.width : windowW;
//const x = toLeft ? -(rect.right + (rect.width / 2)) : windowW / 2;
2020-02-15 12:14:58 +01:00
return match.replace(p1, x + 'px');
});
2020-02-17 13:18:06 +01:00
////////this.log('set newTransform:', newTransform, mover.style.transform, toLeft);
2020-02-15 12:14:58 +01:00
mover.style.transform = newTransform;
setTimeout(() => {
mover.remove();
}, 350);
}
2020-08-27 20:25:47 +02:00
private setNewMover() {
2020-08-26 22:31:07 +02:00
const newMover = document.createElement('div');
2020-02-15 12:14:58 +01:00
newMover.classList.add('media-viewer-mover');
if(this.content.mover) {
2020-08-26 22:31:07 +02:00
const oldMover = this.content.mover;
oldMover.parentElement.append(newMover);
} else {
this.wholeDiv.append(newMover);
}
2020-02-15 12:14:58 +01:00
return this.content.mover = newMover;
}
2020-04-08 17:46:43 +02:00
2020-08-26 22:31:07 +02:00
/* public isElementVisible(container: HTMLElement, target: HTMLElement) {
const rect = container.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
2020-04-08 17:46:43 +02:00
return targetRect.bottom > rect.top && targetRect.top < rect.bottom;
2020-08-26 22:31:07 +02:00
} */
2020-04-08 17:46:43 +02:00
// нет смысла делать проверку для reverse и loadMediaPromise
2020-08-27 20:25:47 +02:00
private loadMoreMedia(older = true) {
2020-04-08 17:46:43 +02:00
//if(!older && this.reverse) return;
if(older && this.loadedAllMediaDown) return;
else if(!older && this.loadedAllMediaUp) return;
if(older && this.loadMediaPromiseDown) return this.loadMediaPromiseDown;
else if(!older && this.loadMediaPromiseUp) return this.loadMediaPromiseUp;
2020-08-26 22:31:07 +02:00
const loadCount = 50;
const backLimit = older ? 0 : loadCount;
2020-04-08 17:46:43 +02:00
let maxID = this.currentMessageID;
2020-02-06 16:43:07 +01:00
2020-04-08 17:46:43 +02:00
let anchor: {element: HTMLElement, mid: number};
if(older) {
anchor = this.reverse ? this.prevTargets[0] : this.nextTargets[this.nextTargets.length - 1];
} else {
anchor = this.reverse ? this.nextTargets[this.nextTargets.length - 1] : this.prevTargets[0];
}
if(anchor) maxID = anchor.mid;
if(!older) maxID += 1;
2020-08-26 22:31:07 +02:00
const peerID = this.peerID;
2020-04-08 17:46:43 +02:00
2020-08-26 22:31:07 +02:00
const promise = appMessagesManager.getSearch(peerID, '',
2020-04-08 17:46:43 +02:00
{_: 'inputMessagesFilterPhotoVideo'}, maxID, loadCount/* older ? loadCount : 0 */, 0, backLimit).then(value => {
if(this.peerID != peerID) {
this.log.warn('peer changed');
return;
}
this.log('loaded more media by maxID:', maxID, value, older, this.reverse);
if(value.history.length < loadCount) {
/* if(this.reverse) {
if(older) this.loadedAllMediaUp = true;
else this.loadedAllMediaDown = true;
} else { */
if(older) this.loadedAllMediaDown = true;
else this.loadedAllMediaUp = true;
//}
}
2020-08-26 22:31:07 +02:00
const method = older ? value.history.forEach : value.history.forEachReverse;
2020-04-08 17:46:43 +02:00
method.call(value.history, mid => {
2020-08-26 22:31:07 +02:00
const message = appMessagesManager.getMessage(mid);
const media = message.media;
2020-04-08 17:46:43 +02:00
if(!media || !(media.photo || media.document || (media.webpage && media.webpage.document))) return;
if(media._ == 'document' && media.type != 'video') return;
2020-08-26 22:31:07 +02:00
const t = {element: null as HTMLElement, mid: mid};
2020-04-08 17:46:43 +02:00
if(older) {
if(this.reverse) this.prevTargets.unshift(t);
else this.nextTargets.push(t);
} else {
if(this.reverse) this.nextTargets.push(t);
else this.prevTargets.unshift(t);
}
});
this.buttons.prev.style.display = this.prevTargets.length ? '' : 'none';
this.buttons.next.style.display = this.nextTargets.length ? '' : 'none';
}, () => {}).then(() => {
if(older) this.loadMediaPromiseDown = null;
else this.loadMediaPromiseUp = null;
});
if(older) this.loadMediaPromiseDown = promise;
else this.loadMediaPromiseUp = promise;
return promise;
}
2020-08-27 20:25:47 +02:00
private updateMediaSource(target: HTMLElement, url: string, tagName: 'video' | 'img') {
2020-04-08 17:46:43 +02:00
//if(target instanceof SVGSVGElement) {
2020-08-26 22:31:07 +02:00
const el = target.querySelector(tagName) as HTMLElement;
2020-04-16 02:48:41 +02:00
renderImageFromUrl(el, url);
2020-04-08 17:46:43 +02:00
/* } else {
} */
}
public async openMedia(message: any, target?: HTMLElement, reverse = false, targetContainer?: HTMLElement,
2020-05-01 23:28:40 +02:00
prevTargets: AppMediaViewer['prevTargets'] = [], nextTargets: AppMediaViewer['prevTargets'] = [], needLoadMore = true) {
if(this.setMoverPromise) return this.setMoverPromise;
this.log('openMedia doc:', message);
const media = message.media.photo || message.media.document || message.media.webpage.document || message.media.webpage.photo;
2020-02-06 16:43:07 +01:00
const isVideo = media.mime_type == 'video/mp4';
const isFirstOpen = !this.peerID;
2020-04-08 17:46:43 +02:00
if(isFirstOpen) {
this.peerID = $rootScope.selectedPeerID;
//this.targetContainer = targetContainer;
2020-04-08 17:46:43 +02:00
this.prevTargets = prevTargets;
this.nextTargets = nextTargets;
this.reverse = reverse;
2020-05-01 23:28:40 +02:00
this.needLoadMore = needLoadMore;
2020-04-08 17:46:43 +02:00
//this.loadMore = loadMore;
if(appForward.container.classList.contains('active')) {
appForward.close();
await new Promise((resolve) => setTimeout(resolve, 200));
}
2020-04-08 17:46:43 +02:00
}
/* if(this.nextTargets.length < 10 && this.loadMore) {
this.loadMore();
} */
2020-02-15 12:14:58 +01:00
let fromRight = 0;
2020-04-08 17:46:43 +02:00
if(!isFirstOpen) {
//if(this.lastTarget === prevTarget) {
if(this.reverse) fromRight = this.currentMessageID < message.mid ? 1 : -1;
else fromRight = this.currentMessageID > message.mid ? 1 : -1;
2020-02-15 12:14:58 +01:00
}
2020-04-08 17:46:43 +02:00
//if(prevTarget && (!prevTarget.parentElement || !this.isElementVisible(this.targetContainer, prevTarget))) prevTarget = null;
//if(nextTarget && (!nextTarget.parentElement || !this.isElementVisible(this.targetContainer, nextTarget))) nextTarget = null;
this.buttons.prev.style.display = this.prevTargets.length ? '' : 'none';
this.buttons.next.style.display = this.nextTargets.length ? '' : 'none';
2020-02-06 16:43:07 +01:00
const container = this.content.container;
const useContainerAsTarget = !target;
2020-04-08 17:46:43 +02:00
if(useContainerAsTarget) target = container;
2020-02-06 16:43:07 +01:00
this.currentMessageID = message.mid;
2020-02-15 12:14:58 +01:00
this.lastTarget = target;
2020-04-08 17:46:43 +02:00
2020-05-01 23:28:40 +02:00
if(this.needLoadMore) {
if(this.nextTargets.length < 20) {
this.loadMoreMedia(!this.reverse);
}
if(this.prevTargets.length < 20) {
this.loadMoreMedia(this.reverse);
}
2020-04-08 17:46:43 +02:00
}
2020-02-06 16:43:07 +01:00
if(container.firstElementChild) {
2020-02-07 07:38:55 +01:00
container.innerHTML = '';
2020-02-06 16:43:07 +01:00
}
const date = new Date(media.date * 1000);
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
2020-02-06 16:43:07 +01:00
const dateStr = months[date.getMonth()] + ' ' + date.getDate() + ' at '+ date.getHours() + ':' + ('0' + date.getMinutes()).slice(-2);
2020-02-06 16:43:07 +01:00
this.author.date.innerText = dateStr;
const name = appPeersManager.getPeerTitle(message.fromID);
this.author.nameEl.innerHTML = name;
2020-02-06 16:43:07 +01:00
if(message.message) {
this.content.caption.innerHTML = RichTextProcessor.wrapRichText(message.message, {
entities: message.totalEntities
});
} else {
this.content.caption.innerHTML = '';
}
let oldAvatar = this.author.avatarEl;
2020-08-27 20:25:47 +02:00
this.author.avatarEl = (this.author.avatarEl.cloneNode() as AvatarElement);
this.author.avatarEl.setAttribute('peer', '' + message.fromID);
oldAvatar.parentElement.replaceChild(this.author.avatarEl, oldAvatar);
// ok set
2020-02-15 12:14:58 +01:00
const wasActive = fromRight !== 0;
2020-02-15 12:14:58 +01:00
if(wasActive) {
this.moveTheMover(this.content.mover, fromRight === 1);
this.setNewMover();
} else {
2020-08-27 20:25:47 +02:00
window.addEventListener('keydown', this.onKeyDown);
this.wholeDiv.classList.add('active');
2020-02-15 12:14:58 +01:00
}
2020-02-17 13:18:06 +01:00
////////this.log('wasActive:', wasActive);
2020-02-15 12:14:58 +01:00
const mover = this.content.mover;
//const maxWidth = appPhotosManager.windowW - 16;
2020-06-21 14:25:17 +02:00
const maxWidth = mediaSizes.isMobile ? this.pageEl.scrollWidth : this.pageEl.scrollWidth - 16;
const maxHeight = appPhotosManager.windowH - 100;
const size = appPhotosManager.setAttachmentSize(media, container, maxWidth, maxHeight);
// need after setAttachmentSize
/* if(useContainerAsTarget) {
target = target.querySelector('img, video') || target;
} */
2020-08-27 20:25:47 +02:00
const preloader = media.supportsStreaming ? this.preloaderStreamable : this.preloader;
let setMoverPromise: Promise<void>;
2020-02-06 16:43:07 +01:00
if(isVideo) {
2020-02-17 13:18:06 +01:00
////////this.log('will wrap video', media, size);
2020-08-27 20:25:47 +02:00
setMoverPromise = this.setMoverToTarget(target, false, fromRight).then(({onAnimationEnd}) => {
//return; // set and don't move
2020-02-15 12:14:58 +01:00
//if(wasActive) return;
2020-04-08 17:46:43 +02:00
//return;
2020-06-16 22:48:08 +02:00
const div = mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter') ? mover.firstElementChild : mover;
const video = mover.querySelector('video') || document.createElement('video');
2020-08-27 20:25:47 +02:00
//video.src = '';
2020-04-08 17:46:43 +02:00
video.setAttribute('playsinline', '');
if(media.type == 'gif') {
video.muted = true;
video.autoplay = true;
video.loop = true;
}
2020-08-27 20:25:47 +02:00
if(!video.parentElement) {
div.append(video);
}
const canPlayThrough = new Promise((resolve) => {
video.addEventListener('canplaythrough', resolve, {once: true});
});
const createPlayer = () => {
if(media.type != 'gif') {
video.dataset.ckin = 'default';
video.dataset.overlay = '1';
2020-08-26 13:59:32 +02:00
// fix for simultaneous play
appAudio.pause();
appAudio.willBePlayedAudio = null;
2020-08-27 20:25:47 +02:00
Promise.all([canPlayThrough, onAnimationEnd]).then(() => {
const player = new VideoPlayer(video, true, media.supportsStreaming);
/* div.append(video);
mover.append(player.wrapper); */
});
2020-08-26 22:31:07 +02:00
}
};
2020-08-27 20:25:47 +02:00
if(media.supportsStreaming) {
onAnimationEnd.then(() => {
if(video.readyState < video.HAVE_FUTURE_DATA) {
preloader.attach(mover, true);
}
/* canPlayThrough.then(() => {
preloader.detach();
}); */
});
const attachCanPlay = () => {
video.addEventListener('canplay', () => {
//this.log('video waited and progress loaded');
preloader.detach();
video.parentElement.classList.remove('is-buffering');
}, {once: true});
};
video.addEventListener('waiting', (e) => {
const loading = video.networkState === video.NETWORK_LOADING;
const isntEnoughData = video.readyState < video.HAVE_FUTURE_DATA;
//this.log('video waiting for progress', loading, isntEnoughData);
if(loading && isntEnoughData) {
attachCanPlay();
preloader.attach(mover, true);
// поставлю класс для плеера, чтобы убрать большую иконку пока прелоадер на месте
video.parentElement.classList.add('is-buffering');
}
});
attachCanPlay();
}
2020-04-08 17:46:43 +02:00
2020-08-27 20:25:47 +02:00
//if(!video.src || media.url != video.src) {
2020-06-16 22:48:08 +02:00
const load = () => {
2020-08-27 20:25:47 +02:00
const promise = media.supportsStreaming ? Promise.resolve() : appDocsManager.downloadDocNew(media);
2020-08-27 20:25:47 +02:00
if(!media.supportsStreaming) {
onAnimationEnd.then(() => {
preloader.attach(mover, true, promise);
});
}
2020-06-16 22:48:08 +02:00
2020-08-27 20:25:47 +02:00
(promise as Promise<any>).then(async() => {
if(this.currentMessageID != message.mid) {
this.log.warn('media viewer changed video');
return;
2020-04-08 17:46:43 +02:00
}
2020-06-16 22:48:08 +02:00
const url = media.url;
2020-08-27 20:25:47 +02:00
if(target instanceof SVGSVGElement/* && (video.parentElement || !isSafari) */) { // if video exists
//if(!video.parentElement) {
2020-06-16 22:48:08 +02:00
div.firstElementChild.lastElementChild.append(video);
2020-08-27 20:25:47 +02:00
//}
2020-06-16 22:48:08 +02:00
this.updateMediaSource(mover, url, 'video');
} else {
2020-06-16 22:48:08 +02:00
//const promise = new Promise((resolve) => video.addEventListener('loadeddata', resolve, {once: true}));
renderImageFromUrl(video, url);
2020-06-16 22:48:08 +02:00
//await promise;
2020-08-27 20:25:47 +02:00
/* const first = div.firstElementChild as HTMLImageElement;
2020-06-21 14:25:17 +02:00
if(!(first instanceof HTMLVideoElement) && first) {
2020-06-16 22:48:08 +02:00
first.remove();
}
2020-06-16 22:48:08 +02:00
if(!video.parentElement) {
div.prepend(video);
2020-08-27 20:25:47 +02:00
} */
}
2020-06-16 22:48:08 +02:00
// я хз что это такое, видео появляются просто чёрными и не включаются без этого кода снизу
/* source.remove();
2020-06-16 22:48:08 +02:00
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
//parent.append(video);
video.append(source);
});
}); */
2020-08-26 22:31:07 +02:00
createPlayer();
});
2020-04-08 17:46:43 +02:00
return promise;
};
2020-04-08 17:46:43 +02:00
this.lazyLoadQueue.unshift({
div: null,
load,
wasSeen: true
});
2020-08-27 20:25:47 +02:00
//} else createPlayer();
});
2020-02-06 16:43:07 +01:00
} else {
2020-08-27 20:25:47 +02:00
setMoverPromise = this.setMoverToTarget(target, false, fromRight).then(({onAnimationEnd}) => {
//return; // set and don't move
2020-02-15 12:14:58 +01:00
//if(wasActive) return;
2020-04-08 17:46:43 +02:00
//return;
let load = () => {
let cancellablePromise = appPhotosManager.preloadPhoto(media.id, size);
2020-08-27 20:25:47 +02:00
onAnimationEnd.then(() => {
this.preloader.attach(mover, true, cancellablePromise);
});
cancellablePromise.then(() => {
if(this.currentMessageID != message.mid) {
this.log.warn('media viewer changed photo');
return;
}
///////this.log('indochina', blob);
let url = media.url;
if(target instanceof SVGSVGElement) {
this.updateMediaSource(target, url, 'img');
this.updateMediaSource(mover, url, 'img');
} else {
let 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();
}
//this.log('will renderImageFromUrl:', image, div, target);
renderImageFromUrl(image, url, () => {
div.append(image);
});
}
this.preloader.detach();
}).catch(err => {
this.log.error(err);
});
return cancellablePromise;
};
this.lazyLoadQueue.unshift({
div: null,
load,
wasSeen: true
});
});
2020-02-06 16:43:07 +01:00
}
2020-08-27 20:25:47 +02:00
return this.setMoverPromise = setMoverPromise.catch(() => {
this.setMoverAnimationPromise = null;
}).finally(() => {
this.setMoverPromise = null;
});
2020-02-06 16:43:07 +01:00
}
}
export default new AppMediaViewer();