Skip to content

Commit 0eef9bc

Browse files
committed
components: Add pre-arm checklist
Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
1 parent 06cf262 commit 0eef9bc

4 files changed

Lines changed: 294 additions & 2 deletions

File tree

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
<template>
2+
<v-dialog v-model="openDialog" max-width="600" scrollable persistent>
3+
<v-card :style="interfaceStore.globalGlassMenuStyles" class="rounded-lg">
4+
<v-card-title class="text-center relative pt-3">
5+
<h6 class="ml-4">Warning! Your vehicle is about to be armed</h6>
6+
<v-btn icon variant="text" color="white" class="absolute right-1 top-1" aria-label="Close" @click="onCancel">
7+
<v-icon>mdi-close</v-icon>
8+
</v-btn>
9+
</v-card-title>
10+
<v-card-text class="mt-[-10px]">
11+
<v-list lines="one" theme="dark" class="bg-transparent">
12+
<v-list-item v-for="item in checklistItems" :key="item.id" theme="dark">
13+
<template #prepend>
14+
<v-checkbox
15+
v-model="doneById[item.id]"
16+
class="mt-0"
17+
hide-details
18+
density="compact"
19+
:disabled="!missionStore.showChecklistBeforeArm"
20+
/>
21+
</template>
22+
<v-list-item-title
23+
:class="[
24+
{ 'line-through opacity-60': doneById[item.id] },
25+
{ 'opacity-50': !missionStore.showChecklistBeforeArm },
26+
]"
27+
>
28+
{{ item.text }}
29+
</v-list-item-title>
30+
<template v-if="isOnEditMode && checklistItems.length > 1" #append>
31+
<v-btn icon variant="text" :disabled="!missionStore.showChecklistBeforeArm" @click="removeItem(item.id)">
32+
<v-icon>mdi-close</v-icon>
33+
</v-btn>
34+
</template>
35+
</v-list-item>
36+
</v-list>
37+
<div class="flex justify-end">
38+
<div v-if="!isOnEditMode" class="fixed top-[80px] right-5">
39+
<v-btn
40+
prepend-icon="mdi-format-list-checks"
41+
:disabled="!missionStore.showChecklistBeforeArm"
42+
variant="text"
43+
color="white"
44+
size="small"
45+
class="elevation-1 bg-[#FFFFFF15] hover:bg-[#FFFFFF22]"
46+
@click="isOnEditMode = true"
47+
>
48+
Edit Items
49+
</v-btn>
50+
</div>
51+
<div v-else class="flex w-full items-center">
52+
<v-text-field
53+
v-if="isOnEditMode"
54+
ref="newItemInput"
55+
v-model="newItemText"
56+
:disabled="!missionStore.showChecklistBeforeArm"
57+
variant="filled"
58+
density="compact"
59+
placeholder="Type an item and press Enter"
60+
hide-details
61+
class="ml-6 w-[80%]"
62+
append-inner-icon="mdi-plus"
63+
@keyup.enter="addItem"
64+
@click:append-inner="addItem"
65+
/>
66+
<v-btn
67+
prepend-icon="mdi-check"
68+
:disabled="!missionStore.showChecklistBeforeArm"
69+
variant="text"
70+
color="white"
71+
size="small"
72+
class="elevation-1 bg-[#FFFFFF15] hover:bg-[#FFFFFF22] ml-10"
73+
@click="
74+
() => {
75+
isOnEditMode = false
76+
addItem()
77+
}
78+
"
79+
>Done</v-btn
80+
>
81+
</div>
82+
</div>
83+
</v-card-text>
84+
<v-divider class="flex center w-[80%]" inset />
85+
<v-card-actions>
86+
<div class="flex justify-between w-full py-1 px-2 pt-3">
87+
<v-btn variant="text" @click="onCancel">Cancel</v-btn>
88+
<div class="flex items-center">
89+
<v-checkbox
90+
:model-value="!missionStore.showChecklistBeforeArm"
91+
class="mt-0 scale-[0.8]"
92+
hide-details
93+
density="compact"
94+
@update:model-value="(val) => (missionStore.showChecklistBeforeArm = !val)"
95+
/>
96+
<p class="text-xs text-center ml-2">Don't show this checklist again</p>
97+
</div>
98+
<v-btn
99+
color="#ffffff33"
100+
:class="{ 'opacity-30': !isArmingEnabled }"
101+
variant="flat"
102+
:disabled="!isArmingEnabled"
103+
@click="onConfirm"
104+
>Go</v-btn
105+
>
106+
</div>
107+
</v-card-actions>
108+
</v-card>
109+
</v-dialog>
110+
</template>
111+
112+
<script setup lang="ts">
113+
import { computed, nextTick, ref, watch } from 'vue'
114+
import { onMounted } from 'vue'
115+
116+
import { useBlueOsStorage } from '@/composables/settingsSyncer'
117+
import { openSnackbar } from '@/composables/snackbar'
118+
import { useAppInterfaceStore } from '@/stores/appInterface'
119+
import { useMissionStore } from '@/stores/mission'
120+
121+
type ChecklistItem = {
122+
/**
123+
* Unique identifier for the checklist item
124+
*/
125+
id: number
126+
/**
127+
* Text description of the checklist item
128+
*/
129+
text: string
130+
}
131+
132+
const interfaceStore = useAppInterfaceStore()
133+
const missionStore = useMissionStore()
134+
135+
const props = defineProps<{
136+
/**
137+
* Whether the dialog is open
138+
*/
139+
modelValue: boolean
140+
}>()
141+
142+
const emit = defineEmits<{
143+
(e: 'update:modelValue', value: boolean): void
144+
(e: 'confirmed', value: boolean): void
145+
(e: 'dismissed'): void
146+
}>()
147+
148+
const checklistItems = useBlueOsStorage<ChecklistItem[]>('cockpit-pre-arm-checklist', [])
149+
150+
const doneById = ref<Record<number, boolean>>({})
151+
const newItemText = ref('')
152+
const newItemInput = ref<any>(null)
153+
const isOnEditMode = ref(false)
154+
155+
const openDialog = computed(() => props.modelValue)
156+
157+
const resetDoneState = (): void => {
158+
const next: Record<number, boolean> = {}
159+
for (const i of checklistItems.value ?? []) next[i.id] = false
160+
doneById.value = next
161+
}
162+
163+
const isArmingEnabled = computed<boolean>(() => {
164+
if (!missionStore.showChecklistBeforeArm) return true
165+
const list = checklistItems.value ?? []
166+
return list.length > 0 && list.every((i) => doneById.value[i.id] === true)
167+
})
168+
169+
watch(
170+
() => openDialog.value,
171+
(open) => {
172+
if (open) {
173+
resetDoneState()
174+
nextTick(() => newItemInput.value?.focus?.())
175+
} else {
176+
doneById.value = {}
177+
isOnEditMode.value = false
178+
}
179+
}
180+
)
181+
182+
watch(
183+
() => checklistItems.value,
184+
(list) => {
185+
if (!openDialog.value) return
186+
const current = { ...doneById.value }
187+
const keep: Record<number, true> = {}
188+
for (const i of list ?? []) {
189+
keep[i.id] = true
190+
if (!(i.id in current)) current[i.id] = false
191+
}
192+
for (const idStr of Object.keys(current)) {
193+
const id = Number(idStr)
194+
if (!keep[id]) delete current[id]
195+
}
196+
doneById.value = current
197+
},
198+
{ deep: true }
199+
)
200+
201+
const addItem = async (): Promise<void> => {
202+
const text = newItemText.value.trim()
203+
if (!text) return
204+
const newEntry: ChecklistItem = { id: Date.now(), text }
205+
checklistItems.value.push(newEntry)
206+
if (openDialog.value) doneById.value[newEntry.id] = false
207+
newItemText.value = ''
208+
await nextTick()
209+
newItemInput.value?.focus?.()
210+
}
211+
212+
const removeItem = (id: number): void => {
213+
checklistItems.value = checklistItems.value.filter((i) => i.id !== id)
214+
if (id in doneById.value) {
215+
const copy = { ...doneById.value }
216+
delete copy[id]
217+
doneById.value = copy
218+
}
219+
}
220+
221+
const onCancel = (): void => {
222+
emit('dismissed')
223+
emit('update:modelValue', false)
224+
}
225+
226+
const onConfirm = (): void => {
227+
emit('confirmed', true)
228+
emit('update:modelValue', false)
229+
}
230+
231+
onMounted(() => {
232+
if (checklistItems.value.length === 0) {
233+
checklistItems.value = [{ id: Date.now(), text: 'Ensure the vehicle is safe to launch' }]
234+
}
235+
if (openDialog.value) {
236+
resetDoneState()
237+
}
238+
})
239+
240+
watch(
241+
() => missionStore.showChecklistBeforeArm,
242+
(value) => {
243+
if (!value) {
244+
openSnackbar({
245+
message: `Pre-arm checklist disabled. You can re-enable it in Settings → Mission → Enable pre-arm checklist.`,
246+
variant: 'info',
247+
duration: 5000,
248+
})
249+
}
250+
},
251+
{ immediate: true }
252+
)
253+
</script>
254+
255+
<style scoped>
256+
.line-through {
257+
text-decoration: line-through;
258+
}
259+
</style>

src/components/widgets/Map.vue

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
style="z-index: 1002; border-radius: 0px"
7373
icon="mdi-play"
7474
size="x-small"
75-
@click.stop="executeMissionOnVehicle"
75+
@click.stop="tryToStartMission"
7676
/>
7777
</template>
7878
</v-tooltip>
@@ -121,6 +121,11 @@
121121
</p>
122122

123123
<PoiManager ref="poiManagerMapWidgetRef" />
124+
<MissionChecklist
125+
:model-value="isMissionChecklistOpen"
126+
@confirmed="executeMissionOnVehicle"
127+
@update:model-value="isMissionChecklistOpen = $event"
128+
/>
124129
</template>
125130

126131
<script setup lang="ts">
@@ -144,6 +149,7 @@ import {
144149
import blueboatMarkerImage from '@/assets/blueboat-marker.png'
145150
import brov2MarkerImage from '@/assets/brov2-marker.png'
146151
import genericVehicleMarkerImage from '@/assets/generic-vehicle-marker.png'
152+
import MissionChecklist from '@/components/MissionChecklist.vue'
147153
import PoiManager from '@/components/poi/PoiManager.vue'
148154
import { useInteractionDialog } from '@/composables/interactionDialog'
149155
import { openSnackbar } from '@/composables/snackbar'
@@ -183,6 +189,8 @@ const mapWaypoints = ref<Waypoint[]>([])
183189
const contextMenuRef = ref()
184190
const isDragging = ref(false)
185191
const isPinching = ref(false)
192+
const isMissionChecklistOpen = ref(false)
193+
186194
let pinchTimeout: number | undefined
187195
188196
const onTouchStart = (e: TouchEvent): void => {
@@ -857,6 +865,15 @@ const executeMissionOnVehicle = async (): Promise<void> => {
857865
} catch (error) {
858866
openSnackbar({ message: 'Failed to start mission.', variant: 'error' })
859867
}
868+
return
869+
}
870+
871+
const tryToStartMission = async (): Promise<void> => {
872+
if (missionStore.showChecklistBeforeArm) {
873+
isMissionChecklistOpen.value = true
874+
return
875+
}
876+
executeMissionOnVehicle()
860877
}
861878
862879
// Set dynamic styles for correct displacement of the bottom buttons when the widget is below the bottom bar

src/stores/mission.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const useMissionStore = defineStore('mission', () => {
4040
const vehicleMissionRevision = useBlueOsStorage<number>('cockpit-vehicle-mission-rev', 0)
4141
const alwaysSwitchToFlightMode = useBlueOsStorage('cockpit-mission-always-switch-to-flight-mode', false)
4242
const showMissionCreationTips = useBlueOsStorage('cockpit-show-mission-creation-tips', true)
43+
const showChecklistBeforeArm = useBlueOsStorage('cockpit-show-checklist-before-arm', true)
4344

4445
const { showDialog } = useInteractionDialog()
4546

@@ -212,5 +213,6 @@ export const useMissionStore = defineStore('mission', () => {
212213
vehicleMissionRevision,
213214
alwaysSwitchToFlightMode,
214215
showMissionCreationTips,
216+
showChecklistBeforeArm,
215217
}
216218
})

src/views/ConfigurationMissionView.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,37 @@
66
class="flex flex-col justify-between items-start ml-[1vw] max-h-[85vh] overflow-y-auto"
77
:class="interfaceStore.isOnSmallScreen ? 'max-w-[70vw]' : 'max-w-[40vw]'"
88
>
9-
<div class="flex justify-start items-center gap-x-4">
9+
<div class="grid grid-cols-3 gap-x-4 mb-4">
10+
<v-switch
11+
v-model="missionStore.showChecklistBeforeArm"
12+
label="Enable pre-arm checklist"
13+
color="white"
14+
hide-details
15+
base-color="#FFFFFF33"
16+
class="mt-2 -mb-2 ml-3"
17+
/>
1018
<v-switch
1119
v-model="missionStore.slideEventsEnabled"
1220
label="Enable slide to confirm"
1321
color="white"
22+
hide-details
23+
base-color="#FFFFFF33"
1424
class="mt-2 -mb-2 ml-3"
1525
/>
1626
<v-switch
1727
v-model="missionStore.alwaysSwitchToFlightMode"
1828
label="Auto switch to flight mode on mission upload"
1929
color="white"
30+
hide-details
31+
base-color="#FFFFFF33"
2032
class="mt-2 -mb-2 ml-3"
2133
/>
2234
<v-switch
2335
v-model="missionStore.showMissionCreationTips"
2436
label="Show mission creation checklist"
2537
color="white"
38+
hide-details
39+
base-color="#FFFFFF33"
2640
class="mt-2 -mb-2 ml-3"
2741
/>
2842
</div>

0 commit comments

Comments
 (0)