Merge remote-tracking branch 'tusooa/from/develop/tusooa/media-touch-actions' into shigusegubu

* tusooa/from/develop/tusooa/media-touch-actions: (26 commits)
  Lint
  Prevent hiding media viewer if swiped over SwipeClick
  Fix webkit image blurs
  Fix video in media modal not displaying properly
  Add changelog for https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1403
  Remove image box-shadow in media modal
  Clean up debug code for image pinch zoom
  Bump @kazvmoe-infra/pinch-zoom-element to 1.2.0 on npm
  Bump pinch-zoom-element version
  Clean up
  Check whether we swiped only for mouse pointer
  Scale swipe threshold with viewport width
  Update pinch-zoom-element
  Allow pinch-zoom to fill the whole screen
  Use native click for hiding overlay
  Reset position on swipe end even if we cannot navigate
  Make lint happy
  Prevent the click event from firing on content below modal
  Add missing swipe click component
  Clean up
  ...
This commit is contained in:
Henry Jameson 2022-02-21 16:59:08 +02:00
commit 5db8112cd5
11 changed files with 372 additions and 43 deletions

View file

@ -23,19 +23,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Attachments are truncated just like post contents
- Media modal now also displays description and counter position in gallery (i.e. 1/5)
- Ability to rearrange order of attachments when uploading
- Enabled users to zoom and pan images in media viewer with mouse and touch
## [2.4.2] - 2022-01-09
### Added
### Added
- Added Apply and Reset buttons to the bottom of theme tab to minimize UI travel
- Implemented user option to always show floating New Post button (normally mobile-only)
- Display reasons for instance specific policies
- Display reasons for instance specific policies
- Added functionality to cancel follow request
### Fixed
- Fixed link to external profile not working on user profiles
- Fixed mobile shoutbox display
- Fixed mobile shoutbox display
- Fixed favicon badge not working in Chrome
- Escape html more properly in subject/display name
- Escape html more properly in subject/display name
## [2.4.0] - 2021-08-08

View file

@ -22,6 +22,7 @@
"@fortawesome/free-regular-svg-icons": "^5.15.1",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/vue-fontawesome": "^2.0.0",
"@kazvmoe-infra/pinch-zoom-element": "^1.2.0",
"body-scroll-lock": "^2.6.4",
"chromatism": "^3.0.0",
"cropperjs": "^1.4.3",

View file

@ -1,9 +1,11 @@
import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue'
import Modal from '../modal/modal.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
import PinchZoom from '../pinch_zoom/pinch_zoom.vue'
import SwipeClick from '../swipe_click/swipe_click.vue'
import GestureService from '../../services/gesture_service/gesture_service'
import Flash from 'src/components/flash/flash.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronLeft,
@ -21,12 +23,21 @@ const MediaModal = {
components: {
StillImage,
VideoAttachment,
PinchZoom,
SwipeClick,
Modal,
Flash
},
data () {
return {
loading: false
loading: false,
swipeDirection: GestureService.DIRECTION_LEFT,
swipeThreshold: () => {
const considerableMoveRatio = 1 / 4
return window.innerWidth * considerableMoveRatio
},
pinchZoomMinScale: 1,
pinchZoomScaleResetLimit: 1.2
}
},
computed: {
@ -52,32 +63,26 @@ const MediaModal = {
return this.currentMedia ? this.getType(this.currentMedia) : null
}
},
created () {
this.mediaSwipeGestureRight = GestureService.swipeGesture(
GestureService.DIRECTION_RIGHT,
this.goPrev,
50
)
this.mediaSwipeGestureLeft = GestureService.swipeGesture(
GestureService.DIRECTION_LEFT,
this.goNext,
50
)
},
methods: {
getType (media) {
return fileTypeService.fileType(media.mimetype)
},
mediaTouchStart (e) {
GestureService.beginSwipe(e, this.mediaSwipeGestureRight)
GestureService.beginSwipe(e, this.mediaSwipeGestureLeft)
},
mediaTouchMove (e) {
GestureService.updateSwipe(e, this.mediaSwipeGestureRight)
GestureService.updateSwipe(e, this.mediaSwipeGestureLeft)
},
hide () {
this.$store.dispatch('closeMediaViewer')
// HACK: Closing immediately via a touch will cause the click
// to be processed on the content below the overlay
const transitionTime = 100 // ms
setTimeout(() => {
this.$store.dispatch('closeMediaViewer')
}, transitionTime)
},
hideIfNotSwiped (event) {
// If we have swiped over SwipeClick, do not trigger hide
const comp = this.$refs.swipeClick
if (!comp) {
this.hide()
} else {
comp.$gesture.click(event)
}
},
goPrev () {
if (this.canNavigate) {
@ -102,6 +107,17 @@ const MediaModal = {
onImageLoaded () {
this.loading = false
},
handleSwipePreview (offsets) {
this.$refs.pinchZoom.setTransform({ scale: 1, x: offsets[0], y: 0 })
},
handleSwipeEnd (sign) {
this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 })
if (sign > 0) {
this.goNext()
} else if (sign < 0) {
this.goPrev()
}
},
handleKeyupEvent (e) {
if (this.showing && e.keyCode === 27) { // escape
this.hide()

View file

@ -2,20 +2,38 @@
<Modal
v-if="showing"
class="media-modal-view"
@backdropClicked="hide"
@backdropClicked="hideIfNotSwiped"
>
<img
<SwipeClick
v-if="type === 'image'"
:class="{ loading }"
class="modal-image"
:src="currentMedia.url"
:alt="currentMedia.description"
:title="currentMedia.description"
@touchstart.stop="mediaTouchStart"
@touchmove.stop="mediaTouchMove"
@click="hide"
@load="onImageLoaded"
ref="swipeClick"
class="modal-image-container"
:direction="swipeDirection"
:threshold="swipeThreshold"
@preview-requested="handleSwipePreview"
@swipe-finished="handleSwipeEnd"
@swipeless-clicked="hide"
>
<PinchZoom
ref="pinchZoom"
class="modal-image-container-inner"
selector=".modal-image"
reach-min-scale-strategy="reset"
stop-propagate-handled="stop-propgate-handled"
:allow-pan-min-scale="pinchZoomMinScale"
:min-scale="pinchZoomMinScale"
:reset-to-min-scale-limit="pinchZoomScaleResetLimit"
>
<img
:class="{ loading }"
class="modal-image"
:src="currentMedia.url"
:alt="currentMedia.description"
:title="currentMedia.description"
@load="onImageLoaded"
>
</PinchZoom>
</SwipeClick>
<VideoAttachment
v-if="type === 'video'"
class="modal-image"
@ -103,6 +121,7 @@
opacity: 1;
}
}
overflow: hidden;
}
.media-modal-view {
@ -115,6 +134,29 @@
}
}
.modal-image-container {
display: flex;
overflow: hidden;
align-items: center;
flex-direction: column;
max-width: 90%;
max-height: 95%;
width: 100%;
height: 100%;
flex-grow: 1;
justify-content: center;
&-inner {
width: 100%;
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
}
.description,
.counter {
/* Hardcoded since background is also hardcoded */
@ -134,9 +176,8 @@
}
.modal-image {
max-width: 90%;
max-height: 90%;
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
max-width: 100%;
max-height: 100%;
image-orientation: from-image; // NOTE: only FF supports this
animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein;

View file

@ -0,0 +1,13 @@
import PinchZoom from '@kazvmoe-infra/pinch-zoom-element'
export default {
methods: {
setTransform ({ scale, x, y }) {
this.$el.setTransform({ scale, x, y })
}
},
created () {
// Make lint happy
(() => PinchZoom)()
}
}

View file

@ -0,0 +1,11 @@
<template>
<pinch-zoom
class="pinch-zoom-parent"
v-bind="$attrs"
v-on="$listeners"
>
<slot />
</pinch-zoom>
</template>
<script src="./pinch_zoom.js"></script>

View file

@ -0,0 +1,84 @@
import GestureService from '../../services/gesture_service/gesture_service'
/**
* props:
* direction: a vector that indicates the direction of the intended swipe
* threshold: the minimum distance in pixels the swipe has moved on `direction'
* for swipe-finished() to have a non-zero sign
* perpendicularTolerance: see gesture_service
*
* Events:
* preview-requested(offsets)
* Emitted when the pointer has moved.
* offsets: the offsets from the start of the swipe to the current cursor position
*
* swipe-canceled()
* Emitted when the swipe has been canceled due to a pointercancel event.
*
* swipe-finished(sign: 0|-1|1)
* Emitted when the swipe has finished.
* sign: if the swipe does not meet the threshold, 0
* if the swipe meets the threshold in the positive direction, 1
* if the swipe meets the threshold in the negative direction, -1
*
* swipeless-clicked()
* Emitted when there is a click without swipe.
* This and swipe-finished() cannot be emitted for the same pointerup event.
*/
const SwipeClick = {
props: {
direction: {
type: Array
},
threshold: {
type: Function,
default: () => 30
},
perpendicularTolerance: {
type: Number,
default: 1.0
}
},
methods: {
handlePointerDown (event) {
this.$gesture.start(event)
},
handlePointerMove (event) {
this.$gesture.move(event)
},
handlePointerUp (event) {
this.$gesture.end(event)
},
handlePointerCancel (event) {
this.$gesture.cancel(event)
},
handleNativeClick (event) {
this.$gesture.click(event)
},
preview (offsets) {
this.$emit('preview-requested', offsets)
},
end (sign) {
this.$emit('swipe-finished', sign)
},
click () {
this.$emit('swipeless-clicked')
},
cancel () {
this.$emit('swipe-canceled')
}
},
created () {
this.$gesture = new GestureService.SwipeAndClickGesture({
direction: this.direction,
threshold: this.threshold,
perpendicularTolerance: this.perpendicularTolerance,
swipePreviewCallback: this.preview,
swipeEndCallback: this.end,
swipeCancelCallback: this.cancel,
swipelessClickCallback: this.click
})
}
}
export default SwipeClick

View file

@ -0,0 +1,14 @@
<template>
<div
v-bind="$attrs"
@pointerdown="handlePointerDown"
@pointermove="handlePointerMove"
@pointerup="handlePointerUp"
@pointercancel="handlePointerCancel"
@click="handleNativeClick"
>
<slot />
</div>
</template>
<script src="./swipe_click.js"></script>

View file

@ -45,6 +45,8 @@ Vue.use(VueClickOutside)
Vue.use(PortalVue)
Vue.use(VBodyScrollLock)
Vue.config.ignoredElements = ['pinch-zoom']
Vue.component('FAIcon', FontAwesomeIcon)
Vue.component('FALayers', FontAwesomeLayers)

View file

@ -4,9 +4,15 @@ const DIRECTION_RIGHT = [1, 0]
const DIRECTION_UP = [0, -1]
const DIRECTION_DOWN = [0, 1]
const BUTTON_LEFT = 0
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY])
const touchCoord = touch => [touch.screenX, touch.screenY]
const touchEventCoord = e => touchCoord(e.touches[0])
const pointerEventCoord = e => [e.clientX, e.clientY]
const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1])
@ -61,6 +67,132 @@ const updateSwipe = (event, gesture) => {
gesture._swiping = false
}
class SwipeAndClickGesture {
// swipePreviewCallback(offsets: Array[Number])
// offsets: the offset vector which the underlying component should move, from the starting position
// swipeEndCallback(sign: 0|-1|1)
// sign: if the swipe does not meet the threshold, 0
// if the swipe meets the threshold in the positive direction, 1
// if the swipe meets the threshold in the negative direction, -1
constructor ({
direction,
// swipeStartCallback
swipePreviewCallback,
swipeEndCallback,
swipeCancelCallback,
swipelessClickCallback,
threshold = 30,
perpendicularTolerance = 1.0,
disableClickThreshold = 1
}) {
const nop = () => {}
this.direction = direction
this.swipePreviewCallback = swipePreviewCallback || nop
this.swipeEndCallback = swipeEndCallback || nop
this.swipeCancelCallback = swipeCancelCallback || nop
this.swipelessClickCallback = swipelessClickCallback || nop
this.threshold = typeof threshold === 'function' ? threshold : () => threshold
this.disableClickThreshold = typeof disableClickThreshold === 'function' ? disableClickThreshold : () => disableClickThreshold
this.perpendicularTolerance = perpendicularTolerance
this._reset()
}
_reset () {
this._startPos = [0, 0]
this._pointerId = -1
this._swiping = false
this._swiped = false
this._preventNextClick = false
}
start (event) {
// Only handle left click
if (event.button !== BUTTON_LEFT) {
return
}
this._startPos = pointerEventCoord(event)
this._pointerId = event.pointerId
this._swiping = true
this._swiped = false
}
move (event) {
if (this._swiping && this._pointerId === event.pointerId) {
this._swiped = true
const coord = pointerEventCoord(event)
const delta = deltaCoord(this._startPos, coord)
this.swipePreviewCallback(delta)
}
}
cancel (event) {
if (!this._swiping || this._pointerId !== event.pointerId) {
return
}
this.swipeCancelCallback()
}
end (event) {
if (!this._swiping) {
return
}
if (this._pointerId !== event.pointerId) {
return
}
this._swiping = false
// movement too small
const coord = pointerEventCoord(event)
const delta = deltaCoord(this._startPos, coord)
const sign = (() => {
if (vectorLength(delta) < this.threshold()) {
return 0
}
// movement is opposite from direction
const isPositive = dotProduct(delta, this.direction) > 0
// movement perpendicular to direction is too much
const towardsDir = project(delta, this.direction)
const perpendicularDir = perpendicular(this.direction)
const towardsPerpendicular = project(delta, perpendicularDir)
if (
vectorLength(towardsDir) * this.perpendicularTolerance <
vectorLength(towardsPerpendicular)
) {
return 0
}
return isPositive ? 1 : -1
})()
if (this._swiped) {
this.swipeEndCallback(sign)
}
this._reset()
// Only a mouse will fire click event when
// the end point is far from the starting point
// so for other kinds of pointers do not check
// whether we have swiped
if (vectorLength(delta) >= this.disableClickThreshold() && event.pointerType === 'mouse') {
this._preventNextClick = true
}
}
click (event) {
if (!this._preventNextClick) {
this.swipelessClickCallback()
}
this._reset()
}
}
const GestureService = {
DIRECTION_LEFT,
DIRECTION_RIGHT,
@ -68,7 +200,8 @@ const GestureService = {
DIRECTION_DOWN,
swipeGesture,
beginSwipe,
updateSwipe
updateSwipe,
SwipeAndClickGesture
}
export default GestureService

View file

@ -915,6 +915,13 @@
resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.0.tgz#63da3e459147cebb0a8d58eed81d6071db9f5973"
integrity sha512-N3VKw7KzRfOm8hShUVldpinlm13HpvLBQgT63QS+aCrIRLwjoEUXY5Rcmttbfb6HkzZaeqjLqd/aZCQ53UjQpg==
"@kazvmoe-infra/pinch-zoom-element@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@kazvmoe-infra/pinch-zoom-element/-/pinch-zoom-element-1.2.0.tgz#eb3ca34c53b4410c689d60aca02f4a497ce84aba"
integrity sha512-HBrhH5O/Fsp2bB7EGTXzCsBAVcMjknSagKC5pBdGpKsF8meHISR0kjDIdw4YoE0S+0oNMwJ6ZUZyIBrdywxPPw==
dependencies:
pointer-tracker "^2.0.3"
"@nodelib/fs.scandir@2.1.3":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
@ -7004,6 +7011,11 @@ pngjs@^3.3.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b"
pointer-tracker@^2.0.3:
version "2.4.0"
resolved "https://registry.yarnpkg.com/pointer-tracker/-/pointer-tracker-2.4.0.tgz#78721c2d2201486db11ec1094377f03023b621b3"
integrity sha512-pWI2tpaM/XNtc9mUTv42Rmjf6mkHvE8LT5DDEq0G7baPNhxNM9E3CepubPplSoSLk9E5bwQrAMyDcPVmJyTW4g==
portal-vue@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.4.tgz#1fc679d77e294dc8d026f1eb84aa467de11b392e"