optimize gallery preview

This commit is contained in:
Weidi Deng 2021-01-04 21:37:31 +08:00
parent 43e0d4a856
commit 61a5d9a069
5 changed files with 346 additions and 13 deletions

View File

@ -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;
}

View 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>

View File

@ -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 {

View File

@ -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;
}

View File

@ -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('/')