Skip to content

Commit e2224b0

Browse files
- ADD: Added internal camera view instead of getting photos/videos from external camera.
-
1 parent b771f66 commit e2224b0

16 files changed

Lines changed: 796 additions & 6 deletions

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default vuetify({
1414
'unicorn/no-new-array': 'off',
1515
'unicorn/no-for-loop': 'off',
1616
'unicorn/no-array-callback-reference': 'off',
17+
'unicorn/prefer-dom-node-dataset': 'off',
1718
'unicorn/catch-error-name': 'off',
1819
'unicorn/no-document-cookie': 'off',
1920
'unicorn/prefer-at': 'off',

src/components.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ declare module 'vue' {
2020
BrapiExportSection: typeof import('./components/dataexport/BrapiExportSection.vue')['default']
2121
BrapiStudySelect: typeof import('./components/util/BrapiStudySelect.vue')['default']
2222
CalendarHeatmap: typeof import('./components/chart/CalendarHeatmap.vue')['default']
23+
CameraView: typeof import('./components/util/CameraView.vue')['default']
2324
CellAutocomplete: typeof import('./components/inputs/CellAutocomplete.vue')['default']
2425
ChangelogModal: typeof import('./components/modals/ChangelogModal.vue')['default']
2526
ColumnHeader: typeof import('./components/data/ColumnHeader.vue')['default']
2627
CommentModal: typeof import('./components/modals/CommentModal.vue')['default']
2728
ConfirmModal: typeof import('./components/modals/ConfirmModal.vue')['default']
29+
copy: typeof import('./components/modals/InternalCameraModal.vue/index.js')['default']
2830
CornerPointsMap: typeof import('./components/setup/CornerPointsMap.vue')['default']
2931
DataCanvas: typeof import('./components/data/DataCanvas.vue')['default']
3032
DataEntryActions: typeof import('./components/modals/DataEntryActions.vue')['default']
@@ -46,6 +48,7 @@ declare module 'vue' {
4648
HelpCard: typeof import('./components/util/HelpCard.vue')['default']
4749
HighlightSelect: typeof import('./components/util/HighlightSelect.vue')['default']
4850
HScroll: typeof import('./components/data/HScroll.vue')['default']
51+
InternalCameraModal: typeof import('./components/modals/InternalCameraModal.vue')['default']
4952
JumpToDropdown: typeof import('./components/util/JumpToDropdown.vue')['default']
5053
LabelEditor: typeof import('./components/setup/LabelEditor.vue')['default']
5154
LayoutDimensions: typeof import('./components/setup/LayoutDimensions.vue')['default']
@@ -90,6 +93,7 @@ declare module 'vue' {
9093
TraitTimeframeChart: typeof import('./components/chart/TraitTimeframeChart.vue')['default']
9194
TraitTimelineChart: typeof import('./components/chart/TraitTimelineChart.vue')['default']
9295
TraitTreeSelect: typeof import('./components/trait/TraitTreeSelect.vue')['default']
96+
TrialActivity: typeof import('./components/trial/TrialActivity.vue')['default']
9397
TrialCard: typeof import('./components/trial/TrialCard.vue')['default']
9498
TrialCreation: typeof import('./components/trial/TrialCreation.vue')['default']
9599
TrialCreationFromBrapiModal: typeof import('./components/modals/TrialCreationFromBrapiModal.vue')['default']
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<template>
2+
<v-dialog v-model="dialog" fullscreen>
3+
<v-card>
4+
<CameraView :can-switch-modes="false" :selected-mode="selectedMode" @media-selected="handleMediaSelected" />
5+
</v-card>
6+
</v-dialog>
7+
</template>
8+
9+
<script setup lang="ts">
10+
const dialog = ref(false)
11+
12+
export interface CameraViewModalProps {
13+
selectedMode?: 'image' | 'video'
14+
}
15+
16+
const compProps = withDefaults(defineProps<CameraViewModalProps>(), {
17+
selectedMode: 'image',
18+
})
19+
20+
function handleMediaSelected (data: Blob) {
21+
emit('media-selected', data)
22+
hide()
23+
}
24+
function show () {
25+
dialog.value = true
26+
}
27+
function hide () {
28+
dialog.value = false
29+
}
30+
31+
defineExpose({
32+
show,
33+
hide,
34+
})
35+
36+
const emit = defineEmits(['media-selected'])
37+
</script>
38+
39+
<style>
40+
</style>

src/components/modals/MediaModal.vue

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
</v-card-actions>
7373
</v-card>
7474
</v-dialog>
75+
76+
<InternalCameraModal v-if="store.storeCameraMode === 'internal'" :selected-mode="mode" ref="internalCameraModal" @media-selected="setMediaFromInternal" />
7577
</template>
7678

7779
<script setup lang="ts">
@@ -108,6 +110,7 @@
108110
const imageInput = useTemplateRef('imageInput')
109111
const videoInput = useTemplateRef('videoInput')
110112
const video = useTemplateRef('video')
113+
const internalCameraModal = useTemplateRef('internalCameraModal')
111114
112115
const canContinue = computed(() => {
113116
return mediaData.value !== undefined
@@ -143,6 +146,14 @@
143146
return mapped.join('_') + extension
144147
})
145148
149+
function setMediaFromInternal (newData: Blob) {
150+
// Convert to base64 for displaying
151+
inputFile.value = new File([newData], `internal.${mode.value === 'image' ? 'jpg' : '.mp4'}`, {
152+
type: newData.type,
153+
lastModified: Date.now(),
154+
})
155+
}
156+
146157
watch(inputFile, newValue => {
147158
// If there is a new media file, reset data
148159
inputFileDate.value = undefined
@@ -190,6 +201,47 @@
190201
}
191202
})
192203
204+
// async function save () {
205+
// if (inputFile.value && mediaData.value) {
206+
// const file = new File([inputFile.value], filename.value, { type: inputFile.value.type })
207+
208+
// // 1. File System Access API — best UX on desktop, works on newer Samsung Internet too
209+
// if ('showSaveFilePicker' in window) {
210+
// try {
211+
// // @ts-expect-error
212+
// const handle = await window.showSaveFilePicker({ suggestedName: filename.value })
213+
// const writable = await handle.createWritable()
214+
// await writable.write(inputFile.value)
215+
// await writable.close()
216+
// return
217+
// } catch (e: unknown) {
218+
// if (e instanceof DOMException && e.name === 'AbortError') {
219+
// return
220+
// }
221+
// console.warn('showSaveFilePicker failed:', e)
222+
// // Fall through
223+
// }
224+
// }
225+
226+
// // 2. Web Share API — best on mobile where showSaveFilePicker is absent
227+
// if (navigator.canShare?.({ files: [file] })) {
228+
// try {
229+
// await navigator.share({ files: [file], title: filename.value })
230+
// return
231+
// } catch (e: unknown) {
232+
// if (e instanceof DOMException && e.name === 'AbortError') {
233+
// return
234+
// }
235+
// console.warn('Web Share failed:', e)
236+
// // Fall through
237+
// }
238+
// }
239+
240+
// // 3. FileSaver.js — desktop browsers without File System Access, and most other cases
241+
// saveAs(inputFile.value, filename.value)
242+
// }
243+
// }
244+
193245
function save () {
194246
if (mediaData.value) {
195247
saveAs(mediaData.value, filename.value)
@@ -219,10 +271,15 @@
219271
cell.value = c
220272
})
221273
dialog.value = true
222-
if (type === 'image') {
223-
imageInput.value?.click()
274+
275+
if (store.storeCameraMode === 'internal') {
276+
internalCameraModal.value?.show()
224277
} else {
225-
videoInput.value?.click()
278+
if (type === 'image') {
279+
imageInput.value?.click()
280+
} else {
281+
videoInput.value?.click()
282+
}
226283
}
227284
}
228285
function hide () {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<template>
2+
<div ref="parent" class="heatmap-container" />
3+
</template>
4+
5+
<script setup lang="ts">
6+
import { createColorGradient } from '@/plugins/color'
7+
8+
const compProps = defineProps<{
9+
data: number[]
10+
}>()
11+
12+
const parent = useTemplateRef('parent')
13+
const width = ref(0)
14+
const gap = shallowRef(2)
15+
const indicatorWidth = shallowRef(8)
16+
const indicatorHeight = shallowRef(4)
17+
18+
const visibleData = computed(() => {
19+
const maxItemsThatFit = Math.floor((width.value + gap.value) / (indicatorWidth.value + gap.value))
20+
21+
return compProps.data.slice(-maxItemsThatFit)
22+
})
23+
24+
const colorGradient = computed(() => {
25+
const max = Math.max.apply(null, visibleData.value || [])
26+
27+
return createColorGradient('#cccccc', '#00acef', max)
28+
})
29+
30+
watch(colorGradient, async () => {
31+
const p = parent.value
32+
33+
if (p) {
34+
p.innerHTML = ''
35+
36+
visibleData.value.forEach(dp => {
37+
const bar = document.createElement('div')
38+
bar.classList.add('heatmap-bar')
39+
40+
// Set heatmap color dynamically via CSS Variable
41+
// const color = getColorForValue(item.value, min, max)
42+
bar.style.setProperty('--bg-color', colorGradient.value[dp] || '#00acef')
43+
44+
// Add data for the CSS tooltip
45+
// bar.setAttribute('data-tooltip', `${item.date}: ${item.value}`)
46+
bar.setAttribute('data-tooltip', `${dp}`)
47+
bar.style.setProperty('height', `${indicatorHeight.value}px`)
48+
49+
p.appendChild(bar)
50+
})
51+
}
52+
}, { immediate: true })
53+
54+
onMounted(() => {
55+
width.value = parent.value?.offsetWidth || 100
56+
})
57+
</script>
58+
59+
<style>
60+
.heatmap-container {
61+
display: flex;
62+
gap: 2px; /* Spacing between blocks */
63+
width: 100%; /* Full width of parent */
64+
box-sizing: border-box;
65+
66+
overflow: visible !important;
67+
}
68+
69+
.heatmap-bar {
70+
flex: 1; /* Dynamic width: stretches to fill available space */
71+
border-radius: 4px;
72+
position: relative;
73+
cursor: pointer;
74+
}
75+
76+
/* Heatmap Color Scale (Using CSS Variables for clean JS injection) */
77+
.heatmap-bar {
78+
/* Default background, overridden by JS */
79+
background-color: var(--bg-color, #e0e0e0);
80+
}
81+
82+
/* Simple Tooltip on Hover */
83+
.heatmap-bar::after {
84+
content: attr(data-tooltip);
85+
position: absolute;
86+
bottom: 125%;
87+
left: 50%;
88+
transform: translateX(-50%);
89+
background: #333;
90+
color: #fff;
91+
padding: 4px 8px;
92+
border-radius: 4px;
93+
font-size: 12px;
94+
white-space: nowrap;
95+
opacity: 0;
96+
pointer-events: none;
97+
transition: opacity 0.2s;
98+
z-index: 10;
99+
}
100+
101+
.heatmap-bar:hover::after {
102+
opacity: 1;
103+
}
104+
</style>

src/components/trial/TrialCard.vue

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<template>
2-
<v-card :variant="isSelected ? 'outlined' : undefined" class="d-flex flex-column flex-grow-1">
2+
<v-card :variant="isSelected ? 'outlined' : undefined" class="d-flex flex-column flex-grow-1 trial-card">
3+
<!-- <template #loader>
4+
<TrialActivity :data="trialActivityData" v-if="trialActivityData" />
5+
</template> -->
36
<template #title>
47
<span v-tooltip:top="trial.name" class="clamp-two">{{ trial.name }}</span>
58
</template>
@@ -251,6 +254,31 @@
251254
const canEdit = computed(() => compProps.trial.editable === true && compProps.trial.isLocked !== true)
252255
const showDetails = computed(() => compProps.forceShowDetails || (store.storeTrialShowDetails !== false))
253256
257+
// const activity = ref<TrialActivity>()
258+
// const trialActivityData = computed(() => {
259+
// if (activity.value) {
260+
// const dates = Object.keys(activity.value).sort((a, b) => a.localeCompare(b))
261+
262+
// const startDate = dates[0] || new Date()
263+
264+
// const values: number[] = []
265+
// // Parse the start date and get "today" at midnight for an accurate comparison
266+
// const currentDate = new Date(startDate)
267+
// const endDate = new Date()
268+
// endDate.setHours(0, 0, 0, 0)
269+
270+
// while (currentDate <= endDate) {
271+
// const formattedDate = toLocalDateString(currentDate)
272+
// values.push(activity.value[formattedDate] || 0)
273+
// currentDate.setDate(currentDate.getDate() + 1)
274+
// }
275+
276+
// return values
277+
// } else {
278+
// return undefined
279+
// }
280+
// })
281+
254282
const hasTimeframeTraits = computed(() => compProps.trial?.traits.some(t => t.timeframe))
255283
256284
const trialDuration = computed(() => {
@@ -311,4 +339,21 @@
311339
emitter.emit('plausible-event', { key: 'trial-comment', props: { type: 'deleted' } })
312340
})
313341
}
342+
343+
// function getActivity () {
344+
// extractTrialActivity(compProps.trial.localId || '')
345+
// .then(act => {
346+
// activity.value = act
347+
// })
348+
// }
349+
350+
// onMounted(() => {
351+
// getActivity()
352+
// })
314353
</script>
354+
355+
<style>
356+
/* .trial-card {
357+
overflow: visible !important;
358+
} */
359+
</style>

0 commit comments

Comments
 (0)