Browse Source

Improved media viewer

master
morethanwords 4 years ago
parent
commit
f85fa8965b
  1. 40
      src/components/horizontalMenu.ts
  2. 15
      src/components/scrollable_new.ts
  3. 2
      src/components/slider.ts
  4. 14
      src/index.hbs
  5. 4
      src/lib/appManagers/appDialogsManager.ts
  6. 62
      src/lib/appManagers/appDocsManager.ts
  7. 289
      src/lib/appManagers/appMediaViewer.ts
  8. 3
      src/lib/config.ts
  9. 43
      src/lib/mediaPlayer.ts
  10. 15
      src/lib/mtproto/mtproto.service.ts
  11. 2
      src/pages/pagesManager.ts
  12. 6
      src/scss/partials/_ckin.scss
  13. 15
      src/scss/partials/_leftSidebar.scss
  14. 24
      src/scss/partials/_mediaViewer.scss
  15. 147
      src/scss/partials/_preloader.scss
  16. 6
      src/scss/partials/_rightSidebar.scss
  17. 42
      src/scss/partials/_slider.scss
  18. 136
      src/scss/style.scss

40
src/components/horizontalMenu.ts

@ -42,7 +42,7 @@ function slideTabs(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight @@ -42,7 +42,7 @@ function slideTabs(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight
tabContent.style.transform = '';
}
export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?: (id: number, tabContent: HTMLDivElement) => void, onTransitionEnd?: () => void, transitionTime = 300) {
export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?: (id: number, tabContent: HTMLDivElement) => void, onTransitionEnd?: () => void, transitionTime = 250) {
const hideTimeouts: {[id: number]: number} = {};
let prevTabContent: HTMLElement = null;
let prevId = -1;
@ -99,13 +99,7 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick? @@ -99,13 +99,7 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?
};
if(tabs) {
let activeStripe: HTMLSpanElement;
if(!tabs.classList.contains('no-stripe')) {
activeStripe = document.createElement('span');
activeStripe.classList.add('menu-horizontal__stripe');
tabs.append(activeStripe);
}
const useStripe = !tabs.classList.contains('no-stripe');
const tagName = 'LI';//tabs.firstElementChild.tagName;
tabs.addEventListener('click', function(e) {
@ -139,16 +133,28 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick? @@ -139,16 +133,28 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?
const prev = tabs.querySelector(tagName.toLowerCase() + '.active') as HTMLElement;
prev && prev.classList.remove('active');
if(activeStripe) {
const tabsRect = tabs.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
const width = 50;
activeStripe.style.cssText = `width: ${width}px; transform: translateX(${targetRect.left - tabsRect.left + ((targetRect.width - width) / 2)}px);`;
/* const textRect = target.firstElementChild.getBoundingClientRect();
activeStripe.style.cssText = `width: ${textRect.width + (2 * 2)}px; transform: translateX(${textRect.left - tabsRect.left}px);`; */
//activeStripe.style.transform = `scaleX(${textRect.width}) translateX(${(textRect.left - tabsRect.left) / textRect.width + 0.5}px)`;
//console.log('tabs click:', tabsRect, textRect);
// stripe from ZINCHUK
if(useStripe && prevId != -1) {
const indicator = target.querySelector('i')!;
const currentIndicator = target.parentElement.children[prevId].querySelector('i')!;
currentIndicator.classList.remove('animate');
indicator.classList.remove('animate');
// We move and resize our indicator so it repeats the position and size of the previous one.
const shiftLeft = currentIndicator.parentElement.parentElement.offsetLeft - indicator.parentElement.parentElement.offsetLeft;
const scaleFactor = currentIndicator.clientWidth / indicator.clientWidth;
indicator.style.transform = `translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`;
console.log(`translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`);
requestAnimationFrame(() => {
// Now we remove the transform to let it animate to its own position and size.
indicator.classList.add('animate');
indicator.style.transform = 'none';
});
}
// stripe END
target.classList.add('active');
selectTab(id);

15
src/components/scrollable_new.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { logger, LogLevels } from "../lib/logger";
import smoothscroll from '../vendor/smoothscroll';
import { touchSupport, isSafari } from "../lib/config";
import { touchSupport, isSafari, mediaSizes } from "../lib/config";
//import { isInDOM } from "../lib/utils";
(window as any).__forceSmoothScrollPolyfill__ = true;
smoothscroll.polyfill();
@ -207,7 +207,7 @@ export default class Scrollable { @@ -207,7 +207,7 @@ export default class Scrollable {
const binded = this.onScroll.bind(this);
window.addEventListener('resize', () => {
this.overflowContainer = window.innerWidth <= 720 && false ? document.documentElement : this.container;
this.overflowContainer = mediaSizes.isMobile && false ? document.documentElement : this.container;
this.onScroll();
});
this.container.addEventListener('scroll', binded, {passive: true, capture: true});
@ -221,7 +221,7 @@ export default class Scrollable { @@ -221,7 +221,7 @@ export default class Scrollable {
}
//this.onScroll();
this.overflowContainer = window.innerWidth <= 720 && false ? document.documentElement : this.container;
this.overflowContainer = mediaSizes.isMobile && false ? document.documentElement : this.container;
/* scrollables.set(this.container, this);
scrollsIntersector.observe(this.container); */
@ -342,8 +342,13 @@ export default class Scrollable { @@ -342,8 +342,13 @@ export default class Scrollable {
public checkForTriggers(container: HTMLElement) {
if(this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) return;
const scrollTop = container.scrollTop;
const maxScrollTop = container.scrollHeight - container.clientHeight;
const scrollHeight = container.scrollHeight;
if(!scrollHeight) { // незачем вызывать триггеры если блок пустой или не виден
return;
}
const {clientHeight, scrollTop} = container;
const maxScrollTop = scrollHeight - clientHeight;
//this.log('checkForTriggers:', scrollTop, maxScrollTop);

2
src/components/slider.ts

@ -7,7 +7,7 @@ export interface SliderTab { @@ -7,7 +7,7 @@ export interface SliderTab {
onCloseAfterTimeout?: () => void
}
const TRANSITIONTIME = 420;
const TRANSITIONTIME = 250;
export default class SidebarSlider {
protected _selectTab: (id: number) => void;

14
src/index.hbs

@ -212,9 +212,9 @@ @@ -212,9 +212,9 @@
<div class="sidebar-content">
<div id="chats-container">
<div class="folders-tabs-scrollable hide">
<nav class="menu-horizontal" id="folders-tabs">
<nav class="menu-horizontal no-stripe" id="folders-tabs">
<ul>
<li class="rp"><span>All</span><span class="unread-count"></span></li>
<li class="rp"><span>All<span class="unread-count"></span><i></i></span></li>
</ul>
</nav>
</div>
@ -629,11 +629,11 @@ @@ -629,11 +629,11 @@
<div class="content-container">
<nav class="profile-tabs menu-horizontal">
<ul>
<li class="profile-tabs-members rp" style="display: none;"><span>Members</span></li>
<li class="profile-tabs-media rp"><span>Media</span></li>
<li class="profile-tabs-docs rp"><span>Docs</span></li>
<li class="profile-tabs-links rp"><span>Links</span></li>
<li class="profile-tabs-audio rp"><span>Audio</span></li>
<li class="profile-tabs-members rp" style="display: none;"><span>Members<i></i></span></li>
<li class="profile-tabs-media rp"><span>Media<i></i></span></li>
<li class="profile-tabs-docs rp"><span>Docs<i></i></span></li>
<li class="profile-tabs-links rp"><span>Links<i></i></span></li>
<li class="profile-tabs-audio rp"><span>Audio<i></i></span></li>
</ul>
</nav>
<div class="profile-tabs-content tabs-container">

4
src/lib/appManagers/appDialogsManager.ts

@ -703,7 +703,9 @@ export class AppDialogsManager { @@ -703,7 +703,9 @@ export class AppDialogsManager {
span.innerHTML = RichTextProcessor.wrapEmojiText(filter.title);
const unreadSpan = document.createElement('span');
unreadSpan.classList.add('unread-count');
li.append(span, unreadSpan);
const i = document.createElement('i');
span.append(unreadSpan, i);
li.append(span);
ripple(li);
this.folders.menu.firstElementChild.append(li);

62
src/lib/appManagers/appDocsManager.ts

@ -232,68 +232,6 @@ class AppDocsManager { @@ -232,68 +232,6 @@ class AppDocsManager {
return getFileNameByLocation(this.getInput(doc, thumbSize), {fileName: doc.file_name});
}
public downloadDoc(docID: string | MTDocument, toFileEntry?: any): CancellablePromise<Blob> {
const doc = this.getDoc(docID);
if(doc._ == 'documentEmpty') {
return Promise.reject();
}
if(doc.downloaded && !toFileEntry) {
if(doc.url) return Promise.resolve(null);
/* const cachedBlob = apiFileManager.getCachedFile(inputFileLocation);
if(cachedBlob) {
return Promise.resolve(cachedBlob);
} */
}
if(this.downloadPromises[doc.id]) {
return this.downloadPromises[doc.id];
}
const deferred = deferredPromise<Blob>();
const url = this.getFileURL(doc);
fetch(url).then(res => res.blob())
/* downloadPromise */.then((blob) => {
if(blob) {
doc.downloaded = true;
if(doc.type == 'voice' && !opusDecodeController.isPlaySupported()/* && false */) {
let reader = new FileReader();
reader.onloadend = (e) => {
let uint8 = new Uint8Array(e.target.result as ArrayBuffer);
//console.log('sending uint8 to decoder:', uint8);
opusDecodeController.decode(uint8).then(result => {
doc.url = result.url;
deferred.resolve(blob);
}, (err) => {
delete doc.downloaded;
deferred.reject(err);
});
};
reader.readAsArrayBuffer(blob);
return;
} else if(doc.type && doc.sticker != 2) {
doc.url = url;
}
}
deferred.resolve(blob);
}, (e) => {
console.log('document download failed', e);
deferred.reject(e);
}).finally(() => {
//deferred.notify = downloadPromise.notify = deferred.cancel = downloadPromise.cancel = null;
});
return this.downloadPromises[doc.id] = deferred;
}
public downloadDocNew(docID: string | MTDocument/* , method: ResponseMethod = 'blob' */): DownloadBlob {
const doc = this.getDoc(docID);

289
src/lib/appManagers/appMediaViewer.ts

@ -13,6 +13,11 @@ import LazyLoadQueue from "../../components/lazyLoadQueue"; @@ -13,6 +13,11 @@ import LazyLoadQueue from "../../components/lazyLoadQueue";
import appForward from "../../components/appForward";
import { isSafari, mediaSizes } from "../config";
import appAudio from "../../components/appAudio";
import { deferredPromise } from "../polyfill";
import { MTDocument } from "../../types";
// TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию
// TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода)
export class AppMediaViewer {
public wholeDiv = document.querySelector('.media-viewer-whole') as HTMLDivElement;
@ -32,6 +37,7 @@ export class AppMediaViewer { @@ -32,6 +37,7 @@ export class AppMediaViewer {
public currentMessageID = 0;
private preloader: ProgressivePreloader = null;
private preloaderStreamable: ProgressivePreloader = null;
private lastTarget: HTMLElement = null;
private prevTargets: {
@ -43,8 +49,6 @@ export class AppMediaViewer { @@ -43,8 +49,6 @@ export class AppMediaViewer {
//private loadMore: () => void = null;
public log: ReturnType<typeof logger>;
public onKeyDownBinded: any;
public onClickBinded: any;
private peerID = 0;
private loadMediaPromiseUp: Promise<void> = null;
@ -58,18 +62,21 @@ export class AppMediaViewer { @@ -58,18 +62,21 @@ export class AppMediaViewer {
private pageEl = document.getElementById('page-chats') as HTMLDivElement;
private setMoverPromise: Promise<void>;
private setMoverAnimationPromise: Promise<void>;
private lazyLoadQueue: LazyLoadQueue;
constructor() {
this.log = logger('AMV');
this.preloader = new ProgressivePreloader();
this.preloaderStreamable = new ProgressivePreloader(undefined, false);
this.preloaderStreamable.preloader.classList.add('preloader-streamable');
this.lazyLoadQueue = new LazyLoadQueue(undefined, true);
parseMenuButtonsTo(this.buttons, this.wholeDiv.querySelectorAll(`[class*='menu']`) as NodeListOf<HTMLElement>);
this.onKeyDownBinded = this.onKeyDown.bind(this);
const close = (e: MouseEvent) => {
cancelEvent(e);
//this.overlaysDiv.classList.remove('active');
@ -97,10 +104,10 @@ export class AppMediaViewer { @@ -97,10 +104,10 @@ export class AppMediaViewer {
}, 200);
}
window.removeEventListener('keydown', this.onKeyDownBinded);
window.removeEventListener('keydown', this.onKeyDown);
};
[this.buttons.close, this.buttons["mobile-close"]].forEach(el => {
[this.buttons.close, this.buttons["mobile-close"], this.preloaderStreamable.preloader].forEach(el => {
el.addEventListener('click', close);
});
@ -130,25 +137,8 @@ export class AppMediaViewer { @@ -130,25 +137,8 @@ export class AppMediaViewer {
}
});
const download = (e: MouseEvent) => {
let message = appMessagesManager.getMessage(this.currentMessageID);
if(message.media.photo) {
appPhotosManager.savePhotoFile(message.media.photo);
} else {
let document: any = 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.id);
}
}
};
[this.buttons.download, this.buttons["menu-download"]].forEach(el => {
el.addEventListener('click', download);
el.addEventListener('click', this.onClickDownload);
});
const forward = (e: MouseEvent) => {
@ -159,31 +149,50 @@ export class AppMediaViewer { @@ -159,31 +149,50 @@ export class AppMediaViewer {
el.addEventListener('click', forward);
});
this.onClickBinded = (e: MouseEvent) => {
let 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();
}
};
this.wholeDiv.addEventListener('click', this.onClickBinded);
this.wholeDiv.addEventListener('click', this.onClick);
//this.content.mover.addEventListener('click', this.onClickBinded);
//this.content.mover.append(this.buttons.prev, this.buttons.next);
this.setNewMover();
}
public onKeyDown(e: KeyboardEvent) {
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) => {
//this.log('onKeyDown', e);
if(e.key == 'ArrowRight') {
@ -191,15 +200,17 @@ export class AppMediaViewer { @@ -191,15 +200,17 @@ export class AppMediaViewer {
} else if(e.key == 'ArrowLeft') {
this.buttons.prev.click();
}
}
};
public async setMoverToTarget(target: HTMLElement, closing = false, fromRight = 0) {
private async setMoverToTarget(target: HTMLElement, closing = false, fromRight = 0) {
const mover = this.content.mover;
if(!closing) {
mover.innerHTML = '';
//mover.append(this.buttons.prev, this.buttons.next);
}
this.removeCenterFromMover(mover);
const wasActive = fromRight !== 0;
@ -240,6 +251,17 @@ export class AppMediaViewer { @@ -240,6 +251,17 @@ export class AppMediaViewer {
top = rect.top;
}
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')) {
@ -260,15 +282,13 @@ export class AppMediaViewer { @@ -260,15 +282,13 @@ export class AppMediaViewer {
}
} else {
aspecter = document.createElement('div');
aspecter.classList.add('media-viewer-aspecter', 'disable-hover');
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});`;
}
transform += `translate(${left}px,${top}px) `;
mover.style.width = containerRect.width + 'px';
mover.style.height = containerRect.height + 'px';
@ -294,6 +314,13 @@ export class AppMediaViewer { @@ -294,6 +314,13 @@ export class AppMediaViewer {
let path: SVGPathElement;
const isOut = target.classList.contains('is-out');
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;
@ -323,6 +350,10 @@ export class AppMediaViewer { @@ -323,6 +350,10 @@ export class AppMediaViewer {
newSvg.setAttributeNS(null, 'width', '' + width);
newSvg.setAttributeNS(null, 'height', '' + height);
// нижние два свойства для масштабирования
newSvg.setAttributeNS(null, 'viewBox', `0 0 ${width} ${height}`);
newSvg.setAttributeNS(null, 'preserveAspectRatio', 'xMidYMid meet');
newSvg.insertAdjacentHTML('beforeend', target.firstElementChild.outerHTML.replace(clipID, newClipID));
newSvg.insertAdjacentHTML('beforeend', target.lastElementChild.outerHTML.replace(clipID, newClipID));
@ -393,6 +424,11 @@ export class AppMediaViewer { @@ -393,6 +424,11 @@ export class AppMediaViewer {
mover.classList.add(wasActive ? 'moving' : 'active');
});
} else {
/* if(mover.classList.contains('center')) {
mover.classList.remove('center');
void mover.offsetLeft; // reflow
} */
if(target instanceof SVGSVGElement) {
path = mover.querySelector('path');
@ -421,17 +457,21 @@ export class AppMediaViewer { @@ -421,17 +457,21 @@ export class AppMediaViewer {
mover.innerHTML = '';
mover.classList.remove('moving', 'active', 'hiding');
mover.style.display = 'none';
deferred.resolve();
}, delay);
return;
return ret;
}
//await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => window.requestAnimationFrame(resolve));
// чтобы проверить установленную позицию - раскомментировать
//throw '';
mover.style.transform = `translate(${containerRect.left}px,${containerRect.top}px) scale(1,1)`;
//mover.style.transform = `translate(-50%,-50%) scale(1,1)`;
if(aspecter) {
this.setFullAspect(aspecter, containerRect, rect);
@ -449,22 +489,33 @@ export class AppMediaViewer { @@ -449,22 +489,33 @@ export class AppMediaViewer {
mover.classList.remove('moving');
if(aspecter) { // всё из-за видео, элементы управления скейлятся, так бы можно было этого не делать
if(mover.querySelector('video')) {
if(mover.querySelector('video') || true) {
mover.classList.remove('active');
aspecter.style.cssText = '';
void mover.offsetLeft; // reflow
}
aspecter.classList.remove('disable-hover');
//aspecter.classList.remove('disable-hover');
}
// эти строки нужны для установки центральной позиции, в случае ресайза это будет нужно
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;
deferred.resolve();
}, delay);
if(path) {
this.sizeTailPath(path, containerRect, scaleX, delay, true, isOut, borderRadius);
}
return ret;
}
private setFullAspect(aspecter: HTMLDivElement, containerRect: DOMRect, rect: DOMRect) {
@ -493,7 +544,7 @@ export class AppMediaViewer { @@ -493,7 +544,7 @@ export class AppMediaViewer {
//}
}
public sizeTailPath(path: SVGPathElement, rect: DOMRect, scaleX: number, delay: number, upscale: boolean, isOut: boolean, borderRadius: string) {
private sizeTailPath(path: SVGPathElement, rect: DOMRect, scaleX: number, delay: number, upscale: boolean, isOut: boolean, borderRadius: string) {
const start = Date.now();
const {width, height} = rect;
delay = delay / 2;
@ -521,9 +572,21 @@ export class AppMediaViewer { @@ -521,9 +572,21 @@ export class AppMediaViewer {
step();
}
public moveTheMover(mover: HTMLDivElement, toLeft = true) {
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) {
const windowW = appPhotosManager.windowW;
this.removeCenterFromMover(mover);
//mover.classList.remove('active');
mover.classList.add('moving');
@ -534,9 +597,8 @@ export class AppMediaViewer { @@ -534,9 +597,8 @@ export class AppMediaViewer {
const rect = mover.getBoundingClientRect();
const newTransform = mover.style.transform.replace(/translate\((.+?),/, (match, p1) => {
/////////this.log('replace func', match, p1);
let x = +p1.slice(0, -2);
x = toLeft ? -rect.width : windowW;
const x = toLeft ? -rect.width : windowW;
//const x = toLeft ? -(rect.right + (rect.width / 2)) : windowW / 2;
return match.replace(p1, x + 'px');
});
@ -549,7 +611,7 @@ export class AppMediaViewer { @@ -549,7 +611,7 @@ export class AppMediaViewer {
}, 350);
}
public setNewMover() {
private setNewMover() {
const newMover = document.createElement('div');
newMover.classList.add('media-viewer-mover');
@ -560,8 +622,6 @@ export class AppMediaViewer { @@ -560,8 +622,6 @@ export class AppMediaViewer {
this.wholeDiv.append(newMover);
}
newMover.addEventListener('click', this.onClickBinded);
return this.content.mover = newMover;
}
@ -573,7 +633,7 @@ export class AppMediaViewer { @@ -573,7 +633,7 @@ export class AppMediaViewer {
} */
// нет смысла делать проверку для reverse и loadMediaPromise
public loadMoreMedia(older = true) {
private loadMoreMedia(older = true) {
//if(!older && this.reverse) return;
if(older && this.loadedAllMediaDown) return;
@ -648,7 +708,7 @@ export class AppMediaViewer { @@ -648,7 +708,7 @@ export class AppMediaViewer {
return promise;
}
public updateMediaSource(target: HTMLElement, url: string, tagName: 'video' | 'img') {
private updateMediaSource(target: HTMLElement, url: string, tagName: 'video' | 'img') {
//if(target instanceof SVGSVGElement) {
const el = target.querySelector(tagName) as HTMLElement;
renderImageFromUrl(el, url);
@ -737,8 +797,7 @@ export class AppMediaViewer { @@ -737,8 +797,7 @@ export class AppMediaViewer {
}
let oldAvatar = this.author.avatarEl;
// @ts-ignore
this.author.avatarEl = this.author.avatarEl.cloneNode();
this.author.avatarEl = (this.author.avatarEl.cloneNode() as AvatarElement);
this.author.avatarEl.setAttribute('peer', '' + message.fromID);
oldAvatar.parentElement.replaceChild(this.author.avatarEl, oldAvatar);
@ -749,7 +808,7 @@ export class AppMediaViewer { @@ -749,7 +808,7 @@ export class AppMediaViewer {
this.moveTheMover(this.content.mover, fromRight === 1);
this.setNewMover();
} else {
window.addEventListener('keydown', this.onKeyDownBinded);
window.addEventListener('keydown', this.onKeyDown);
this.wholeDiv.classList.add('active');
}
@ -767,18 +826,20 @@ export class AppMediaViewer { @@ -767,18 +826,20 @@ export class AppMediaViewer {
target = target.querySelector('img, video') || target;
} */
const preloader = media.supportsStreaming ? this.preloaderStreamable : this.preloader;
let setMoverPromise: Promise<void>;
if(isVideo) {
////////this.log('will wrap video', media, size);
setMoverPromise = this.setMoverToTarget(target, false, fromRight).then(() => {
setMoverPromise = this.setMoverToTarget(target, false, fromRight).then(({onAnimationEnd}) => {
//return; // set and don't move
//if(wasActive) return;
//return;
const div = mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter') ? mover.firstElementChild : mover;
const video = mover.querySelector('video') || document.createElement('video');
video.src = '';
//video.src = '';
video.setAttribute('playsinline', '');
if(media.type == 'gif') {
@ -787,58 +848,104 @@ export class AppMediaViewer { @@ -787,58 +848,104 @@ export class AppMediaViewer {
video.loop = true;
}
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';
if(!video.parentElement) {
div.append(video);
}
// fix for simultaneous play
appAudio.pause();
appAudio.willBePlayedAudio = null;
new VideoPlayer(video, true, media.supportsStreaming);
Promise.all([canPlayThrough, onAnimationEnd]).then(() => {
const player = new VideoPlayer(video, true, media.supportsStreaming);
/* div.append(video);
mover.append(player.wrapper); */
});
}
};
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();
}
if(!video.src || (media.url && media.url != video.src)) {
//if(!video.src || media.url != video.src) {
const load = () => {
const promise = appDocsManager.downloadDoc(media.id);
const promise = media.supportsStreaming ? Promise.resolve() : appDocsManager.downloadDocNew(media);
//if(!streamable) {
this.preloader.attach(mover, true, promise);
//}
if(!media.supportsStreaming) {
onAnimationEnd.then(() => {
preloader.attach(mover, true, promise);
});
}
promise.then(async() => {
(promise as Promise<any>).then(async() => {
if(this.currentMessageID != message.mid) {
this.log.warn('media viewer changed video');
return;
}
const url = media.url;
if(target instanceof SVGSVGElement && (video.parentElement || !isSafari)) { // if video exists
if(!video.parentElement) {
if(target instanceof SVGSVGElement/* && (video.parentElement || !isSafari) */) { // if video exists
//if(!video.parentElement) {
div.firstElementChild.lastElementChild.append(video);
}
//}
this.updateMediaSource(mover, url, 'video');
//this.updateMediaSource(target, url, 'source');
} else {
//const promise = new Promise((resolve) => video.addEventListener('loadeddata', resolve, {once: true}));
renderImageFromUrl(video, url);
//await promise;
const first = div.firstElementChild as HTMLImageElement;
/* const first = div.firstElementChild as HTMLImageElement;
if(!(first instanceof HTMLVideoElement) && first) {
first.remove();
}
if(!video.parentElement) {
div.prepend(video);
}
} */
}
// я хз что это такое, видео появляются просто чёрными и не включаются без этого кода снизу
@ -861,17 +968,19 @@ export class AppMediaViewer { @@ -861,17 +968,19 @@ export class AppMediaViewer {
load,
wasSeen: true
});
} else createPlayer();
//} else createPlayer();
});
} else {
setMoverPromise = this.setMoverToTarget(target, false, fromRight).then(() => {
setMoverPromise = this.setMoverToTarget(target, false, fromRight).then(({onAnimationEnd}) => {
//return; // set and don't move
//if(wasActive) return;
//return;
let load = () => {
let cancellablePromise = appPhotosManager.preloadPhoto(media.id, size);
this.preloader.attach(mover, true, cancellablePromise);
onAnimationEnd.then(() => {
this.preloader.attach(mover, true, cancellablePromise);
});
cancellablePromise.then(() => {
if(this.currentMessageID != message.mid) {
this.log.warn('media viewer changed photo');
@ -914,7 +1023,9 @@ export class AppMediaViewer { @@ -914,7 +1023,9 @@ export class AppMediaViewer {
});
}
return this.setMoverPromise = setMoverPromise.then(() => {
return this.setMoverPromise = setMoverPromise.catch(() => {
this.setMoverAnimationPromise = null;
}).finally(() => {
this.setMoverPromise = null;
});
}

3
src/lib/config.ts

@ -77,7 +77,8 @@ class MediaSizes { @@ -77,7 +77,8 @@ class MediaSizes {
private handleResize() {
const innerWidth = window.innerWidth;
this.isMobile = innerWidth <= 720;
//this.isMobile = innerWidth <= 720;
this.isMobile = innerWidth <= 896;
this.active = this.isMobile ? this.sizes.handhelds : this.sizes.desktop;
/* if(this.isMobile) {

43
src/lib/mediaPlayer.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { cancelEvent } from "./utils";
import { cancelEvent, whichChild } from "./utils";
export class ProgressLine {
public container: HTMLDivElement;
@ -179,7 +179,7 @@ export class MediaProgressLine extends ProgressLine { @@ -179,7 +179,7 @@ export class MediaProgressLine extends ProgressLine {
const scrubTime = super.scrub(e);
this.media.currentTime = scrubTime;
return scrubTime;
};
}
protected setLoadProgress() {
const buf = this.media.buffered;
@ -249,6 +249,9 @@ export default class VideoPlayer { @@ -249,6 +249,9 @@ export default class VideoPlayer {
public progress: MediaProgressLine;
private skin: string;
/* private videoParent: HTMLElement;
private videoWhichChild: number; */
constructor(public video: HTMLVideoElement, play = false, streamable = false) {
this.wrapper = document.createElement('div');
this.wrapper.classList.add('ckin__player');
@ -353,11 +356,19 @@ export default class VideoPlayer { @@ -353,11 +356,19 @@ export default class VideoPlayer {
this.togglePlay();
});
});
video.addEventListener('click', () => {
this.togglePlay();
});
/* player.addEventListener('click', (e) => {
if(e.target != player) {
return;
}
this.togglePlay();
}); */
video.addEventListener('play', () => {
this.updateButton(toggle);
});
@ -513,11 +524,21 @@ export default class VideoPlayer { @@ -513,11 +524,21 @@ export default class VideoPlayer {
public toggleFullScreen(fullScreenButton: HTMLElement) {
// alternative standard method
let player = this.wrapper;
const player = this.wrapper;
// @ts-ignore
if(!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) {
player.classList.add('ckin__fullscreen');
/* const videoParent = this.video.parentElement;
const videoWhichChild = whichChild(this.video);
const needVideoRemount = videoParent != player;
if(needVideoRemount) {
this.videoParent = videoParent;
this.videoWhichChild = videoWhichChild;
player.prepend(this.video);
} */
if(player.requestFullscreen) {
player.requestFullscreen();
@ -540,6 +561,18 @@ export default class VideoPlayer { @@ -540,6 +561,18 @@ export default class VideoPlayer {
fullScreenButton.setAttribute('title', 'Exit Full Screen');
} else {
player.classList.remove('ckin__fullscreen');
/* if(this.videoParent) {
const {videoWhichChild, videoParent} = this;
if(!videoWhichChild) {
videoParent.prepend(this.video);
} else {
videoParent.insertBefore(this.video, videoParent.children[videoWhichChild]);
}
this.videoParent = null;
this.videoWhichChild = -1;
} */
// @ts-ignore
if(document.cancelFullScreen) {

15
src/lib/mtproto/mtproto.service.ts

@ -257,12 +257,15 @@ const onFetch = (event: FetchEvent): void => { @@ -257,12 +257,15 @@ const onFetch = (event: FetchEvent): void => {
headers['Content-Range'] = `bytes ${offset}-${offset + ab.byteLength - 1}/${info.size || '*'}`;
headers['Content-Length'] = `${ab.byteLength}`;
}
resolve(new Response(ab, {
status: 206,
statusText: 'Partial Content',
headers,
}));
// simulate slow connection
//setTimeout(() => {
resolve(new Response(ab, {
status: 206,
statusText: 'Partial Content',
headers,
}));
//}, 2.5e3);
}).catch(err => {});
})
]));

2
src/pages/pagesManager.ts

@ -11,7 +11,7 @@ class PagesManager { @@ -11,7 +11,7 @@ class PagesManager {
constructor() {
this.pagesDiv = document.getElementById('auth-pages') as HTMLDivElement;
this.selectTab = horizontalMenu(null, this.pagesDiv.firstElementChild as HTMLDivElement, null, null, 420);
this.selectTab = horizontalMenu(null, this.pagesDiv.firstElementChild as HTMLDivElement, null, null);
}
public setPage(page: Page) {

6
src/scss/partials/_ckin.scss

@ -220,6 +220,12 @@ @@ -220,6 +220,12 @@
}
}
&.is-buffering {
> .toggle {
display: none !important;
}
}
}
@media (max-width: 480px) {

15
src/scss/partials/_leftSidebar.scss

@ -20,9 +20,21 @@ @@ -20,9 +20,21 @@
}
li {
padding: 9px 16px 7px 16px;
height: 43px;
padding: 0 16px;
display: flex;
justify-content: center;
align-items: center;
> span:first-child {
overflow: visible;
i {
bottom: calc(-.625rem - -2px);
padding-right: 1rem !important;
margin-left: -.5rem !important;
}
}
}
&__stripe {
@ -38,7 +50,6 @@ @@ -38,7 +50,6 @@
font-weight: 500;
color: white;
line-height: 22px;
margin-top: 3px;
min-width: 20px;
padding: 0 6px;

24
src/scss/partials/_mediaViewer.scss

@ -177,10 +177,15 @@ $move-duration: .35s; @@ -177,10 +177,15 @@ $move-duration: .35s;
//transition: .5s all;
left: 0;
top: 0;
/* left: 50%;
top: 50%; */
transform-origin: top left;
overflow: hidden;
//border-radius: 0;
max-width: 100%;
max-height: 100%;
.ckin__player {
width: 100%;
height: 100%;
@ -189,6 +194,11 @@ $move-duration: .35s; @@ -189,6 +194,11 @@ $move-duration: .35s;
top: 0;
}
> svg {
width: 100%;
height: 100%;
}
img, video {
width: 100%;
height: 100%;
@ -210,6 +220,20 @@ $move-duration: .35s; @@ -210,6 +220,20 @@ $move-duration: .35s;
transition: $move-duration transform ease;
}
/* &.center {
transition: none !important;
} */
&.no-transition {
transition: none !important;
}
&.center {
left: 50% !important;
top: 50% !important;
transform: translate(-50%, -50%) !important;
}
&.hiding {
img, video {
transition: $open-duration opacity;

147
src/scss/partials/_preloader.scss

@ -0,0 +1,147 @@ @@ -0,0 +1,147 @@
.preloader {
&-circular {
animation: rotate 2s linear infinite;
height: 100%;
transform-origin: center center;
/* width: 100%; */
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
&-path {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
animation: dash 1.5s ease-in-out infinite/* , color 6s ease-in-out infinite */;
stroke-linecap: round;
stroke: white;
stroke-width: 3;
}
&-container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
width: 50px;
height: 50px;
/* cursor: pointer; */
}
}
.preloader-container {
.you-spin-me-round {
width: 100%;
height: 100%;
animation: rotate 2s linear infinite;
}
.preloader-circular {
animation: none;
cursor: pointer;
background-color: rgba(0, 0, 0, .7);
border-radius: 50%;
width: 100%;
height: 100%;
}
.preloader-path-new {
stroke-dasharray: 5, 200;
stroke-dashoffset: 0;
transition: stroke-dasharray 400ms ease-in-out;
stroke-linecap: round;
stroke: white;
stroke-width: 1.5;
}
&.preloader-swing {
cursor: default;
.preloader-circular {
cursor: default;
}
.preloader-path-new {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
animation: dashNew 1.5s ease-in-out infinite/* , color 6s ease-in-out infinite */;
}
}
.preloader-close {
cursor: pointer;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
color: #fff;
stroke: #fff;
width: 34%;
height: 34%;
html.no-touch &:hover {
background: none;
}
}
&.preloader-streamable {
&, svg {
cursor: pointer !important;
}
&:after {
content: "";
position: absolute;
width: .8125rem;
height: .8125rem;
border-radius: .125rem;
background-color: #fff;
left: 50%;
top: 50%;
transform: translate3d(-50%,-50%,0);
}
}
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35px;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -124px;
}
}
@keyframes dashNew {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35px;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -286%;
}
}

6
src/scss/partials/_rightSidebar.scss

@ -191,6 +191,7 @@ @@ -191,6 +191,7 @@
height: 120px;
margin: 1px auto 10px;
font-size: 2.5rem !important;
flex: 0 0 auto;
@include respond-to(handhelds) {
margin: 0 auto 10px;
@ -211,6 +212,11 @@ @@ -211,6 +212,11 @@
z-index: 2;
background-color: #fff;
i {
padding-right: 1.5rem !important;
margin-left: -.75rem !important;
}
&-content {
//min-height: 100%;
min-height: calc(100% - 49px);

42
src/scss/partials/_slider.scss

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
$slider-time: .25s;
.menu-horizontal {
color: $color-gray;
border-bottom: 1px solid $lightgrey;
@ -27,14 +29,24 @@ @@ -27,14 +29,24 @@
font-weight: 500;
position: relative;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
border-top-right-radius: 6px;
> span {
position: relative;
display: inline-flex;
align-items: center;
}
&.active {
color: $color-blue;
i {
opacity: 1;
}
}
}
&__stripe {
/* &__stripe {
position: absolute;
background: $color-blue;
left: 0;
@ -52,6 +64,26 @@ @@ -52,6 +64,26 @@
@include respond-to(handhelds) {
display: none;
}
} */
i {
position: absolute;
bottom: calc(-.625rem - 2px);
left: 0;
opacity: 0;
background-color: $color-blue;
height: .1875rem;
width: 100%;
border-radius: .1875rem .1875rem 0 0;
pointer-events: none;
padding-right: .5rem;
margin-left: -.25rem;
box-sizing: content-box;
transform-origin: left;
&.animate {
transition: transform $slider-time;
}
}
}
@ -74,7 +106,7 @@ @@ -74,7 +106,7 @@
width: 100%;
max-width: 100%;
//transition: transform .42s, filter .42s;
transition: transform .2s, filter .2s;
transition: transform $slider-time, filter $slider-time;
display: none;
flex-direction: column;
position: relative;
@ -98,4 +130,8 @@ @@ -98,4 +130,8 @@
position: relative;
}
}
&[data-slider="tabs"] {
transition: transform $slider-time;
}
}

136
src/scss/style.scss

@ -16,13 +16,16 @@ $lightgrey: #dadce0; @@ -16,13 +16,16 @@ $lightgrey: #dadce0;
$light: rgba($color-gray, 0.08);
$small-screen: 720px;
//$small-screen: 720px;
$small-screen: 896px;
//$small-screen: 900px;
$large-screen: 1680px;
//$large-screen: 16800px;
@mixin respond-to($media) {
@if $media == handhelds {
@media only screen and (max-width: $small-screen) { @content; }
//@media only screen and (orientation: landscape) and (max-device-width: 896px) { @content; } // iPhone 11 Pro Max
}
@else if $media == medium-screens {
@media only screen and (min-width: $small-screen + 1) and (max-width: $large-screen) { @content; }
@ -49,6 +52,7 @@ $large-screen: 1680px; @@ -49,6 +52,7 @@ $large-screen: 1680px;
@import "partials/slider";
@import "partials/selector";
@import "partials/gifsMasonry";
@import "partials/preloader";
@import "partials/popups/popup";
@import "partials/popups/editAvatar";
@ -1188,136 +1192,6 @@ input:focus, button:focus { @@ -1188,136 +1192,6 @@ input:focus, button:focus {
}
}
.preloader {
&-circular {
animation: rotate 2s linear infinite;
height: 100%;
transform-origin: center center;
/* width: 100%; */
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
&-path {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
animation: dash 1.5s ease-in-out infinite/* , color 6s ease-in-out infinite */;
stroke-linecap: round;
stroke: white;
stroke-width: 3;
}
&-container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
width: 50px;
height: 50px;
/* cursor: pointer; */
}
}
.preloader-container {
.you-spin-me-round {
width: 100%;
height: 100%;
animation: rotate 2s linear infinite;
}
.preloader-circular {
animation: none;
cursor: pointer;
background-color: rgba(0, 0, 0, .7);
border-radius: 50%;
width: 100%;
height: 100%;
}
.preloader-path-new {
stroke-dasharray: 5, 200;
stroke-dashoffset: 0;
transition: stroke-dasharray 400ms ease-in-out;
stroke-linecap: round;
stroke: white;
stroke-width: 1.5;
}
&.preloader-swing {
cursor: default;
.preloader-circular {
cursor: default;
}
.preloader-path-new {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
animation: dashNew 1.5s ease-in-out infinite/* , color 6s ease-in-out infinite */;
}
}
.preloader-close {
cursor: pointer;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
color: #fff;
stroke: #fff;
width: 34%;
height: 34%;
html.no-touch &:hover {
background: none;
}
}
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35px;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -124px;
}
}
@keyframes dashNew {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35px;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -286%;
}
}
.emoji {
display: inline-block;
/* width: 100%;

Loading…
Cancel
Save