Browse Source

Media viewer improvements:

Resizing content
Mobile video player
Minor fixes
master
morethanwords 4 years ago
parent
commit
2d6b25a0a4
  1. 2
      src/index.hbs
  2. 84
      src/lib/appManagers/appMediaViewer.ts
  3. 16
      src/lib/appManagers/appSidebarRight.ts
  4. 81
      src/lib/mediaPlayer.ts
  5. 16
      src/scss/partials/_ckin.scss
  6. 85
      src/scss/partials/_mediaViewer.scss
  7. 1
      src/scss/style.scss

2
src/index.hbs

@ -144,7 +144,6 @@ @@ -144,7 +144,6 @@
<div class="overlays">
<div class="media-viewer">
<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>
<div class="media-viewer-name"></div>
<div class="media-viewer-date"></div>
@ -164,6 +163,7 @@ @@ -164,6 +163,7 @@
</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-menu bottom-left">
<div class="btn-menu-item menu-menu-forward tgico-forward rp">Forward</div>

84
src/lib/appManagers/appMediaViewer.ts

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

16
src/lib/appManagers/appSidebarRight.ts

@ -226,20 +226,24 @@ export class AppSidebarRight extends SidebarSlider { @@ -226,20 +226,24 @@ export class AppSidebarRight extends SidebarSlider {
});
this.sharedMedia.contentMedia.addEventListener('click', (e) => {
let target = e.target as HTMLDivElement;
const target = e.target as HTMLDivElement;
let messageID = +target.dataset.mid;
const messageID = +target.dataset.mid;
if(!messageID) {
this.log.warn('no messageID by click on target:', target);
return;
}
let message = appMessagesManager.getMessage(messageID);
const message = appMessagesManager.getMessage(messageID);
let ids = Object.keys(this.mediaDivsByIDs).map(k => +k).sort((a, b) => a - b);
let idx = ids.findIndex(i => i == messageID);
const ids = Object.keys(this.mediaDivsByIDs).map(k => +k).sort((a, b) => a - b);
const idx = ids.findIndex(i => i == messageID);
let targets = ids.map(id => ({element: this.mediaDivsByIDs[id]/* .firstElementChild */ as HTMLElement, mid: id}));
const targets = ids.map(id => {
const element = this.mediaDivsByIDs[id] as HTMLElement;
//element = element.querySelector('img') || element;
return {element, mid: id};
});
appMediaViewer.openMedia(message, target, false, this.sidebarEl, targets.slice(idx + 1).reverse(), targets.slice(0, idx).reverse(), true);
});

81
src/lib/mediaPlayer.ts

@ -2,6 +2,8 @@ import { cancelEvent } from "./utils"; @@ -2,6 +2,8 @@ import { cancelEvent } from "./utils";
import { touchSupport } from "./config";
import appMediaPlaybackController from "../components/appMediaPlaybackController";
type SUPEREVENT = MouseEvent | TouchEvent;
export class ProgressLine {
public container: HTMLDivElement;
protected filled: HTMLDivElement;
@ -46,17 +48,17 @@ export class ProgressLine { @@ -46,17 +48,17 @@ export class ProgressLine {
this.events = events;
}
onMouseMove = (e: MouseEvent) => {
onMouseMove = (e: SUPEREVENT) => {
this.mousedown && this.scrub(e);
};
onMouseDown = (e: MouseEvent) => {
onMouseDown = (e: SUPEREVENT) => {
this.scrub(e);
this.mousedown = true;
this.events?.onMouseDown && this.events.onMouseDown(e);
};
onMouseUp = (e: MouseEvent) => {
onMouseUp = (e: SUPEREVENT) => {
this.mousedown = false;
this.events?.onMouseUp && this.events.onMouseUp(e);
};
@ -65,6 +67,12 @@ export class ProgressLine { @@ -65,6 +67,12 @@ export class ProgressLine {
this.container.addEventListener('mousemove', this.onMouseMove);
this.container.addEventListener('mousedown', this.onMouseDown);
this.container.addEventListener('mouseup', this.onMouseUp);
if(touchSupport) {
this.container.addEventListener('touchmove', this.onMouseMove);
this.container.addEventListener('touchstart', this.onMouseDown);
this.container.addEventListener('touchend', this.onMouseUp);
}
}
public setProgress(scrubTime: number) {
@ -78,8 +86,16 @@ export class ProgressLine { @@ -78,8 +86,16 @@ export class ProgressLine {
this.filled.style.transform = 'scaleX(' + scaleX + ')';
}
protected scrub(e: MouseEvent) {
const scrubTime = e.offsetX / this.container.offsetWidth * this.duration;
protected scrub(e: SUPEREVENT) {
let offsetX: number;
if(e instanceof TouchEvent) {
const rect = (e.target as HTMLElement).getBoundingClientRect();
offsetX = e.targetTouches[0].pageX - rect.left;
} else {
offsetX = e.offsetX;
}
const scrubTime = offsetX / this.container.offsetWidth * this.duration;
this.setFilled(scrubTime);
@ -92,6 +108,12 @@ export class ProgressLine { @@ -92,6 +108,12 @@ export class ProgressLine {
this.container.removeEventListener('mousedown', this.onMouseDown);
this.container.removeEventListener('mouseup', this.onMouseUp);
if(touchSupport) {
this.container.removeEventListener('touchmove', this.onMouseMove);
this.container.removeEventListener('touchstart', this.onMouseDown);
this.container.removeEventListener('touchend', this.onMouseUp);
}
this.events = {};
}
}
@ -119,7 +141,7 @@ export class MediaProgressLine extends ProgressLine { @@ -119,7 +141,7 @@ export class MediaProgressLine extends ProgressLine {
this.setSeekMax();
this.setListeners();
this.setHandlers({
onMouseDown: (e: MouseEvent) => {
onMouseDown: (e: SUPEREVENT) => {
//super.onMouseDown(e);
//Таймер для того, чтобы стопать видео, если зажал мышку и не отпустил клик
@ -133,7 +155,7 @@ export class MediaProgressLine extends ProgressLine { @@ -133,7 +155,7 @@ export class MediaProgressLine extends ProgressLine {
}, 150);
},
onMouseUp: (e: MouseEvent) => {
onMouseUp: (e: SUPEREVENT) => {
//super.onMouseUp(e);
if(this.stopAndScrubTimeout) {
@ -379,8 +401,41 @@ export default class VideoPlayer { @@ -379,8 +401,41 @@ export default class VideoPlayer {
video.addEventListener('click', () => {
if(!touchSupport) {
this.togglePlay();
return;
}
});
if(touchSupport) {
let showControlsTimeout = 0;
const t = () => {
showControlsTimeout = setTimeout(() => {
showControlsTimeout = 0;
player.classList.remove('show-controls');
}, 3e3);
};
player.addEventListener('click', () => {
if(showControlsTimeout) {
clearTimeout(showControlsTimeout);
} else {
player.classList.add('show-controls');
}
t();
});
player.addEventListener('touchstart', () => {
player.classList.add('show-controls');
clearTimeout(showControlsTimeout);
});
player.addEventListener('touchend', () => {
if(player.classList.contains('is-playing')) {
t();
}
});
}
/* player.addEventListener('click', (e) => {
if(e.target != player) {
@ -398,6 +453,10 @@ export default class VideoPlayer { @@ -398,6 +453,10 @@ export default class VideoPlayer {
});
video.addEventListener('dblclick', () => {
if(touchSupport) {
return;
}
return this.toggleFullScreen(fullScreenButton);
})
@ -540,13 +599,17 @@ export default class VideoPlayer { @@ -540,13 +599,17 @@ export default class VideoPlayer {
`;
}
}
public static isFullScreen(): boolean {
// @ts-ignore
return !!(document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
}
public toggleFullScreen(fullScreenButton: HTMLElement) {
// alternative standard method
const player = this.wrapper;
// @ts-ignore
if(!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) {
if(!VideoPlayer.isFullScreen()) {
player.classList.add('ckin__fullscreen');
/* const videoParent = this.video.parentElement;

16
src/scss/partials/_ckin.scss

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

85
src/scss/partials/_mediaViewer.scss

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

1
src/scss/style.scss

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

Loading…
Cancel
Save