diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index 0c2c2a6d..4590cfca 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -51,6 +51,13 @@ import setInnerHTML from "../helpers/dom/setInnerHTML"; import { doubleRaf, fastRaf } from "../helpers/schedulers"; import { attachClickEvent } from "../helpers/dom/clickEvent"; import PopupDeleteMessages from "./popups/deleteMessages"; +import RangeSelector from "./rangeSelector"; +import attachGrabListeners, { GrabEvent } from "../helpers/dom/attachGrabListeners"; + +const ZOOM_STEP = 0.5; +const ZOOM_INITIAL_VALUE = 1; +const ZOOM_MIN_VALUE = 1; +const ZOOM_MAX_VALUE = 4; // TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию // TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода) @@ -63,8 +70,9 @@ class AppMediaViewerBase Promise; protected videoPlayer: VideoPlayer; + + protected zoomElements: { + container: HTMLElement, + btnOut: HTMLElement, + btnIn: HTMLElement, + rangeSelector: RangeSelector + } = {} as any; + // protected zoomValue = ZOOM_INITIAL_VALUE; + protected zoomSwipeHandler: SwipeHandler; + protected zoomSwipeStartX = 0; + protected zoomSwipeStartY = 0; + protected zoomSwipeX = 0; + protected zoomSwipeY = 0; + protected ctrlKeyDown: boolean; + constructor(topButtons: Array['buttons']>) { this.log = logger('AMV'); this.preloader = new ProgressivePreloader(); @@ -153,12 +176,34 @@ class AppMediaViewerBase { - const button = ButtonIcon(name, {noRipple: name === 'close' || undefined}); + topButtons.concat(['download', 'zoom', 'close']).forEach(name => { + const button = ButtonIcon(name, {noRipple: true}); this.buttons[name] = button; buttonsDiv.append(button); }); + this.buttons.zoom.classList.add('zoom-in'); + + // * zoom + this.zoomElements.container = document.createElement('div'); + this.zoomElements.container.classList.add('zoom-container'); + + this.zoomElements.btnOut = ButtonIcon('zoomout', {noRipple: true}); + this.zoomElements.btnOut.addEventListener('click', () => this.changeZoom(false)); + this.zoomElements.btnIn = ButtonIcon('zoomin', {noRipple: true}); + this.zoomElements.btnIn.addEventListener('click', () => this.changeZoom(true)); + + this.zoomElements.rangeSelector = new RangeSelector(ZOOM_STEP, ZOOM_INITIAL_VALUE + ZOOM_STEP, ZOOM_MIN_VALUE, ZOOM_MAX_VALUE, true); + this.zoomElements.rangeSelector.setListeners(); + this.zoomElements.rangeSelector.setHandlers({ + onScrub: this.setZoomValue, + onMouseUp: () => this.setZoomValue() + }); + + this.zoomElements.container.append(this.zoomElements.btnOut, this.zoomElements.rangeSelector.container, this.zoomElements.btnIn); + + this.wholeDiv.append(this.zoomElements.container); + // * content this.content.main = document.createElement('div'); this.content.main.classList.add(MEDIA_VIEWER_CLASSNAME + '-content'); @@ -187,7 +232,17 @@ class AppMediaViewerBase`; - this.wholeDiv.append(this.overlaysDiv, this.buttons.prev, this.buttons.next, this.topbar); + this.moversContainer = document.createElement('div'); + this.moversContainer.classList.add(MEDIA_VIEWER_CLASSNAME + '-movers'); + this.moversContainer.addEventListener('wheel', (e) => { + if(this.ctrlKeyDown) { + cancelEvent(e); + const scrollingUp = e.deltaY < 0; + this.changeZoom(!!scrollingUp); + } + }); + + this.wholeDiv.append(this.overlaysDiv, this.buttons.prev, this.buttons.next, this.topbar, this.moversContainer); // * constructing html end @@ -224,6 +279,8 @@ class AppMediaViewerBase this.toggleZoom()); + this.wholeDiv.addEventListener('click', this.onClick); if(isTouchSupported) { @@ -268,6 +325,80 @@ class AppMediaViewerBase { + lastDiffX = lastDiffY = 0; + this.moversContainer.classList.add('no-transition'); + }, + onSwipe: (xDiff, yDiff) => { + [xDiff, yDiff] = [xDiff * multiplier, yDiff * multiplier]; + this.zoomSwipeX += xDiff - lastDiffX; + this.zoomSwipeY += yDiff - lastDiffY; + [lastDiffX, lastDiffY] = [xDiff, yDiff]; + + this.setZoomValue(); + }, + onReset: () => { + this.moversContainer.classList.remove('no-transition'); + }, + cursor: 'move' + }); + } else { + this.zoomSwipeHandler.setListeners(); + } + + this.zoomElements.rangeSelector.setProgress(zoomValue); + } else if(!enable) { + this.zoomSwipeHandler.removeListeners(); + } + } + + protected changeZoom(add: boolean) { + this.zoomElements.rangeSelector.addProgress(ZOOM_STEP * (add ? 1 : -1)); + this.setZoomValue(); + } + + protected setZoomValue = (value = this.zoomElements.rangeSelector.value) => { + // this.zoomValue = value; + if(value === ZOOM_INITIAL_VALUE) { + this.zoomSwipeX = 0; + this.zoomSwipeY = 0; + } + + this.moversContainer.style.transform = `matrix(${value}, 0, 0, ${value}, ${this.zoomSwipeX}, ${this.zoomSwipeY})`; + + this.zoomElements.btnOut.classList.toggle('inactive', value === ZOOM_MIN_VALUE); + this.zoomElements.btnIn.classList.toggle('inactive', value === ZOOM_MAX_VALUE); + + this.toggleZoom(value > ZOOM_INITIAL_VALUE); + }; + + protected isZooming() { + return this.zoomElements.container.classList.contains('is-visible'); + } + protected setBtnMenuToggle(buttons: ButtonMenuItemOptions[]) { const btnMenuToggle = ButtonMenuToggle({onlyMobile: true}, 'bottom-left', buttons); this.topbar.append(btnMenuToggle); @@ -294,6 +425,11 @@ class AppMediaViewerBase { appSidebarRight.forwardTab.closeBtn.click(); @@ -301,6 +437,7 @@ class AppMediaViewerBase { this.wholeDiv.remove(); @@ -333,26 +470,56 @@ class AppMediaViewerBase { + const classNames = ['ckin__player', 'media-viewer-buttons', 'media-viewer-author', 'media-viewer-caption', 'zoom-container']; + if(isZooming) { + classNames.push('media-viewer-movers'); + } + + classNames.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') { + if(/* target === this.mediaViewerDiv */!mover || (!isZooming && (target.tagName === 'IMG' || target.tagName === 'image'))) { this.buttons.close.click(); } }; private onKeyDown = (e: KeyboardEvent) => { //this.log('onKeyDown', e); + if(rootScope.overlaysActive > 1) { + return; + } + let good = true; if(e.key === 'ArrowRight') { this.buttons.next.click(); } else if(e.key === 'ArrowLeft') { this.buttons.prev.click(); + } else { + good = false; + } + + if(e.ctrlKey || e.metaKey) { + this.ctrlKeyDown = true; + } + + if(good) { + cancelEvent(e); + } + }; + + private onKeyUp = (e: KeyboardEvent) => { + if(!(e.ctrlKey || e.metaKey)) { + this.ctrlKeyDown = false; + + if(this.isZooming()) { + this.setZoomValue(); + } } }; @@ -370,7 +537,8 @@ class AppMediaViewerBase 1 && closing)) */ this.removeCenterFromMover(mover); const wasActive = fromRight !== 0; @@ -444,6 +612,14 @@ class AppMediaViewerBase 1) { // 33 + // const diffX = (rect.width * zoomValue - rect.width) / 4; + const diffX = (rect.width * zoomValue - rect.width) / 2; + const diffY = (rect.height * zoomValue - rect.height) / 4; + // left -= diffX; + // top += diffY; + } + transform += `translate3d(${left}px,${top}px,0) `; /* if(wasActive) { @@ -485,6 +661,8 @@ class AppMediaViewerBase 1) { + const width = this.moversContainer.scrollWidth * scaleX; + const height = this.moversContainer.scrollHeight * scaleY; + const willBeLeft = appPhotosManager.windowW / 2 - rect.width / 2; + const willBeTop = appPhotosManager.windowH / 2 - rect.height / 2; + const left = rect.left - willBeLeft/* + (width - rect.width) / 2 */; + const top = rect.top - willBeTop/* + (height - rect.height) / 2 */; + this.moversContainer.style.transform = `matrix(${scaleX}, 0, 0, ${scaleY}, ${left}, ${top})`; + } else { + mover.style.transform = transform; + } needOpacity && (mover.style.opacity = '0'/* !closing ? '0' : '' */); @@ -663,6 +851,8 @@ class AppMediaViewerBase { mover.style.borderRadius = borderRadius; @@ -679,10 +869,12 @@ class AppMediaViewerBase setTimeout(resolve, 0)); //await new Promise((resolve) => window.requestAnimationFrame(resolve)); @@ -713,7 +905,7 @@ class AppMediaViewerBase { - mover.classList.remove('moving'); + mover.classList.remove('moving', 'opening'); if(aspecter) { // всё из-за видео, элементы управления скейлятся, так бы можно было этого не делать if(mover.querySelector('video') || true) { @@ -848,7 +1040,7 @@ class AppMediaViewerBase = Promise.resolve(); diff --git a/src/components/rangeSelector.ts b/src/components/rangeSelector.ts index fabff8de..6cd6d6a2 100644 --- a/src/components/rangeSelector.ts +++ b/src/components/rangeSelector.ts @@ -25,9 +25,12 @@ export default class RangeSelector { protected decimals: number; - constructor(protected step: number, protected value: number, protected min: number, protected max: number) { + constructor(protected step: number, value: number, protected min: number, protected max: number, withTransition = false) { this.container = document.createElement('div'); this.container.classList.add('progress-line'); + if(withTransition) { + this.container.classList.add('with-transition'); + } this.filled = document.createElement('div'); this.filled.classList.add('progress-line__filled'); @@ -54,6 +57,10 @@ export default class RangeSelector { this.container.append(this.filled, seek); } + get value() { + return +this.seek.value; + } + public setHandlers(events: RangeSelector['events']) { this.events = events; } @@ -86,8 +93,13 @@ export default class RangeSelector { }; public setProgress(value: number) { - this.setFilled(value); this.seek.value = '' + value; + this.setFilled(+this.seek.value); // clamp + } + + public addProgress(value: number) { + this.seek.value = '' + (+this.seek.value + value); + this.setFilled(+this.seek.value); // clamp } public setFilled(value: number) { diff --git a/src/components/swipeHandler.ts b/src/components/swipeHandler.ts index 6c769b3f..df3638d8 100644 --- a/src/components/swipeHandler.ts +++ b/src/components/swipeHandler.ts @@ -16,10 +16,11 @@ const attachGlobalListenerTo = window; export default class SwipeHandler { private element: HTMLElement; - private onSwipe: (xDiff: number, yDiff: number) => boolean; + private onSwipe: (xDiff: number, yDiff: number) => boolean | void; private verifyTouchTarget: (evt: TouchEvent | MouseEvent) => boolean; private onFirstSwipe: () => void; private onReset: () => void; + private cursor: 'grabbing' | 'move' = 'grabbing'; private hadMove = false; private xDown: number = null; @@ -31,9 +32,14 @@ export default class SwipeHandler { verifyTouchTarget?: SwipeHandler['verifyTouchTarget'], onFirstSwipe?: SwipeHandler['onFirstSwipe'], onReset?: SwipeHandler['onReset'], + cursor?: SwipeHandler['cursor'] }) { safeAssign(this, options); + this.setListeners(); + } + + public setListeners() { if(!isTouchSupported) { this.element.addEventListener('mousedown', this.handleStart, false); attachGlobalListenerTo.addEventListener('mouseup', this.reset); @@ -43,6 +49,16 @@ export default class SwipeHandler { } } + public removeListeners() { + if(!isTouchSupported) { + this.element.removeEventListener('mousedown', this.handleStart, false); + attachGlobalListenerTo.removeEventListener('mouseup', this.reset); + } else { + this.element.removeEventListener('touchstart', this.handleStart, false); + attachGlobalListenerTo.removeEventListener('touchend', this.reset); + } + } + reset = (e?: Event) => { /* if(e) { cancelEvent(e); @@ -101,7 +117,7 @@ export default class SwipeHandler { this.hadMove = true; if(!isTouchSupported) { - this.element.style.cursor = 'grabbing'; + this.element.style.cursor = this.cursor; } if(this.onFirstSwipe) { @@ -124,7 +140,8 @@ export default class SwipeHandler { // } /* reset values */ - if(this.onSwipe(xDiff, yDiff)) { + const onSwipeResult = this.onSwipe(xDiff, yDiff); + if(onSwipeResult !== undefined && onSwipeResult) { this.reset(); } }; diff --git a/src/helpers/dom/attachGrabListeners.ts b/src/helpers/dom/attachGrabListeners.ts index 986bfd75..3a70dbf9 100644 --- a/src/helpers/dom/attachGrabListeners.ts +++ b/src/helpers/dom/attachGrabListeners.ts @@ -6,7 +6,10 @@ export type GrabEvent = {x: number, y: number, isTouch?: boolean}; -export default function attachGrabListeners(element: HTMLElement, onStart: (position: GrabEvent) => void, onMove: (position: GrabEvent) => void, onEnd: (position: GrabEvent) => void) { +export default function attachGrabListeners(element: HTMLElement, + onStart: (position: GrabEvent) => void, + onMove: (position: GrabEvent) => void, + onEnd?: (position: GrabEvent) => void) { // * Mouse const onMouseMove = (event: MouseEvent) => { onMove({x: event.pageX, y: event.pageY}); diff --git a/src/scss/partials/_audio.scss b/src/scss/partials/_audio.scss index 6356f246..e27f75b2 100644 --- a/src/scss/partials/_audio.scss +++ b/src/scss/partials/_audio.scss @@ -488,11 +488,8 @@ .progress-line { --height: 2px; --border-radius: 4px; + --thumb-size: .75rem; flex: 1 1 auto; margin-left: 5px; - - &__seek { - --thumb-size: .75rem; - } } } diff --git a/src/scss/partials/_ckin.scss b/src/scss/partials/_ckin.scss index c95c19d8..c4928139 100644 --- a/src/scss/partials/_ckin.scss +++ b/src/scss/partials/_ckin.scss @@ -45,8 +45,6 @@ } .default { - border: 0 solid rgba(0, 0, 0, .2); - box-shadow: 0 0 20px rgba(0, 0, 0, .2); position: relative; font-size: 0; //overflow: hidden; @@ -134,8 +132,12 @@ &__filled { background: var(--primary-color); } + + &__loaded { + background-color: #fff; + } - &__loaded, & { + & { background: rgba(255, 255, 255, .38); } @@ -241,15 +243,12 @@ --color: #fff; margin: 0; width: 50px; + --thumb-size: 15px; // https://stackoverflow.com/a/4816050 html.is-ios & { display: none; } - - &__seek { - --thumb-size: 15px; - } } } @@ -268,6 +267,7 @@ video::-webkit-media-controls-enclosure { --color: var(--primary-color); --height: 5px; --border-radius: 6px; + --thumb-size: 13px; border-radius: var(--border-radius); height: var(--height); position: relative; @@ -285,12 +285,10 @@ video::-webkit-media-controls-enclosure { } &__seek { - --thumb-size: 13px; -webkit-appearance: none; -moz-appearance: none; background: transparent; width: 100%; - height: 100%; cursor: pointer; padding: 0; margin: 0; @@ -300,63 +298,42 @@ video::-webkit-media-controls-enclosure { &:focus { outline: none; - &::-webkit-slider-runnable-track { + /* &::-webkit-slider-runnable-track { background: transparent; } &::-moz-range-track { outline: none; - } - } - - &::-webkit-slider-runnable-track { - width: 100%; - cursor: pointer; - border-radius: 1.3px; - -webkit-appearance: none; + } */ } &::-webkit-slider-thumb { - height: var(--thumb-size); - width: var(--thumb-size); - border-radius: 50%; - background-color: var(--color); - cursor: pointer; - -webkit-appearance: none; - border: none; - //margin-left: -.5px; + display: none; } &::-moz-range-thumb { - height: var(--thumb-size); - width: var(--thumb-size); - border-radius: 50%; - background-color: var(--color); - cursor: pointer; - -webkit-appearance: none; - border: none; - //margin-left: -.5px; + display: none; } - &::-ms-thumb { - height: var(--thumb-size); - width: var(--thumb-size); - border-radius: 50%; - background-color: var(--color); - cursor: pointer; - -webkit-appearance: none; - border: none; - //margin-left: -.5px; + &::-moz-range-track { + display: none; } - &::-moz-range-track { - width: 100%; - height: 8.4px; - cursor: pointer; - border: 1px solid transparent; - background: transparent; - //border-radius: 1.3px; + &::-webkit-slider-runnable-track { + display: none; } + + /* &::-webkit-slider-thumb, + &::-moz-range-thumb, + &::-moz-range-track, + &::-webkit-slider-runnable-track { + -webkit-appearance: none; + background: transparent; + border-color: transparent; + color: transparent; + width: 0; + height: 0; + } */ } &__filled { @@ -366,6 +343,20 @@ video::-webkit-media-controls-enclosure { &:not(.progress-line__loaded) { background-color: var(--color); z-index: 1; + + &:after { + content: " "; + display: block; + height: var(--thumb-size); + width: var(--thumb-size); + border-radius: 50%; + background-color: var(--color); + cursor: pointer; + position: absolute; + right: 0; + top: 50%; + transform: translate(calc(var(--thumb-size) / 2), -50%); + } } } @@ -374,11 +365,19 @@ video::-webkit-media-controls-enclosure { background-color: var(--secondary-color); } - &__seek, &__filled, &__loaded { + &__seek, + &__filled, + &__loaded { border-radius: var(--border-radius); position: absolute; - height: 100%; top: 0; + bottom: 0; + } + + @include animation-level(2) { + &.with-transition .progress-line__filled { + transition: width .2s; + } } } diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index 69701b83..3b54f8cf 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -1123,17 +1123,14 @@ .progress-line { --height: 2px; + --color: var(--primary-color); --border-radius: 4px; + --thumb-size: 12px; background-color: #e6ecf0; &__filled { background-color: var(--primary-color); } - - &__seek { - --thumb-color: var(--primary-color); - --thumb-size: 12px; - } } } diff --git a/src/scss/partials/_mediaViewer.scss b/src/scss/partials/_mediaViewer.scss index f71c49ee..91d1b93d 100644 --- a/src/scss/partials/_mediaViewer.scss +++ b/src/scss/partials/_mediaViewer.scss @@ -4,6 +4,8 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +$inactive-opacity: .4; + .media-viewer { position: fixed; top: 0; @@ -119,7 +121,7 @@ word-break: break-word; overflow: hidden; text-overflow: ellipsis; - z-index: 5; + z-index: 4; bottom: .75rem; left: 0; right: 0; @@ -151,7 +153,7 @@ .media-viewer-whole.active & { html.no-touch & { - opacity: .4; + opacity: $inactive-opacity; &:hover { opacity: 1; @@ -255,7 +257,7 @@ &-mover/* , &-canvas */ { position: fixed!important; - z-index: 4; + // z-index: 4; //transition: .5s all; display: flex; justify-content: center; @@ -435,14 +437,15 @@ padding: 0 1.25rem; .btn-icon, .media-viewer-author { - color: #8b8b8b; + color: #fff; + opacity: $inactive-opacity; @include animation-level(2) { - transition: color var(--open-duration) ease-in-out; + transition: opacity var(--open-duration) ease-in-out, color var(--open-duration) ease-in-out, background-color var(--open-duration) ease-in-out; } @include hover() { - color: #fff; + opacity: 1; } } @@ -530,6 +533,9 @@ } */ .btn-menu-toggle { + color: rgba(255, 255, 255, $inactive-opacity); + opacity: 1; + &.menu-open { color: #fff; background-color: rgba(112, 117, 121, .2) !important; @@ -544,6 +550,19 @@ } } + &-movers { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 4; + + @include animation-level(2) { + transition: transform var(--open-duration); + } + } + /* &-switchers { position: relative; width: $large-screen; @@ -553,6 +572,74 @@ } */ } +.tgico-zoom { + &:before { + content: $tgico-zoomout; + } + + &.zoom-in:before { + content: $tgico-zoomin; + } +} + +.zoom-container { + width: 17.125rem; + height: 3.375rem; + background-color: rgba(0, 0, 0, .4); + border-radius: $border-radius-big; + padding: .5rem; + opacity: 1; + + display: flex; + align-items: center; + justify-content: space-between; + + position: absolute; + bottom: 1.25rem; + left: 50%; + transform: translateX(-50%); + z-index: 5; + + @include animation-level(2) { + transition: opacity var(--open-duration); + } + + .btn-icon { + color: #fff; + + &.inactive { + pointer-events: none; + opacity: $inactive-opacity; + } + } + + .progress-line { + --color: #fff; + --height: 2px; + flex: 1 1 auto; + margin: 0 1px; + + &:before { + opacity: 1; + } + } + + &:not(.is-visible), + .media-viewer-whole:not(.active) & { + opacity: 0; + pointer-events: none; + } + + &.is-visible { + opacity: 1; + + & ~ .media-viewer-caption { + opacity: 0 !important; + pointer-events: none; + } + } +} + .overlays { top: 0; left: 0;