|
| 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> |
0 commit comments