94 lines
3.7 KiB
Vue
94 lines
3.7 KiB
Vue
<script setup lang="ts">
|
|
import { computed, inject } from 'vue';
|
|
import AccessibleVideo, { type AccessibleVideoSource } from './AccessibleVideo.vue';
|
|
import ChartFigure from './ChartFigure.vue';
|
|
import { productionInputsKey } from '@/app';
|
|
import { assetUrl } from '@/content/asset-url';
|
|
import type { MediaPresentation, PublicAsset, PublicAssetManifest } from '@/content/types';
|
|
|
|
const props = withDefaults(defineProps<{
|
|
presentation: Readonly<MediaPresentation>;
|
|
allowAutomaticVideo?: boolean;
|
|
manifest?: Readonly<PublicAssetManifest>;
|
|
}>(), { allowAutomaticVideo: false });
|
|
|
|
const inputs = inject(productionInputsKey, undefined);
|
|
const resolvedManifest = computed(() => props.manifest ?? inputs?.manifest);
|
|
|
|
const resolveAsset = (assetId: string): PublicAsset => {
|
|
const manifest = resolvedManifest.value;
|
|
if (manifest === undefined) throw new Error('Project media requires the public asset manifest.');
|
|
const asset = manifest.assets.find((candidate) => candidate.id === assetId);
|
|
if (asset === undefined) throw new Error(`Unknown asset ID: ${assetId}`);
|
|
return asset;
|
|
};
|
|
|
|
const primaryAsset = computed(() => resolveAsset(props.presentation.assetId));
|
|
const primaryUrl = computed(() => assetUrl(resolvedManifest.value!, props.presentation.assetId));
|
|
const assertImage = (asset: PublicAsset): PublicAsset => {
|
|
if (!asset.mediaType.startsWith('image/') || asset.width === null || asset.height === null) {
|
|
throw new Error(`Project media asset is not an intrinsic-size image: ${asset.id}`);
|
|
}
|
|
return asset;
|
|
};
|
|
const imageAsset = computed(() => assertImage(primaryAsset.value));
|
|
const videoSources = computed<readonly AccessibleVideoSource[]>(() => {
|
|
if (props.presentation.kind !== 'video') return [];
|
|
const ids = [props.presentation.assetId, props.presentation.fallbackAssetId].filter((id): id is string => id !== undefined);
|
|
return ids.map((id) => {
|
|
const asset = resolveAsset(id);
|
|
if (asset.mediaType !== 'video/webm' && asset.mediaType !== 'video/mp4') {
|
|
throw new Error(`Project video source has an invalid media type: ${id}`);
|
|
}
|
|
return { src: assetUrl(resolvedManifest.value!, id), type: asset.mediaType };
|
|
});
|
|
});
|
|
const videoAsset = computed(() => {
|
|
const asset = primaryAsset.value;
|
|
if ((asset.mediaType !== 'video/webm' && asset.mediaType !== 'video/mp4') || asset.width === null || asset.height === null) {
|
|
throw new Error(`Project media asset is not an intrinsic-size video: ${asset.id}`);
|
|
}
|
|
return asset;
|
|
});
|
|
const posterUrl = computed(() => {
|
|
if (props.presentation.kind !== 'video') return '';
|
|
assertImage(resolveAsset(props.presentation.posterAssetId));
|
|
return assetUrl(resolvedManifest.value!, props.presentation.posterAssetId);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<ChartFigure
|
|
v-if="presentation.kind === 'chart'"
|
|
:presentation="presentation"
|
|
:src="primaryUrl"
|
|
:width="imageAsset.width!"
|
|
:height="imageAsset.height!"
|
|
/>
|
|
<AccessibleVideo
|
|
v-else-if="presentation.kind === 'video'"
|
|
:sources="videoSources"
|
|
:poster="posterUrl"
|
|
:width="videoAsset.width!"
|
|
:height="videoAsset.height!"
|
|
:accessible-label="presentation.alt"
|
|
:caption="presentation.caption"
|
|
:description="presentation.longDescription"
|
|
:allow-automatic="allowAutomaticVideo"
|
|
/>
|
|
<figure v-else class="project-media project-media--image" data-project-media>
|
|
<img
|
|
:src="primaryUrl"
|
|
:alt="presentation.alt"
|
|
:width="imageAsset.width!"
|
|
:height="imageAsset.height!"
|
|
loading="lazy"
|
|
decoding="async"
|
|
/>
|
|
<figcaption v-if="presentation.caption">{{ presentation.caption }}</figcaption>
|
|
<p v-if="presentation.longDescription" class="media-description" data-testid="media-long-description">
|
|
{{ presentation.longDescription }}
|
|
</p>
|
|
</figure>
|
|
</template>
|