Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6ed1fff
feat(web-client): add motionPhoto facet
dschmidt Jul 7, 2026
2110766
feat(web-pkg): add motion photo badge, overlay and playback composables
dschmidt Jul 7, 2026
f1d3188
feat: play motion photos in the files list, sidebar and viewer
dschmidt Jul 7, 2026
180bb5a
test(e2e): add motion photo scenarios
dschmidt Jul 7, 2026
1baff8b
refactor(web-client): read oc:motion-photo, kebab-case like the other…
dschmidt Sep 3, 2026
c612178
test(web-pkg): type the deferred fetch resolver in the playback spec
dschmidt Sep 3, 2026
ffb3677
ci: bump opencloud commit id
dschmidt Sep 7, 2026
eb5b41c
ci: run the e2e stack against tika 4.1.0-SNAPSHOT
dschmidt Sep 7, 2026
52ee6b2
test(e2e): do not wait for a 2xx upload response when the upload is e…
dschmidt Sep 7, 2026
a18f64c
fix(web-pkg): keep stale motion photo loads out of the playback state
dschmidt Sep 8, 2026
8f45701
refactor(web-pkg): mount motion photo playback only where there is a …
dschmidt Sep 8, 2026
2291a35
fix(preview): keep zoom, pan and rotation for motion photos
dschmidt Sep 8, 2026
5ccb6bb
ci: follow the tika 4.1.0-SNAPSHOT tag instead of a digest
dschmidt Sep 8, 2026
c9450c7
chore: trim comments to what the code does not say itself
dschmidt Sep 8, 2026
05cbcbb
fix(web-pkg): drop the z-index of the tile's motion photo player
dschmidt Sep 8, 2026
b0e8409
refactor(web-pkg): render the motion photo badge through OcIcon
dschmidt Sep 8, 2026
a433240
fix(web-pkg): keep the tile's clip inside the hovered preview
dschmidt Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .woodpecker.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# The version of OpenCloud to use in pipelines
OPENCLOUD_COMMITID=cd9095833af0ba1dc7449194e62799931d38a7b1
OPENCLOUD_COMMITID=81536bbd0e10edfe7fc7ce8a7cee7fadf09c3316
OPENCLOUD_BRANCH=main
2 changes: 1 addition & 1 deletion .woodpecker.star
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ repo_slug = "opencloud-eu/web"
docker_repo_slug = "opencloudeu/web"

ALPINE_GIT = "alpine/git:latest"
APACHE_TIKA = "apache/tika:2.8.0.0"
APACHE_TIKA = "apache/tika:4.1.0-SNAPSHOT"

# renovate: datasource=docker depName=collabora/code
COLLABORA_CODE = "collabora/code:26.04.2.4.1"
Expand Down
1 change: 1 addition & 0 deletions dev/docker/opencloud/csp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ directives:
- '''self'''
media-src:
- '''self'''
- 'blob:'
object-src:
- '''self'''
- 'blob:'
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ services:
- traefik

tika:
image: apache/tika:3.3.1.0
image: apache/tika:4.1.0-SNAPSHOT
container_name: web_tika
ports:
- 9998:9998
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@
class="relative flex items-center justify-center bg-role-surface-container rounded-xl p-4 mb-4"
>
<div v-if="preview" data-testid="preview">
<img
key="file-thumbnail"
:src="preview"
class="details-preview h-[160px]"
alt=""
decoding="async"
/>
<motion-photo-overlay
:resource="resource"
:space="space"
badge-size-class="size-5"
badge-class="top-1 right-1"
>
<img
key="file-thumbnail"
:src="preview"
class="details-preview h-[160px]"
alt=""
decoding="async"
/>
</motion-photo-overlay>
</div>
<resource-icon
v-else
Expand Down Expand Up @@ -172,7 +179,7 @@ import {
isTrashResource,
ShareTypes
} from '@opencloud-eu/web-client'
import { useGetMatchingSpace } from '@opencloud-eu/web-pkg'
import { MotionPhotoOverlay, useGetMatchingSpace } from '@opencloud-eu/web-pkg'
import { formatFileSize, formatRelativeDateFromJSDate } from '@opencloud-eu/web-pkg'
import { Resource, SpaceResource } from '@opencloud-eu/web-client'
import { useGettext } from 'vue3-gettext'
Expand Down
14 changes: 14 additions & 0 deletions packages/web-app-preview/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
{{ $gettext('Failed to load "%{filename}"', { filename: activeMediaFile.name }) }}
</p>
</div>
<media-motion-photo
v-else-if="activeMediaFile.isMotionPhoto"
ref="motionPlayer"
:key="activeMediaFile.id"
:file="activeMediaFile"
:current-image-rotation="currentImageRotation"
/>
<media-image
v-else-if="activeMediaFile.isImage"
:file="activeMediaFile"
Expand All @@ -64,8 +71,11 @@
:is-full-screen-mode-activated="isFullScreenModeActivated"
:is-folder-loading="isFolderLoading"
:show-image-controls="activeMediaFile?.isImage && !activeMediaFile?.isError"
:show-motion-control="activeMediaFile?.isMotionPhoto && !activeMediaFile?.isError"
:is-motion-playing="motionPlayer?.isPlaying"
:current-image-rotation="currentImageRotation"
:photo-roll-enabled="photoRollEnabled"
@toggle-motion="motionPlayer?.toggle()"
@set-rotation-right="imageRotateRight"
@set-rotation-left="imageRotateLeft"
@set-zoom="imageZoom"
Expand Down Expand Up @@ -93,6 +103,7 @@ import {
watch
} from 'vue'
import omit from 'lodash-es/omit'
import isEmpty from 'lodash-es/isEmpty'
import { IncomingShareResource, Resource } from '@opencloud-eu/web-client'
import {
createFileRouteOptions,
Expand All @@ -115,6 +126,7 @@ import {
import MediaControls from './components/MediaControls.vue'
import MediaAudio from './components/Sources/MediaAudio.vue'
import MediaImage from './components/Sources/MediaImage.vue'
import MediaMotionPhoto from './components/Sources/MediaMotionPhoto.vue'
import MediaVideo from './components/Sources/MediaVideo.vue'
import PhotoRoll from './components/PhotoRoll.vue'
import { MediaFile } from './helpers/types'
Expand Down Expand Up @@ -172,6 +184,7 @@ const isAutoPlayEnabled = ref(true)
const isAutoAdvancing = ref(false)
const photoRollEnabled = ref(true)
const preview = useTemplateRef<HTMLElement>('preview')
const motionPlayer = useTemplateRef<{ isPlaying: boolean; toggle: () => void }>('motionPlayer')
const keyBindings: string[] = []
let loadPreviewImageController: AbortController = null

Expand Down Expand Up @@ -233,6 +246,7 @@ const buildMediaFiles = () => {
isVideo: isFileTypeVideo(file),
isImage: isFileTypeImage(file),
isAudio: isFileTypeAudio(file),
isMotionPhoto: isFileTypeImage(file) && !isEmpty(file.motionPhoto),
isLoading: true,
isError: false,
resource: file
Expand Down
19 changes: 19 additions & 0 deletions packages/web-app-preview/src/components/MediaControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@
/>
</oc-button>
</div>
<oc-button
v-if="showMotionControl"
v-oc-tooltip="motionDescription"
class="preview-controls-motion raw-hover-surface p-1"
data-testid="motion-photo-toggle"
appearance="raw"
:aria-label="motionDescription"
@click="emit('toggleMotion')"
>
<oc-icon fill-type="line" :name="isMotionPlaying ? 'pause-circle' : 'play-circle'" />
</oc-button>

<template v-if="showFavoriteButton || showDeleteButton">
<span
Expand Down Expand Up @@ -163,13 +174,17 @@ const {
isFullScreenModeActivated = false,
isFolderLoading = false,
showImageControls = false,
showMotionControl = false,
isMotionPlaying = false,
currentImageRotation = 0
} = defineProps<{
files: MediaFile[]
activeIndex?: number
isFullScreenModeActivated?: boolean
isFolderLoading?: boolean
showImageControls?: boolean
showMotionControl?: boolean
isMotionPlaying?: boolean
currentImageRotation?: number
photoRollEnabled?: boolean
}>()
Expand All @@ -187,6 +202,7 @@ const emit = defineEmits<{
(e: 'resetImage'): void
(e: 'deleteResource'): void
(e: 'togglePhotoRoll'): void
(e: 'toggleMotion'): void
}>()

const { $gettext } = useGettext()
Expand Down Expand Up @@ -274,4 +290,7 @@ const imageRotateRightDescription = computed(() =>
)
const previousDescription = computed(() => $gettext('Show previous media file in folder'))
const nextDescription = computed(() => $gettext('Show next media file in folder'))
const motionDescription = computed(() =>
isMotionPlaying ? $gettext('Pause motion photo') : $gettext('Play motion photo')
)
</script>
36 changes: 24 additions & 12 deletions packages/web-app-preview/src/components/PhotoRollItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,24 @@
:aria-current="isActive ? 'true' : 'false'"
@click="$emit('select')"
>
<img
v-if="item && item.resource.thumbnail"
:src="item.resource.thumbnail"
class="object-cover h-25 rounded-md aspect-video"
:alt="item.name"
referrerpolicy="no-referrer"
decoding="async"
/>
<div v-else class="aspect-video h-25 flex items-center justify-center">
<resource-icon class="aspect-video" :resource="iconResource" size-class="size-12" />
</div>
<motion-photo-overlay
class="mx-auto w-fit"
:resource="item.resource"
badge-class="top-0 right-0 m-1"
video-class="rounded-md"
>
<img
v-if="item && item.resource.thumbnail"
:src="item.resource.thumbnail"
class="object-cover h-25 rounded-md aspect-video"
:alt="item.name"
referrerpolicy="no-referrer"
decoding="async"
/>
<div v-else class="aspect-video h-25 flex items-center justify-center">
<resource-icon class="aspect-video" :resource="iconResource" size-class="size-12" />
</div>
</motion-photo-overlay>
<span class="w-full">
<resource-name
:name="item.resource.name"
Expand All @@ -43,7 +50,12 @@
<script setup lang="ts">
import { Resource } from '@opencloud-eu/web-client'
import { MediaFile } from '../helpers/types'
import { ResourceIcon, ResourceName, useResourcesStore } from '@opencloud-eu/web-pkg'
import {
MotionPhotoOverlay,
ResourceIcon,
ResourceName,
useResourcesStore
} from '@opencloud-eu/web-pkg'
import { useIsVisible } from '@opencloud-eu/design-system/composables'
import { computed, ref, useTemplateRef } from 'vue'

Expand Down
12 changes: 12 additions & 0 deletions packages/web-app-preview/src/components/Sources/MediaImage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@
@loaded="initPanzoom"
/>
</div>
<div
v-else-if="$slots.overlay"
ref="img"
:key="`media-image-wrapper-${file.id}`"
class="relative grid size-full grid-cols-[100%] grid-rows-[100%] place-items-center pt-4 [&>*]:[grid-area:1/1]"
:data-id="file.id"
>
<img :src="file.url" :alt="file.name" class="max-w-full max-h-full" />
<slot name="overlay" />
</div>
<img
v-else
ref="img"
Expand Down Expand Up @@ -47,6 +57,8 @@ const { file, currentImageRotation } = defineProps<{

const eventBus = useEventBus()

// with an overlay slot this is a stage-filling wrapper around the still: a
// content-sized wrapper would void the percentage max-w/max-h of its children
const img = useTemplateRef<HTMLElement>('img')
const panzoom = ref<PanzoomObject>()
const isSvgImage = computed(() => file.mimeType === 'image/svg+xml')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<template>
<media-image :file="file" :current-image-rotation="currentImageRotation">
<template #overlay>
<video
v-if="isPlaying"
:key="`motion-photo-video-${file.id}`"
:src="videoUrl"
:loop="looping"
muted
autoplay
playsinline
preload="auto"
class="max-w-full max-h-full object-contain"
data-testid="motion-photo-video"
@loadedmetadata="seekToStill"
@ended="onEnded"
@error="stop"
/>
<oc-spinner
v-if="isLoading && !isPlaying"
class="absolute inset-0 m-auto"
size="large"
:aria-label="$gettext('Loading motion photo')"
/>
</template>
</media-image>
</template>

<script setup lang="ts">
import { onMounted, ref, unref } from 'vue'
import { useGettext } from 'vue3-gettext'
import { useGetMatchingSpace, useMotionPhotoPlayback } from '@opencloud-eu/web-pkg'
import { MediaFile } from '../../helpers/types'
import MediaImage from './MediaImage.vue'

const { file, currentImageRotation } = defineProps<{
file: MediaFile
currentImageRotation: number
}>()

const { $gettext } = useGettext()
const { getMatchingSpace } = useGetMatchingSpace()

const { isPlaying, isLoading, videoUrl, play, stop, seekToStill } = useMotionPhotoPlayback(
() => file.resource,
() => getMatchingSpace(file.resource)
)

// auto-play on open runs once, playback started from the controls loops
const looping = ref(false)

const onEnded = () => {
if (!unref(looping)) {
stop()
}
}

const togglePlayback = () => {
if (unref(isPlaying)) {
looping.value = false
stop()
} else {
looping.value = true
play()
}
}

onMounted(() => {
looping.value = false
play()
})

defineExpose({ isPlaying, toggle: togglePlayback })
</script>
1 change: 1 addition & 0 deletions packages/web-app-preview/src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type MediaFile = {
isVideo: boolean
isImage: boolean
isAudio: boolean
isMotionPhoto: boolean
isLoading: boolean
isError: boolean
resource: Resource
Expand Down
Loading