Files
Portfolio/src/components/ProjectMedia.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>