optimize gallery preview
This commit is contained in:
parent
43e0d4a856
commit
61a5d9a069
@ -4,6 +4,7 @@
|
||||
ref="container"
|
||||
@touchstart="touchStart"
|
||||
@touchmove="touchMove"
|
||||
@touchend="touchEnd"
|
||||
@dblclick="zoomAuto"
|
||||
@mousedown="mousedownStart"
|
||||
@mousemove="mouseMove"
|
||||
@ -29,7 +30,7 @@ export default {
|
||||
},
|
||||
minScale: {
|
||||
type: Number,
|
||||
default: () => 0.25
|
||||
default: () => 1
|
||||
},
|
||||
classList: {
|
||||
type: Array,
|
||||
@ -50,6 +51,8 @@ export default {
|
||||
lastX: null,
|
||||
lastY: null,
|
||||
inDrag: false,
|
||||
touches: 0,
|
||||
navOffset: 50,
|
||||
lastTouchDistance: 0,
|
||||
moveDisabled: false,
|
||||
disabledTimer: null,
|
||||
@ -79,12 +82,69 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
src: function () {
|
||||
this.scale = 1
|
||||
this.setZoom()
|
||||
this.setCenter()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fit() {
|
||||
let img = this.$refs.imgex
|
||||
|
||||
const wScale = window.innerWidth / img.clientWidth
|
||||
const hScale = window.innerHeight / img.clientHeight
|
||||
|
||||
this.scale = wScale < hScale? wScale: hScale
|
||||
this.minScale = this.scale
|
||||
this.setZoom()
|
||||
},
|
||||
refit() {
|
||||
const target = this.fitScreenTarget()
|
||||
this.doMove(target[0], target[1])
|
||||
},
|
||||
fitScreenTarget() {
|
||||
if (this.scale <= this.minScale) {
|
||||
let style = this.$refs.imgex.style
|
||||
let posX = this.pxStringToNumber(style.left)
|
||||
let posY = this.pxStringToNumber(style.top)
|
||||
return [this.position.center.x - posX, this.position.center.y - posY]
|
||||
}
|
||||
else {
|
||||
let img = this.$refs.imgex
|
||||
|
||||
const rect = img.getBoundingClientRect()
|
||||
const width = window.innerWidth
|
||||
const height = window.innerHeight
|
||||
|
||||
let x = 0,y = 0
|
||||
|
||||
// left out of viewport
|
||||
if (rect.left < 0 && rect.right < width) x = width - rect.right
|
||||
|
||||
// right out of viewport
|
||||
else if (rect.left > 0 && rect.right > width) x = -rect.left
|
||||
|
||||
// top out of viewport
|
||||
if (rect.top < 0 && rect.bottom < height) y = height - rect.bottom
|
||||
|
||||
// bottom out of viewport
|
||||
else if (rect.top > 0 && rect.bottom > height) y = -rect.top
|
||||
|
||||
return [x,y]
|
||||
}
|
||||
},
|
||||
checkNav(x) {
|
||||
if (this.scale <= this.minScale) {
|
||||
if (x > this.navOffset) this.$root.$emit('gallery-nav', 0)
|
||||
else if (x < -this.navOffset) this.$root.$emit('gallery-nav', 1)
|
||||
} else {
|
||||
let img = this.$refs.imgex
|
||||
|
||||
const rect = img.getBoundingClientRect()
|
||||
const width = window.innerWidth
|
||||
|
||||
if (rect.left > this.navOffset && rect.right > width + this.navOffset) this.$root.$emit('gallery-nav', 0)
|
||||
else if (rect.left < - this.navOffset && rect.right < width - this.navOffset) this.$root.$emit('gallery-nav', 1)
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
let img = this.$refs.imgex
|
||||
|
||||
@ -102,6 +162,7 @@ export default {
|
||||
},
|
||||
onMouseUp() {
|
||||
this.inDrag = false
|
||||
this.refit()
|
||||
},
|
||||
onResize: throttle(function() {
|
||||
if (this.imageLoaded) {
|
||||
@ -113,11 +174,13 @@ export default {
|
||||
let container = this.$refs.container
|
||||
let img = this.$refs.imgex
|
||||
|
||||
this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2)
|
||||
this.position.center.y = Math.floor((container.clientHeight - img.clientHeight) / 2)
|
||||
this.position.center.x = Math.floor((window.innerWidth - img.clientWidth) / 2 - container.offsetLeft)
|
||||
this.position.center.y = Math.floor((window.innerHeight - img.clientHeight) / 2 - container.offsetTop)
|
||||
|
||||
img.style.left = this.position.center.x + 'px'
|
||||
img.style.top = this.position.center.y + 'px'
|
||||
|
||||
this.fit()
|
||||
},
|
||||
mousedownStart(event) {
|
||||
this.lastX = null
|
||||
@ -128,6 +191,7 @@ export default {
|
||||
mouseMove(event) {
|
||||
if (!this.inDrag) return
|
||||
this.doMove(event.movementX, event.movementY)
|
||||
this.checkNav(event.movementX)
|
||||
event.preventDefault()
|
||||
},
|
||||
mouseUp(event) {
|
||||
@ -138,6 +202,15 @@ export default {
|
||||
this.lastX = null
|
||||
this.lastY = null
|
||||
this.lastTouchDistance = null
|
||||
|
||||
setTimeout(() => {
|
||||
this.touches = 0
|
||||
}, 300)
|
||||
|
||||
this.touches++
|
||||
if (this.touches > 1) {
|
||||
this.zoomAuto(event)
|
||||
}
|
||||
event.preventDefault()
|
||||
},
|
||||
zoomAuto(event) {
|
||||
@ -192,6 +265,12 @@ export default {
|
||||
this.lastX = event.targetTouches[0].pageX
|
||||
this.lastY = event.targetTouches[0].pageY
|
||||
this.doMove(x, y)
|
||||
this.checkNav(x)
|
||||
}
|
||||
},
|
||||
touchEnd(event) {
|
||||
if (event.targetTouches.length === 0) {
|
||||
this.refit()
|
||||
}
|
||||
},
|
||||
doMove(x, y) {
|
||||
@ -231,7 +310,6 @@ export default {
|
||||
<style>
|
||||
.image-ex-container {
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
218
frontend/src/components/files/ImagePreview.vue
Normal file
218
frontend/src/components/files/ImagePreview.vue
Normal file
@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div id="previewer">
|
||||
<div class="image-bar">
|
||||
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
|
||||
<div class="title">{{ this.name }}</div>
|
||||
|
||||
<preview-size-button v-if="isResizeEnabled" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading"></preview-size-button>
|
||||
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||
<i class="material-icons">more_vert</i>
|
||||
</button>
|
||||
|
||||
<div id="dropdown" :class="{ active : showMore }">
|
||||
<rename-button :disabled="loading" v-if="user.perm.rename"></rename-button>
|
||||
<delete-button :disabled="loading" v-if="user.perm.delete"></delete-button>
|
||||
<download-button :disabled="loading" v-if="user.perm.download"></download-button>
|
||||
<info-button :disabled="loading"></info-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loading" v-if="loading">
|
||||
<div class="spinner">
|
||||
<div class="bounce1"></div>
|
||||
<div class="bounce2"></div>
|
||||
<div class="bounce3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
|
||||
<i class="material-icons">chevron_left</i>
|
||||
</button>
|
||||
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
|
||||
<i class="material-icons">chevron_right</i>
|
||||
</button>
|
||||
|
||||
<template v-if="!loading">
|
||||
<div class="preview">
|
||||
<ExtendedImage :src="raw"></ExtendedImage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-show="showMore" @click="resetPrompts" class="overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import { baseURL, resizePreview } from '@/utils/constants'
|
||||
import { files as api } from '@/api'
|
||||
import PreviewSizeButton from '@/components/buttons/PreviewSize'
|
||||
import InfoButton from '@/components/buttons/Info'
|
||||
import DeleteButton from '@/components/buttons/Delete'
|
||||
import RenameButton from '@/components/buttons/Rename'
|
||||
import DownloadButton from '@/components/buttons/Download'
|
||||
import ExtendedImage from './ExtendedImage'
|
||||
|
||||
export default {
|
||||
name: 'image-preview',
|
||||
components: {
|
||||
PreviewSizeButton,
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
RenameButton,
|
||||
DownloadButton,
|
||||
ExtendedImage
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
previousLink: '',
|
||||
nextLink: '',
|
||||
listing: null,
|
||||
name: '',
|
||||
fullSize: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'user', 'oldReq', 'jwt', 'loading', 'show']),
|
||||
hasPrevious () {
|
||||
return (this.previousLink !== '')
|
||||
},
|
||||
hasNext () {
|
||||
return (this.nextLink !== '')
|
||||
},
|
||||
download () {
|
||||
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||
},
|
||||
previewUrl () {
|
||||
if (!this.fullSize) {
|
||||
return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||
}
|
||||
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||
},
|
||||
raw () {
|
||||
return `${this.previewUrl}&inline=true`
|
||||
},
|
||||
showMore () {
|
||||
return this.$store.state.show === 'more'
|
||||
},
|
||||
isResizeEnabled () {
|
||||
return resizePreview
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.updatePreview()
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
window.addEventListener('keydown', this.key)
|
||||
this.$store.commit('setPreviewMode', true)
|
||||
this.listing = this.oldReq.items
|
||||
this.$root.$on('preview-deleted', this.deleted)
|
||||
this.$root.$on('gallery-nav', this.nav)
|
||||
this.updatePreview()
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.key)
|
||||
this.$store.commit('setPreviewMode', false)
|
||||
this.$root.$off('preview-deleted', this.deleted)
|
||||
this.$root.$off('gallery-nav', this.nav)
|
||||
},
|
||||
methods: {
|
||||
nav(e) {
|
||||
if (e===0 && this.hasPrevious) this.prev()
|
||||
else if (e===1 && this.hasNext) this.next()
|
||||
},
|
||||
deleted () {
|
||||
this.listing = this.listing.filter(item => item.name !== this.name)
|
||||
|
||||
if (this.hasNext) {
|
||||
this.next()
|
||||
} else if (!this.hasPrevious && !this.hasNext) {
|
||||
this.back()
|
||||
} else {
|
||||
this.prev()
|
||||
}
|
||||
},
|
||||
back () {
|
||||
this.$store.commit('setPreviewMode', false)
|
||||
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||
this.$router.push({ path: uri })
|
||||
},
|
||||
prev () {
|
||||
this.$router.push({ path: this.previousLink })
|
||||
},
|
||||
next () {
|
||||
this.$router.push({ path: this.nextLink })
|
||||
},
|
||||
key (event) {
|
||||
|
||||
if (this.show !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.which === 13 || event.which === 39) { // right arrow
|
||||
if (this.hasNext) this.next()
|
||||
} else if (event.which === 37) { // left arrow
|
||||
if (this.hasPrevious) this.prev()
|
||||
}
|
||||
},
|
||||
async updatePreview () {
|
||||
if (this.req.subtitles) {
|
||||
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
|
||||
}
|
||||
|
||||
let dirs = this.$route.fullPath.split("/")
|
||||
this.name = decodeURIComponent(dirs[dirs.length - 1])
|
||||
|
||||
if (!this.listing) {
|
||||
try {
|
||||
const path = url.removeLastDir(this.$route.path)
|
||||
const res = await api.fetch(path)
|
||||
this.listing = res.items
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
}
|
||||
|
||||
this.previousLink = ''
|
||||
this.nextLink = ''
|
||||
|
||||
for (let i = 0; i < this.listing.length; i++) {
|
||||
if (this.listing[i].name !== this.name) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
if (this.listing[j].type === 'image') {
|
||||
this.previousLink = this.listing[j].url
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = i + 1; j < this.listing.length; j++) {
|
||||
if (this.listing[j].type === 'image') {
|
||||
this.nextLink = this.listing[j].url
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
},
|
||||
openMore () {
|
||||
this.$store.commit('showHover', 'more')
|
||||
},
|
||||
resetPrompts () {
|
||||
this.$store.commit('closeHovers')
|
||||
},
|
||||
toggleSize () {
|
||||
this.fullSize = !this.fullSize
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -37,8 +37,7 @@
|
||||
|
||||
<template v-if="!loading">
|
||||
<div class="preview">
|
||||
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
|
||||
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
|
||||
<audio v-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
|
||||
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
|
||||
<track
|
||||
kind="captions"
|
||||
@ -71,10 +70,8 @@ import InfoButton from '@/components/buttons/Info'
|
||||
import DeleteButton from '@/components/buttons/Delete'
|
||||
import RenameButton from '@/components/buttons/Rename'
|
||||
import DownloadButton from '@/components/buttons/Download'
|
||||
import ExtendedImage from './ExtendedImage'
|
||||
|
||||
const mediaTypes = [
|
||||
"image",
|
||||
"video",
|
||||
"audio",
|
||||
"blob"
|
||||
@ -87,8 +84,7 @@ export default {
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
RenameButton,
|
||||
DownloadButton,
|
||||
ExtendedImage
|
||||
DownloadButton
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
|
||||
@ -139,6 +139,41 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#previewer .image-bar {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
padding: 0.5em;
|
||||
height: 3.7em;
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
|
||||
#previewer .image-bar:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#previewer .image-bar > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
#previewer .image-bar .title {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
padding: 0 1em;
|
||||
line-height: 2.3em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.2em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#previewer .action {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#previewer .action i {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
<forbidden v-else-if="error.message === '403'"></forbidden>
|
||||
<internal-error v-else></internal-error>
|
||||
</div>
|
||||
<image-preview v-else-if="isImagePreview"></image-preview>
|
||||
<preview v-else-if="isPreview"></preview>
|
||||
<editor v-else-if="isEditor"></editor>
|
||||
<listing :class="{ multiple }" v-else-if="isListing"></listing>
|
||||
@ -32,6 +33,7 @@ import Forbidden from './errors/403'
|
||||
import NotFound from './errors/404'
|
||||
import InternalError from './errors/500'
|
||||
import Preview from '@/components/files/Preview'
|
||||
import ImagePreview from "@/components/files/ImagePreview"
|
||||
import Listing from '@/components/files/Listing'
|
||||
import { files as api } from '@/api'
|
||||
import { mapGetters, mapState, mapMutations } from 'vuex'
|
||||
@ -43,6 +45,7 @@ function clean (path) {
|
||||
export default {
|
||||
name: 'files',
|
||||
components: {
|
||||
ImagePreview,
|
||||
Forbidden,
|
||||
NotFound,
|
||||
InternalError,
|
||||
@ -65,8 +68,11 @@ export default {
|
||||
'loading',
|
||||
'show'
|
||||
]),
|
||||
isImagePreview () {
|
||||
return (!this.loading && !this.isListing && !this.isEditor || this.loading && this.$store.state.previewMode) && this.req.type === 'image'
|
||||
},
|
||||
isPreview () {
|
||||
return !this.loading && !this.isListing && !this.isEditor || this.loading && this.$store.state.previewMode
|
||||
return (!this.loading && !this.isListing && !this.isEditor || this.loading && this.$store.state.previewMode) && this.req.type !== 'image'
|
||||
},
|
||||
breadcrumbs () {
|
||||
let parts = this.$route.path.split('/')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user