Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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=f293031e404d51a7da4a5c06d88b449dc226b9cb
OPENCLOUD_COMMITID=4dec19089de0ab4b2d6e9d77bb3e11bb1dcc26f7
OPENCLOUD_BRANCH=main
2 changes: 1 addition & 1 deletion .woodpecker.star
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ COLLABORA_CODE = "collabora/code:26.04.4.2.1"
KEYCLOAK = "quay.io/keycloak/keycloak:26.7.3"
MINIO_MC = "quay.io/opencloudeu/minio-mc-ci:1.0"
OC_CI_BAZEL_BUILDIFIER = "quay.io/opencloudeu/bazel-buildifier-ci:latest"
OC_CI_GOLANG = "quay.io/opencloudeu/golang-ci:1.25"
OC_CI_GOLANG = "quay.io/opencloudeu/golang-ci:1.26"
OC_CI_NODEJS = "quay.io/opencloudeu/nodejs-ci:24"
OC_CI_NODEJS_ALPINE = "quay.io/opencloudeu/nodejs-alpine-ci:24"
OC_CI_WAIT_FOR = "quay.io/opencloudeu/wait-for-ci:latest"
Expand Down
8 changes: 4 additions & 4 deletions packages/web-app-preview/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
</p>
</div>
<media-motion-photo
v-else-if="activeMediaFile.isMotionPhoto"
v-else-if="activeMediaFile.isMotionOrLivePhoto"
ref="motionPlayer"
:key="activeMediaFile.id"
:file="activeMediaFile"
Expand Down Expand Up @@ -72,7 +72,7 @@
:is-full-screen-mode-activated="isFullScreenModeActivated"
:is-folder-loading="isFolderLoading"
:show-image-controls="activeMediaFile?.isImage && !activeMediaFile?.isError"
:show-motion-control="activeMediaFile?.isMotionPhoto && !activeMediaFile?.isError"
:show-motion-control="activeMediaFile?.isMotionOrLivePhoto && !activeMediaFile?.isError"
:is-motion-playing="motionPlayer?.isPlaying"
:current-image-rotation="currentImageRotation"
:photo-roll-enabled="photoRollEnabled"
Expand Down Expand Up @@ -104,13 +104,13 @@ 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,
determineResourceTableSortFields,
FolderViewerSlotProps,
isLocationSharesActive,
isMotionOrLivePhoto,
Key,
Modifier,
ProcessorType,
Expand Down Expand Up @@ -248,7 +248,7 @@ const buildMediaFiles = () => {
isVideo: isFileTypeVideo(file),
isImage: isFileTypeImage(file),
isAudio: isFileTypeAudio(file),
isMotionPhoto: isFileTypeImage(file) && !isEmpty(file.motionPhoto),
isMotionOrLivePhoto: isMotionOrLivePhoto(file),
isLoading: true,
isError: false,
resource: file
Expand Down
17 changes: 13 additions & 4 deletions packages/web-app-preview/src/components/MediaControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,13 @@
<script setup lang="ts">
import { computed, unref } from 'vue'
import { useGettext } from 'vue3-gettext'
import { ActionOptions, isMacOs, useFileActions, useGetMatchingSpace } from '@opencloud-eu/web-pkg'
import {
ActionOptions,
isLivePhoto,
isMacOs,
useFileActions,
useGetMatchingSpace
} from '@opencloud-eu/web-pkg'
import { MediaFile } from '../helpers/types'
import { previewToolbarActionsExtensionPoint } from '../extensionPoints'

Expand Down Expand Up @@ -271,7 +277,10 @@ 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')
)
const motionDescription = computed(() => {
if (isLivePhoto(files[activeIndex]?.resource)) {
return isMotionPlaying ? $gettext('Pause Live Photo') : $gettext('Play Live Photo')
}
return isMotionPlaying ? $gettext('Pause Motion Photo') : $gettext('Play Motion Photo')
})
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@
v-if="isLoading && !isPlaying"
class="absolute inset-0 m-auto"
size="large"
:aria-label="$gettext('Loading motion photo')"
:aria-label="loadingLabel"
/>
</template>
</media-image>
</template>

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

Expand All @@ -46,6 +46,12 @@ const { isPlaying, isLoading, videoUrl, play, stop, seekToStill } = useMotionPho
() => getMatchingSpace(file.resource)
)

const loadingLabel = computed(() => {
return isLivePhoto(file.resource)
? $gettext('Loading Live Photo')
: $gettext('Loading Motion Photo')
})

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

Expand Down
2 changes: 1 addition & 1 deletion packages/web-app-preview/src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type MediaFile = {
isVideo: boolean
isImage: boolean
isAudio: boolean
isMotionPhoto: boolean
isMotionOrLivePhoto: boolean
isLoading: boolean
isError: boolean
resource: Resource
Expand Down
29 changes: 27 additions & 2 deletions packages/web-app-preview/tests/unit/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,29 @@ describe('Preview app', () => {
})

describe('Generated "mediaFiles"', () => {
it('flags the still of a live photo as a motion or live photo', () => {
const file = (name: string, extra: Partial<Resource> = {}) =>
({
id: name,
fileId: name,
name,
mimeType: 'image/jpeg',
path: `personal/admin/${name}`,
hidden: false,
canDownload: () => true,
...extra
}) as unknown as Resource
const { wrapper } = createShallowMountWrapper({
files: [file('live.jpg', { livePhoto: { contentId: 'preview-live' } }), file('plain.jpg')]
})
const mediaFiles = (wrapper.vm as any).mediaFiles as {
name: string
isMotionOrLivePhoto: boolean
}[]
expect(mediaFiles.find(({ name }) => name === 'live.jpg').isMotionOrLivePhoto).toBe(true)
expect(mediaFiles.find(({ name }) => name === 'plain.jpg').isMotionOrLivePhoto).toBe(false)
})

it('should hide hidden shares if the share visibility query is not set to "hidden"', () => {
const { wrapper } = createShallowMountWrapper()
expect((wrapper.vm as any).mediaFiles.length).toStrictEqual(7)
Expand All @@ -272,9 +295,11 @@ describe('Preview app', () => {
})

function createShallowMountWrapper({
currentFileContext
currentFileContext,
files = activeFiles
}: {
currentFileContext?: Partial<FileContext>
files?: unknown[]
} = {}) {
const mocks = defaultComponentMocks()
mocks.$previewService.loadPreview.mockResolvedValue('')
Expand All @@ -290,7 +315,7 @@ function createShallowMountWrapper({
path: 'personal/admin/bear.png',
...currentFileContext
}),
activeFiles,
activeFiles: files as Resource[],
isFolderLoading: true,
revokeUrl,
getUrlForResource,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,33 @@ describe('MediaControls component', () => {
})
})

describe('motion control', () => {
const motionToggle = '[data-testid="motion-photo-toggle"]'

it('is labelled for a motion photo', () => {
const { wrapper } = getWrapper({
showMotionControl: true,
files: [{ resource: { motionPhoto: { videoSize: 120000 } } } as unknown as MediaFile]
})
expect(wrapper.find(motionToggle).attributes('aria-label')).toBe('Play Motion Photo')
})

it('is labelled for a live photo', async () => {
const { wrapper } = getWrapper({
showMotionControl: true,
files: [
{
resource: { mimeType: 'image/heic', livePhoto: { contentId: 'controls-live' } }
} as unknown as MediaFile
]
})
expect(wrapper.find(motionToggle).attributes('aria-label')).toBe('Play Live Photo')

await wrapper.setProps({ isMotionPlaying: true })
expect(wrapper.find(motionToggle).attributes('aria-label')).toBe('Pause Live Photo')
})
})

function getWrapper(props: PartialComponentProps<typeof MediaControls> = {}) {
const mocks = defaultComponentMocks()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const mediaFile = (motionPhoto: unknown = { videoSize: 120000, presentationTimes
isVideo: false,
isImage: true,
isAudio: false,
isMotionPhoto: true,
isMotionOrLivePhoto: true,
isLoading: false,
isError: false,
resource: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('PhotoRoll component', () => {
})

it('wraps the thumbnail in a motion photo overlay for the resource', () => {
const item = mock<MediaFile>({ isMotionPhoto: true })
const item = mock<MediaFile>({ isMotionOrLivePhoto: true })
const { wrapper } = getWrapper({ item })
const overlay = wrapper.findComponent(MotionPhotoOverlay)
expect(overlay.exists()).toBe(true)
Expand Down
5 changes: 3 additions & 2 deletions packages/web-client/src/helpers/resource/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ export const isShareRoot = (resource: Resource) => {
return typeof resource.isShareRoot === 'function' && resource.isShareRoot()
}

const convertObjectToCamelCaseKeys = (data: Record<string, any>) => {
const convertObjectToCamelCaseKeys = <T extends Record<string, any>>(data: T): T => {
if (!data) {
return data
}
const converted: Record<string, any> = {}
Object.keys(data).forEach((key) => {
converted[camelCase(key)] = data[key]
})
return converted
return converted as T
}

export function buildResource(
Expand Down Expand Up @@ -174,6 +174,7 @@ export function buildResource(
image: convertObjectToCamelCaseKeys(resource.props[DavProperty.Image]),
photo: convertObjectToCamelCaseKeys(resource.props[DavProperty.Photo]),
motionPhoto: convertObjectToCamelCaseKeys(resource.props[DavProperty.MotionPhoto]),
livePhoto: convertObjectToCamelCaseKeys(resource.props[DavProperty.LivePhoto]),
extraProps,
hasPreview: () => resource.props[DavProperty.HasPreview] === 1,
canUpload: function () {
Expand Down
2 changes: 2 additions & 0 deletions packages/web-client/src/helpers/resource/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
GeoCoordinates,
Identity,
Image,
LivePhoto,
MotionPhoto,
Photo,
User
Expand Down Expand Up @@ -55,6 +56,7 @@ export interface Resource {
image?: Image
photo?: Photo
motionPhoto?: MotionPhoto
livePhoto?: LivePhoto
path: string
webDavPath?: string
downloadURL?: string
Expand Down
7 changes: 6 additions & 1 deletion packages/web-client/src/webdav/constants/dav.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Audio, GeoCoordinates, Image, MotionPhoto, Photo } from '../../graph/generated'
import { Audio, GeoCoordinates, Image, LivePhoto, MotionPhoto, Photo } from '../../graph/generated'

export abstract class DavPermission {
static readonly Shared: string = 'S'
Expand Down Expand Up @@ -81,6 +81,10 @@ const DavPropertyMapping = {
value: 'motion-photo',
type: null as MotionPhoto
},
LivePhoto: {
value: 'live-photo',
type: null as LivePhoto
},
ETag: defString('getetag' as const),
MimeType: defString('getcontenttype' as const),
ResourceType: defStringArray('resourcetype' as const),
Expand Down Expand Up @@ -169,6 +173,7 @@ export abstract class DavProperties {
DavProperty.Image,
DavProperty.Photo,
DavProperty.MotionPhoto,
DavProperty.LivePhoto,
DavProperty.HasPreview
]

Expand Down
36 changes: 36 additions & 0 deletions packages/web-client/tests/unit/helpers/resource/functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,42 @@ describe('buildResource', () => {
})
})

describe('livePhoto facet', () => {
it('maps the live photo facet and camel-cases its keys', () => {
const webDavResponse = mockDeep<WebDavResponseResource>({
props: {
[DavProperty.LivePhoto]: {
'content-id': 'A1B2C3D4-0000-1111-2222-333344445555',
'still-image-time-us': 1250000,
auto: true,
'vitality-score': 0.75,
'vitality-scoring-version': 4
} as never,
[DavProperty.Tags]: undefined
}
})
const resource = buildResource(webDavResponse)
expect(resource.livePhoto).toEqual({
contentId: 'A1B2C3D4-0000-1111-2222-333344445555',
stillImageTimeUs: 1250000,
auto: true,
vitalityScore: 0.75,
vitalityScoringVersion: 4
})
})

it('leaves livePhoto undefined when the facet is absent', () => {
const webDavResponse = mockDeep<WebDavResponseResource>({
props: {
[DavProperty.LivePhoto]: undefined,
[DavProperty.Tags]: undefined
}
})
const resource = buildResource(webDavResponse)
expect(resource.livePhoto).toBeUndefined()
})
})

it('handles extraProps', () => {
const webDavResponse = mockDeep<WebDavResponseResource>({
props: {
Expand Down
11 changes: 7 additions & 4 deletions packages/web-pkg/src/components/FilesList/MotionPhotoBadge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { computed, unref } from 'vue'
import { useGettext } from 'vue3-gettext'

const {
Expand All @@ -41,21 +41,24 @@ const {
muted = false,
loading = false,
icon = 'play-circle',
label: labelProp
label: labelProp,
loadingLabel: loadingLabelProp = undefined
} = defineProps<{
sizeClass?: string
interactive?: boolean
muted?: boolean
loading?: boolean
icon?: string
label?: string
loadingLabel?: string
}>()

const isPause = computed(() => icon.includes('pause'))

const { $gettext } = useGettext()
const label = computed(() => labelProp ?? $gettext('Motion photo'))
const tooltip = computed(() => (loading ? $gettext('Loading motion photo') : label.value))
const label = computed(() => labelProp ?? $gettext('Motion Photo'))
const loadingLabel = computed(() => loadingLabelProp ?? $gettext('Loading Motion Photo'))
const tooltip = computed(() => (loading ? unref(loadingLabel) : unref(label)))
const showTooltip = computed(() => !interactive)
</script>

Expand Down
Loading
Loading