diff --git a/.changeset/moody-otters-taste.md b/.changeset/moody-otters-taste.md
new file mode 100644
index 000000000..a61e25660
--- /dev/null
+++ b/.changeset/moody-otters-taste.md
@@ -0,0 +1,6 @@
+---
+'@getodk/web-forms': minor
+'@getodk/common': minor
+---
+
+Adds support for labels with audio and video
diff --git a/.gitattributes b/.gitattributes
index 83b751ecb..f39246dae 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -3,3 +3,5 @@
*.jpg -text
*.xlsx binary
+*.mp3 binary
+*.mp4 binary
diff --git a/README.md b/README.md
index bb29fa135..6c2125dba 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ This section is auto generated. Please update `feature-matrix.json` and then run
- ##### Question types (basic functionality)
🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩⬜⬜⬜⬜⬜ 70\%
+ ##### Question types (basic functionality)
🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩⬜⬜⬜⬜ 76\%
@@ -58,9 +58,9 @@ This section is auto generated. Please update `feature-matrix.json` and then run
| range | ✅ |
| image | ✅ |
| barcode | |
-| audio | |
+| audio | ✅ |
| background-audio | |
-| video | |
+| video | ✅ |
| file | |
| date | 🚧 |
| time | |
diff --git a/feature-matrix.json b/feature-matrix.json
index f17b9d224..d716ba1ac 100644
--- a/feature-matrix.json
+++ b/feature-matrix.json
@@ -16,9 +16,9 @@
"range": "✅",
"image": "✅",
"barcode": "",
- "audio": "",
+ "audio": "✅",
"background-audio": "",
- "video": "",
+ "video": "✅",
"file": "",
"date": "🚧",
"time": "",
diff --git a/packages/common/src/fixtures/itext/media-label.xml b/packages/common/src/fixtures/itext/media-label.xml
new file mode 100644
index 000000000..8e3bed808
--- /dev/null
+++ b/packages/common/src/fixtures/itext/media-label.xml
@@ -0,0 +1,159 @@
+
+
+
+ Media Label Test
+
+
+
+
+ Question with audio label
+ jr://audio/test-audio.mp3
+
+
+ Question with video label
+ jr://video/test-video.mp4
+
+
+ Question with image label
+ jr://images/test-image.jpg
+
+
+ Edge case: question with all media in label
+ jr://audio/test-audio.mp3
+ jr://video/test-video.mp4
+ jr://images/test-image.jpg
+
+
+ Select one with media options (special style when it contains images)
+
+
+ Select multiple with media options (special style when it contains images)
+
+
+ Select one audio or video
+
+
+ Select multiple audio or video
+
+
+ Option 1: Audio
+ jr://audio/test-audio.mp3
+
+
+ Option 2: Video
+ jr://video/test-video.mp4
+
+
+ Option 3: Image
+ jr://images/test-image.jpg
+
+
+ Option 4: Audio + Image
+ jr://images/test-image.jpg
+ jr://audio/test-audio.mp3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ audio
+
+ -
+
+ video
+
+ -
+
+ image
+
+ -
+
+ image-audio
+
+
+
+
+
+ -
+
+ audio
+
+ -
+
+ video
+
+
+
+
+
diff --git a/packages/common/src/fixtures/itext/test-audio.mp3 b/packages/common/src/fixtures/itext/test-audio.mp3
new file mode 100644
index 000000000..54600762b
Binary files /dev/null and b/packages/common/src/fixtures/itext/test-audio.mp3 differ
diff --git a/packages/common/src/fixtures/itext/test-image.jpg b/packages/common/src/fixtures/itext/test-image.jpg
new file mode 100644
index 000000000..3dbefcdea
Binary files /dev/null and b/packages/common/src/fixtures/itext/test-image.jpg differ
diff --git a/packages/common/src/fixtures/itext/test-video.mp4 b/packages/common/src/fixtures/itext/test-video.mp4
new file mode 100644
index 000000000..cc444d18b
Binary files /dev/null and b/packages/common/src/fixtures/itext/test-video.mp4 differ
diff --git a/packages/common/src/fixtures/xform-attachments.ts b/packages/common/src/fixtures/xform-attachments.ts
index 1af23eff1..71389ac08 100644
--- a/packages/common/src/fixtures/xform-attachments.ts
+++ b/packages/common/src/fixtures/xform-attachments.ts
@@ -28,6 +28,11 @@ const xformAttachmentFileExtensions = [
'.jpeg',
'.gif',
'.svg',
+ '.mp4',
+ '.mp3',
+ '.m4a',
+ '.wav',
+ '.3gp',
] as const;
type XFormAttachmentFileExtensions = typeof xformAttachmentFileExtensions;
@@ -108,6 +113,26 @@ export class XFormAttachmentFixture {
this.mimeType = 'image/svg+xml';
break;
+ case '.mp4':
+ this.mimeType = 'video/mp4';
+ break;
+
+ case '.mp3':
+ this.mimeType = 'audio/mpeg';
+ break;
+
+ case '.m4a':
+ this.mimeType = 'audio/mp4';
+ break;
+
+ case '.wav':
+ this.mimeType = 'audio/wav';
+ break;
+
+ case '.3gp':
+ this.mimeType = 'video/3gpp';
+ break;
+
default:
throw new UnreachableError(fileExtension);
}
diff --git a/packages/common/src/fixtures/xforms.ts b/packages/common/src/fixtures/xforms.ts
index 106fdffa9..206d9bc98 100644
--- a/packages/common/src/fixtures/xforms.ts
+++ b/packages/common/src/fixtures/xforms.ts
@@ -102,7 +102,7 @@ export class XFormResource {
const service = new JRResourceService();
const parentPath = localPath.replace(/\/[^/]+$/, '');
- service.activateFixtures(parentPath, ['file', 'file-csv', 'images']);
+ service.activateFixtures(parentPath, ['file', 'file-csv', 'images', 'audio', 'video']);
return service;
},
@@ -119,7 +119,7 @@ export class XFormResource {
const service = new JRResourceService();
const parentPath = resourceURL.pathname.replace(/\/[^/]+$/, '');
- service.activateFixtures(parentPath, ['file', 'file-csv', 'images']);
+ service.activateFixtures(parentPath, ['file', 'file-csv', 'images', 'audio', 'video']);
return service;
};
diff --git a/packages/web-forms/src/assets/images/broken-audio.svg b/packages/web-forms/src/assets/images/broken-audio.svg
new file mode 100644
index 000000000..1d2f03406
--- /dev/null
+++ b/packages/web-forms/src/assets/images/broken-audio.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/web-forms/src/assets/images/broken-video.svg b/packages/web-forms/src/assets/images/broken-video.svg
new file mode 100644
index 000000000..b4e4fe7e0
--- /dev/null
+++ b/packages/web-forms/src/assets/images/broken-video.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/web-forms/src/assets/styles/select-options.scss b/packages/web-forms/src/assets/styles/select-options.scss
index e64c193ce..6d9488c04 100644
--- a/packages/web-forms/src/assets/styles/select-options.scss
+++ b/packages/web-forms/src/assets/styles/select-options.scss
@@ -1,3 +1,5 @@
+@use 'primeflex/core/_variables.scss' as pf;
+
.value-option {
--text-content-margin: 15px;
}
@@ -55,10 +57,12 @@
}
:deep(.media-content) {
- background: var(--odk-base-background-color);
+ background: transparent;
height: calc(100% - (var(--odk-base-font-size) + (var(--text-content-margin) * 2)));
- width: 100%;
- justify-items: center;
+ min-height: 54px;
+ flex: 0 1 auto;
+ margin-left: auto;
+ align-content: center;
}
&:has(.media-content) {
@@ -71,8 +75,11 @@
}
:deep(.text-content) {
+ width: fit-content;
+ min-width: unset;
max-width: calc(100% - 70px);
margin: var(--text-content-margin) 20px;
+ flex: 1 1 auto;
}
}
}
@@ -94,4 +101,27 @@
background: var(--odk-light-background-color);
align-content: flex-start;
flex-wrap: wrap;
+
+ :deep(.media-content) {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ justify-content: flex-start;
+ gap: 10px;
+
+ .media-block {
+ border-radius: 0;
+ }
+ }
+}
+
+@media screen and (max-width: #{pf.$sm}) {
+ :not(.select-with-images) .value-option {
+ flex-wrap: wrap;
+
+ :deep(.media-content) {
+ margin-left: unset;
+ }
+ }
}
diff --git a/packages/web-forms/src/components/OdkWebForm.vue b/packages/web-forms/src/components/OdkWebForm.vue
index 58b14be60..debd1f4b8 100644
--- a/packages/web-forms/src/components/OdkWebForm.vue
+++ b/packages/web-forms/src/components/OdkWebForm.vue
@@ -5,7 +5,7 @@ import FormHeader from '@/components/form-layout/FormHeader.vue';
import QuestionList from '@/components/form-layout/QuestionList.vue';
import { waitAllTasksToFinish } from '@/lib/async/event-loop.ts';
import {
- FORM_IMAGE_CACHE,
+ FORM_MEDIA_CACHE,
FORM_OPTIONS,
IS_FORM_EDIT_MODE,
SUBMIT_PRESSED,
@@ -192,7 +192,7 @@ const formOptions = readonly({
geolocationProvider: { getLocation: () => getLocation() },
});
provide(FORM_OPTIONS, formOptions);
-provide(FORM_IMAGE_CACHE, new Map());
+provide(FORM_MEDIA_CACHE, new Map());
const state = initializeFormState();
const submitPressed = ref(false);
diff --git a/packages/web-forms/src/components/common/CheckboxWidget.vue b/packages/web-forms/src/components/common/CheckboxWidget.vue
index 937e1e5b1..0f9fe0bbb 100644
--- a/packages/web-forms/src/components/common/CheckboxWidget.vue
+++ b/packages/web-forms/src/components/common/CheckboxWidget.vue
@@ -40,7 +40,7 @@ const selectValues = (values: readonly string[]) => {
@update:model-value="selectValues"
@change="$emit('change')"
/>
-
+
diff --git a/packages/web-forms/src/components/common/IconSVG.vue b/packages/web-forms/src/components/common/IconSVG.vue
index 1d4fe26ab..40be4621f 100644
--- a/packages/web-forms/src/components/common/IconSVG.vue
+++ b/packages/web-forms/src/components/common/IconSVG.vue
@@ -28,11 +28,13 @@ import {
mdiPlus,
mdiPrinter,
mdiRefresh,
+ mdiStopCircleOutline,
mdiTrashCanOutline,
mdiUnfoldMoreHorizontal,
mdiUpload,
mdiVectorPolygon,
mdiVectorPolyline,
+ mdiVolumeHigh,
mdiWeb,
} from '@mdi/js';
@@ -65,11 +67,13 @@ const iconMap: Record = {
mdiPlus,
mdiPrinter,
mdiRefresh,
+ mdiStopCircleOutline,
mdiTrashCanOutline,
mdiUnfoldMoreHorizontal,
mdiUpload,
mdiVectorPolygon,
mdiVectorPolyline,
+ mdiVolumeHigh,
mdiWeb,
};
diff --git a/packages/web-forms/src/components/common/ImageBlock.vue b/packages/web-forms/src/components/common/ImageBlock.vue
deleted file mode 100644
index d2c3ca204..000000000
--- a/packages/web-forms/src/components/common/ImageBlock.vue
+++ /dev/null
@@ -1,156 +0,0 @@
-
-
-
-
-
-
![]()
-
-
-
-
-
-
- {{ errorMessage }}
-
-
-
-
-
-
diff --git a/packages/web-forms/src/components/common/LikertWidget.vue b/packages/web-forms/src/components/common/LikertWidget.vue
index d1e5738ab..f0f83dc65 100644
--- a/packages/web-forms/src/components/common/LikertWidget.vue
+++ b/packages/web-forms/src/components/common/LikertWidget.vue
@@ -73,7 +73,7 @@ defineEmits(['change']);
.text-content {
text-align: center;
margin: -11.5px 0 10px 0;
- width: 100%;
+ width: fit-content;
position: relative;
padding-top: 20px;
word-break: break-word;
diff --git a/packages/web-forms/src/components/common/RadioButton.vue b/packages/web-forms/src/components/common/RadioButton.vue
index 25fa0ceb7..b4cf4c695 100644
--- a/packages/web-forms/src/components/common/RadioButton.vue
+++ b/packages/web-forms/src/components/common/RadioButton.vue
@@ -40,7 +40,7 @@ const selectValue = (value: string) => {
@update:model-value="selectValue"
@change="$emit('change')"
/>
-
+
diff --git a/packages/web-forms/src/components/common/TextMedia.vue b/packages/web-forms/src/components/common/TextMedia.vue
index 97d0a4f97..ea1056cbe 100644
--- a/packages/web-forms/src/components/common/TextMedia.vue
+++ b/packages/web-forms/src/components/common/TextMedia.vue
@@ -1,11 +1,14 @@
-
+
+
-
-
-
🚧 Video media type is not supported
-
-
-
🚧 Video media type is not supported
+
+
@@ -37,4 +37,11 @@ const audio = computed(() => props.label.audioSource);
.text-content {
margin-left: 0;
}
+
+.text-content.audio-icons-only {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 10px;
+}
diff --git a/packages/web-forms/src/components/common/media/AudioBlock.vue b/packages/web-forms/src/components/common/media/AudioBlock.vue
new file mode 100644
index 000000000..b09cf5b20
--- /dev/null
+++ b/packages/web-forms/src/components/common/media/AudioBlock.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/web-forms/src/components/common/media/ImageBlock.vue b/packages/web-forms/src/components/common/media/ImageBlock.vue
new file mode 100644
index 000000000..c25bd2af5
--- /dev/null
+++ b/packages/web-forms/src/components/common/media/ImageBlock.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/web-forms/src/components/common/media/MediaBlockBase.vue b/packages/web-forms/src/components/common/media/MediaBlockBase.vue
new file mode 100644
index 000000000..d04e719e7
--- /dev/null
+++ b/packages/web-forms/src/components/common/media/MediaBlockBase.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
diff --git a/packages/web-forms/src/components/common/media/VideoBlock.vue b/packages/web-forms/src/components/common/media/VideoBlock.vue
new file mode 100644
index 000000000..3fee34905
--- /dev/null
+++ b/packages/web-forms/src/components/common/media/VideoBlock.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/web-forms/src/components/form-elements/ControlLabel.vue b/packages/web-forms/src/components/form-elements/ControlLabel.vue
index c23b8f592..41595bf2f 100644
--- a/packages/web-forms/src/components/form-elements/ControlLabel.vue
+++ b/packages/web-forms/src/components/form-elements/ControlLabel.vue
@@ -1,6 +1,8 @@