Skip to content

feat: motion photo support - #2819

Merged
dschmidt merged 17 commits into
mainfrom
feat/motion-photos
Sep 8, 2026
Merged

feat: motion photo support#2819
dschmidt merged 17 commits into
mainfrom
feat/motion-photos

Conversation

@dschmidt

@dschmidt dschmidt commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Description

Surfaces Google Motion Photos (a still JPEG with a short MP4 appended) in the UI: a play badge everywhere plus inline playback.

Note

The backend side has landed: opencloud-eu/reva#790 emits the oc:motion-photo PROPFIND property, opencloud-eu/opencloud#3478 bumped reva to that commit on 2026-09-07. This PR bumps OPENCLOUD_COMMITID in .woodpecker.env to a main commit that includes it, so the e2e scenarios run against a backend that emits the property.

The facet also needs Tika 4.1.0-SNAPSHOT: the search service only writes libre.graph.motionPhoto when Tika flattens the motion photo XMP into Camera: keys (Tika 4) and emits the appended clip as an embedded video/* document (apache/tika#3115, merged 2026-09-02, unreleased). Verified locally: Tika 3.3.1 yields no keys, 4.0.0 yields keys but no property, 4.1.0-SNAPSHOT yields oc:motion-photo with video-size. The CI Tika image (.woodpecker.star) and the dev stack (docker-compose.yml) follow the 4.1.0-SNAPSHOT tag on purpose (a digest of a snapshot can be pruned upstream) and switch to the release tag as soon as Tika 4.1.0 ships.

  • Data: threads the oc:motion-photo WebDAV property (version, presentationTimestampUs, videoSize) through buildResource onto Resource, using the generated MotionPhoto type from the libre-graph client.
  • Badge: MotionPhotoBadge (MDI motion-play/pause icon) on grid tiles, table rows, the sidebar preview and the media viewer photo roll.
  • Playback: useMotionPhoto range-fetches just the embedded MP4 (Range: bytes=-videoSize) and wraps it as a blob; useMotionPhotoPlayback owns the shared hover/play state, buffering indicator and abort. MotionPhotoOverlay reuses both across the files list, sidebar and photo roll.
    • Grid tiles, table rows, sidebar preview, photo roll: hover to play (muted, looping), badge click toggles.
    • Media viewer: auto-plays once on open, loop play/pause control in the bottom controls bar. The still stays a regular MediaImage, so zoom, pan, rotation and reset keep working and carry over to the clip.
    • Touch: badge tap toggles play/pause; hover is limited to hover-capable devices. Hover-to-play waits for a short hover intent (150 ms) so sweeping across a list does not fire a range request per item.
    • A file whose facet exists but whose clip cannot be located (implausible videoSize) still shows the badge, greyed out with an explaining tooltip: knowing that a file is a motion photo matters (e.g. before sharing it) even when it cannot be played here.
    • The badge is a real <button> and lives next to the resource link, never inside it. Only actual motion photos mount the playback state; plain rows and tiles stay untouched.
  • CSP: the clip plays from a blob: URL (same as every video in the media viewer), so media-src needs blob:. The dev stack and CI CSP files are updated; the server default (services/proxy/pkg/config/csp.yaml) needs the same change.

Related Issue

How Has This Been Tested?

  • test environment: local dev stack with a reva build that emits the facet, plus pnpm check:all
  • test case 1: unit specs for useMotionPhoto, useMotionPhotoPlayback, MotionPhotoOverlay, MediaMotionPhoto, ResourceTile, ResourceListItem, PhotoRollItem and buildResource
  • test case 2: e2e Motion photo indicator: badge in tiles and table view, badge in the media viewer preview strip, playback control in the media viewer, inline playback from the sidebar
  • test case 3: manual: hover playback in list and tiles, sidebar and photo roll, auto-play and loop toggle in the media viewer, badge tap on a touch device

Types of changes

  • Bugfix
  • Enhancement (a change that doesn't break existing code or deployments)
  • Breaking change (a modification that affects current functionality)
  • Technical debt (addressing code that needs refactoring or improvements)
  • Tests (adding or improving tests)
  • Documentation (updates or additions to documentation)
  • Maintenance (like dependency updates or tooling adjustments)

@dschmidt
dschmidt force-pushed the feat/motion-photos branch 4 times, most recently from 2d81b74 to a37ca7d Compare July 7, 2026 13:10
@dschmidt
dschmidt changed the base branch from main to chore/regenerate-libre-graph-client July 7, 2026 13:11
@dschmidt
dschmidt force-pushed the chore/regenerate-libre-graph-client branch from 4d882ab to cae8685 Compare July 7, 2026 13:18
@dschmidt
dschmidt force-pushed the feat/motion-photos branch from a37ca7d to f013edc Compare July 7, 2026 13:18
Base automatically changed from chore/regenerate-libre-graph-client to main July 7, 2026 13:34
@dschmidt
dschmidt force-pushed the feat/motion-photos branch 2 times, most recently from 7a98785 to ecff135 Compare July 7, 2026 14:01
@dschmidt
dschmidt force-pushed the feat/motion-photos branch from ecff135 to 161f9c5 Compare August 15, 2026 20:25
@dschmidt
dschmidt force-pushed the feat/motion-photos branch 3 times, most recently from bf54e80 to ba234f1 Compare September 7, 2026 11:43
@dschmidt
dschmidt marked this pull request as ready for review September 7, 2026 11:45
@dschmidt dschmidt self-assigned this Sep 7, 2026
@dschmidt
dschmidt force-pushed the feat/motion-photos branch 2 times, most recently from f550137 to 5170436 Compare September 7, 2026 14:41
Map the oc:motionPhoto WebDAV property (version, presentationTimestampUs, videoSize) onto Resource in buildResource, using the generated MotionPhoto type from the libre-graph client.
MotionPhotoBadge renders the MDI motion-play/pause icon; MotionPhotoOverlay pairs the badge with the hover-to-play clip so every surface reuses the same markup and behaviour. useMotionPhoto range-fetches the embedded MP4; useMotionPhotoPlayback owns the shared hover/play state, buffering and abort.
Show the badge and hover-to-play the clip on grid tiles, table rows, the sidebar preview and the media viewer photo roll. The media viewer auto-plays once on open and offers a loop play/pause control in the bottom bar.
Cover the badge in tiles/table, the media viewer preview strip, the viewer playback control and inline sidebar playback.
The motion photo facet landed in opencloud main on 2026-09-03, the
e2e scenarios need a server that emits it.
The motion photo facet needs the Camera: XMP keys (Tika 4) and the
video emitted as an embedded document (apache/tika#3115, merged on
2026-09-02 and not released yet). Without it the search service never
writes libre.graph.motionPhoto and oc:motion-photo stays empty.

The snapshot tag moves, so the image is pinned by digest to the build
that was verified against the e2e fixture. Replace it with the release
tag once Tika 4.1.0 ships.
…xpected to fail

A quota-exhausted upload is rejected at the tus POST by the bumped
opencloud, so no 201/204 ever arrives. The dangling waitForResponse
then rejected when the page closed and failed an otherwise passing
scenario.
A play() cancelled while its fetch was in flight still ran its finally
block and reset the state of the next run: it dropped the in-flight
guard, cleared the newer spinner timer and hid its loading state, so a
later run could become uncancelable and start after the pointer left.
Every run now carries a generation and only cleans up its own.

Hover-to-play waits for a short hover intent (150 ms) so sweeping the
pointer across a list does not fire and abort one range request per
item, and a consumer that switches resources (the sidebar) revokes the
previous clip instead of keeping one blob per resource it ever showed.
…clip

The overlay used to instantiate the playback composable (with its media
query listener) for every list row. It is now a plain positioned wrapper
and defers to a MotionPhotoPlayer that is only mounted for motion
photos. The player is pointer-transparent apart from its badge.

The badge is a real button now and sits next to the resource link
instead of inside it, in the list and in the tile. A file whose facet
is unusable still shows the badge, greyed out with an explaining label,
because knowing that a file is a motion photo matters even when the
clip cannot be played here.
The viewer rendered motion photos through their own component and lost
the image controls. The still is a regular MediaImage again, which
gained an overlay slot whose wrapper is the panzoom target, so zoom,
pan and rotation carry over to the clip laid on top.
A digest of a snapshot build can be pruned upstream, which would break
CI and dev stacks at a random point in time. The tag moves, but it
moves forward, and it is replaced by the release tag as soon as Tika
4.1.0 ships.
No template comments, no prop docs (web-pkg components do not document
props, unlike the design system), and only the facts that are not
readable from the code: the motion photo file layout and range math,
the stale-run generation, the hover intent, the touch gating, the
206 content type, and why the viewer wrapper has to fill the stage.
Comment thread packages/web-pkg/src/components/FilesList/MotionPhotoBadge.vue Outdated
Comment thread packages/web-pkg/src/components/FilesList/MotionPhotoBadge.vue Outdated
Comment thread packages/web-pkg/src/components/FilesList/MotionPhotoBadge.vue Outdated
As a positioned sibling after the media link it already paints above the
thumbnail; z-20 lifted the badge above the selection checkbox and, since
the card has no stacking context of its own, above sticky chrome.
The play and pause glyphs are design-system icons now (motion-play-line,
motion-pause-line, the dot in its own group so it can orbit while
loading), and the badge takes a Tailwind sizeClass like OcIcon instead
of the deprecated SizeType, which drops the copied size map.
The preview shrinks while hovered or selected, the clip did not: it
played at full tile size on top. The preview inset is now driven by the
media area's hover state (badge included, so hovering the badge does
not pop the still back out), the clip follows that inset with the same
rounding, and the badge keeps the checkbox's fixed edge distance. The
clip sits in a wrapper because insets alone do not size a replaced
element, and the media link is a flex item again so the wrapper is not
a line taller than the link.

@JammingBen JammingBen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it! 😍

@dschmidt
dschmidt merged commit a6902a9 into main Sep 8, 2026
31 checks passed
@dschmidt
dschmidt deleted the feat/motion-photos branch September 8, 2026 11:33
openclouders pushed a commit that referenced this pull request Sep 8, 2026
@openclouders openclouders mentioned this pull request Sep 8, 2026
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants