Skip to content

Commit

Permalink
enhancement: preview image presentation
Browse files Browse the repository at this point in the history
use a more user friendly image preview and take care that images are no longer cropped
  • Loading branch information
fschade committed Oct 15, 2023
1 parent 8a05291 commit 62b9985
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 85 deletions.
@@ -0,0 +1,8 @@
Enhancement: Preview image presentation

We've updated the preview app to have a more user friendly image browsing experience,
image zooming, rotation and movement is smoother, images are no longer cropped.

https://github.com/owncloud/web/pull/9806
https://github.com/owncloud/ocis/pull/7409
https://github.com/owncloud/web/issues/7728
1 change: 1 addition & 0 deletions packages/web-app-preview/package.json
Expand Up @@ -5,6 +5,7 @@
"description": "ownCloud Web Preview",
"license": "AGPL-3.0",
"devDependencies": {
"@panzoom/panzoom": "^4.5.1",
"web-test-helpers": "workspace:*"
},
"peerDependencies": {
Expand Down
121 changes: 51 additions & 70 deletions packages/web-app-preview/src/App.vue
Expand Up @@ -29,50 +29,49 @@
:accessible-label="$gettext('Failed to load media file')"
/>
<template v-else>
<div
v-show="activeMediaFileCached"
class="oc-width-1-1 oc-flex oc-flex-center oc-flex-middle oc-p-s oc-box-shadow-medium preview-player"
:class="{ lightbox: isFullScreenModeActivated }"
>
<media-image
v-if="activeMediaFileCached.isImage"
:file="activeMediaFileCached"
<div class="stage" :class="{ lightbox: isFullScreenModeActivated }">
<div v-show="activeMediaFileCached" class="stage_media">
<media-image
v-if="activeMediaFileCached.isImage"
:file="activeMediaFileCached"
:current-image-rotation="currentImageRotation"
:current-image-zoom="currentImageZoom"
/>
<media-video
v-else-if="activeMediaFileCached.isVideo"
:file="activeMediaFileCached"
:is-auto-play-enabled="isAutoPlayEnabled"
/>
<media-audio
v-else-if="activeMediaFileCached.isAudio"
:file="activeMediaFileCached"
:is-auto-play-enabled="isAutoPlayEnabled"
/>
</div>
<media-controls
class="stage_controls"
:files="filteredFiles"
:active-index="activeIndex"
:is-full-screen-mode-activated="isFullScreenModeActivated"
:is-folder-loading="isFolderLoading"
:is-image="activeMediaFileCached?.isImage"
:current-image-rotation="currentImageRotation"
:current-image-zoom="currentImageZoom"
/>
<media-video
v-else-if="activeMediaFileCached.isVideo"
:file="activeMediaFileCached"
:is-auto-play-enabled="isAutoPlayEnabled"
/>
<media-audio
v-else-if="activeMediaFileCached.isAudio"
:file="activeMediaFileCached"
:is-auto-play-enabled="isAutoPlayEnabled"
@set-rotation="currentImageRotation = $event"
@set-zoom="currentImageZoom = $event"
@toggle-full-screen="toggleFullscreenMode"
@toggle-previous="prev"
@toggle-next="next"
/>
</div>
</template>
<media-controls
:files="filteredFiles"
:active-index="activeIndex"
:is-full-screen-mode-activated="isFullScreenModeActivated"
:is-folder-loading="isFolderLoading"
:is-image="activeMediaFileCached?.isImage"
:current-image-rotation="currentImageRotation"
:current-image-zoom="currentImageZoom"
@set-rotation="currentImageRotation = $event"
@set-zoom="currentImageZoom = $event"
@toggle-full-screen="toggleFullscreenMode"
@toggle-previous="prev"
@toggle-next="next"
/>
</main>
</template>
<script lang="ts">
import { computed, defineComponent, ref, unref } from 'vue'
import { RouteLocationRaw } from 'vue-router'
import { Resource } from '@ownclouders/web-client/src'
import { AppTopBar } from '@ownclouders/web-pkg'
import { AppTopBar, ProcessorType } from '@ownclouders/web-pkg'
import {
queryItemAsString,
sortHelper,
Expand Down Expand Up @@ -443,7 +442,8 @@ export default defineComponent({
return this.$previewService.loadPreview({
space: unref(this.currentFileContext.space),
resource: file,
dimensions: [this.thumbDimensions, this.thumbDimensions] as [number, number]
dimensions: [this.thumbDimensions, this.thumbDimensions] as [number, number],
processor: ProcessorType.enum.fit
})
},
preloadImages() {
Expand Down Expand Up @@ -496,44 +496,25 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.preview-player {
overflow: auto;
max-width: 90vw;
height: 70vh;
margin: 10px auto;
object-fit: contain;
img,
video {
max-width: 85vw;
max-height: 65vh;
}
}
.preview-player.lightbox {
position: absolute;
top: 0;
left: 0;
z-index: 999;
margin: 0;
background: rgba(38, 38, 38, 0.8);
width: 100%;
.stage {
display: flex;
flex-direction: column;
height: 100%;
max-width: 100%;
}
.preview-player.lightbox > * {
max-width: 100vw;
max-height: 100vh;
}
.preview-details.lightbox {
z-index: 1000;
opacity: 0.9;
}
text-align: center;
&_media {
flex-grow: 1;
overflow: hidden;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 959px) {
.preview-player {
max-width: 100vw;
&_controls {
height: auto;
margin: 10px auto;
}
}
</style>
5 changes: 1 addition & 4 deletions packages/web-app-preview/src/components/MediaControls.vue
@@ -1,8 +1,5 @@
<template>
<div
class="oc-position-medium oc-position-bottom-center preview-details"
:class="{ lightbox: isFullScreenModeActivated }"
>
<div class="preview-details" :class="{ lightbox: isFullScreenModeActivated }">
<div
class="oc-background-brand oc-p-s oc-width-large oc-flex oc-flex-middle oc-flex-center oc-flex-around preview-controls-action-bar"
>
Expand Down
64 changes: 62 additions & 2 deletions packages/web-app-preview/src/components/Sources/MediaImage.vue
@@ -1,15 +1,18 @@
<template>
<img
ref="img"
:key="`media-image-${file.id}`"
:src="file.url"
:alt="file.name"
:data-id="file.id"
:style="`zoom: ${currentImageZoom};transform: rotate(${currentImageRotation}deg)`"
:style="`transform: rotate(${currentImageRotation}deg)`"
/>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { defineComponent, PropType, onMounted, ref, watch } from 'vue'
import { CachedFile } from '../../helpers/types'
import type { PanzoomObject } from '@panzoom/panzoom'
import Panzoom from '@panzoom/panzoom'
export default defineComponent({
name: 'MediaImage',
Expand All @@ -26,6 +29,63 @@ export default defineComponent({
type: Number,
required: true
}
},
setup(props) {
const img = ref()
let panzoom: PanzoomObject
onMounted(() => {
panzoom = Panzoom(img.value, {
animate: true,
duration: 300,
overflow: 'auto',
maxScale: 10,
setTransform: (_, { scale, x, y }) => {
let h: number
let v: number
switch (props.currentImageRotation) {
case -270:
case 90:
h = y
v = 0 - x
break
case -180:
case 180:
h = 0 - x
v = 0 - y
break
case -90:
case 270:
h = 0 - y
v = x
break
default:
h = x
v = y
}
panzoom.setStyle(
'transform',
`rotate(${props.currentImageRotation}deg) scale(${scale}) translate(${h}px, ${v}px)`
)
}
})
})
watch([() => props.currentImageZoom, () => props.currentImageRotation], () => {
panzoom.zoom(props.currentImageZoom)
})
return {
img
}
}
})
</script>
<style lang="scss" scoped>
img {
max-width: 80%;
max-height: 80%;
}
</style>
3 changes: 2 additions & 1 deletion packages/web-pkg/package.json
Expand Up @@ -14,7 +14,8 @@
"directory": "packages/web-pkg"
},
"devDependencies": {
"web-test-helpers": "workspace:*"
"web-test-helpers": "workspace:*",
"zod": "^3.22.4"
},
"peerDependencies": {
"@ownclouders/web-client": "workspace:*",
Expand Down
12 changes: 8 additions & 4 deletions packages/web-pkg/src/services/preview/previewService.ts
Expand Up @@ -112,14 +112,15 @@ export class PreviewService {
scalingup: options.scalingup || 0,
preview: Object.hasOwnProperty.call(options, 'preview') ? options.preview : 1,
a: Object.hasOwnProperty.call(options, 'a') ? options.a : 1,
...(options.processor && { processor: options.processor }),
...(options.etag && { c: options.etag.replaceAll('"', '') }),
...(options.dimensions && options.dimensions[0] && { x: options.dimensions[0] }),
...(options.dimensions && options.dimensions[1] && { y: options.dimensions[1] })
})
}

private async privatePreviewBlob(options: LoadPreviewOptions, cached = false): Promise<string> {
const { resource, dimensions } = options
const { resource, dimensions, processor } = options
if (cached) {
return this.cacheFactory(options)
}
Expand All @@ -129,7 +130,7 @@ export class PreviewService {
'remote.php/dav',
encodePath(resource.webDavPath),
'?',
this.buildQueryString({ etag: resource.etag, dimensions })
this.buildQueryString({ etag: resource.etag, dimensions, processor })
].join('')
try {
const { data } = await this.clientService.httpAuthenticated.get(url, {
Expand All @@ -140,14 +141,17 @@ export class PreviewService {
}

private async publicPreviewUrl(options: LoadPreviewOptions): Promise<string> {
const { resource, dimensions } = options
const { resource, dimensions, processor } = options
// In a public context, i.e. public shares, the downloadURL contains a pre-signed url to
// download the file.
const [url, signedQuery] = resource.downloadURL.split('?')

// Since the pre-signed url contains query parameters and the caller of this method
// can also provide query parameters we have to combine them.
const combinedQuery = [this.buildQueryString({ etag: resource.etag, dimensions }), signedQuery]
const combinedQuery = [
this.buildQueryString({ etag: resource.etag, dimensions, processor }),
signedQuery
]
.filter(Boolean)
.join('&')

Expand Down
7 changes: 7 additions & 0 deletions packages/web-pkg/src/services/preview/types.ts
@@ -1,17 +1,24 @@
import { Resource, SpaceResource } from '@ownclouders/web-client'
import { z } from 'zod'

export const ProcessorType = z.enum(['fit', 'resize', 'fill', 'thumbnail'])

export type ProcessorType = z.infer<typeof ProcessorType>

export interface BuildQueryStringOptions {
preview?: number
scalingup?: number
a?: number
etag?: string
dimensions?: [number, number]
processor?: ProcessorType
}

export interface LoadPreviewOptions {
space: SpaceResource
resource: Resource
dimensions?: [number, number]
processor?: ProcessorType
}

export interface PreviewCapability {
Expand Down

0 comments on commit 62b9985

Please sign in to comment.