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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ - - - - - 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 @@ @@ -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 @@