Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hold to confirm functionality in action that need confirmation #822

Merged
merged 9 commits into from Apr 11, 2024
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -24,6 +24,7 @@
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^3.0.3",
"@j2only/slide-unlock": "^0.5.5",
"@mdi/font": "^7.0.96",
"@mdi/js": "^7.2.96",
"@vue-leaflet/vue-leaflet": "^0.6.1",
Expand Down Expand Up @@ -53,7 +54,6 @@
"vue": "^3.4.21",
"vue-draggable-plus": "^0.2.0-beta.2",
"vue-router": "^4.0.14",
"vue-slide-unlock": "^0.4.8",
"vue-virtual-scroller": "^2.0.0-beta.8",
"vue3-slider": "^1.9.0",
"vuetify": "3.5.8",
Expand Down
106 changes: 97 additions & 9 deletions src/components/SlideToConfirm.vue
Expand Up @@ -3,16 +3,17 @@
<div class="flex items-center space-x-4 mb-3">
<slide-unlock
ref="vueslideunlock"
:position="sliderPercentage"
:auto-width="false"
:circle="true"
:disabled="false"
:disabled="cancelled || expired"
:noanimate="false"
:width="400"
:height="50"
:text="sliderText"
:success-text="confirmationSliderText"
name="slideunlock"
class="slide-unlock"
:class="notConfirmedClasses"
@completed="onSlideConfirmed()"
/>
<button
Expand All @@ -26,23 +27,110 @@
</template>

<script setup lang="ts">
import SlideUnlock from 'vue-slide-unlock'
import SlideUnlock from '@j2only/slide-unlock'
import { v4 as uuid } from 'uuid'
import { computed, ref, watch } from 'vue'

import { confirmationSliderText, confirmed, showSlideToConfirm, sliderText } from '@/libs/slide-to-confirm'
import {
confirmationSliderText,
deniedText,
expiredText,
onAction,
showSlideToConfirm,
sliderPercentage,
sliderText,
} from '@/libs/slide-to-confirm'

/**
* Timeout constant used to keep track of the slide expiration
* @type {string}
*/
const slideExpireIntervalMs = 5000

// Unique id for each slide open to keep track of timeouts
const slideUniqueId = ref('')

// Local states for slide unlock
const cancelled = ref(false)
const expired = ref(false)

// Watch for changes in showSlideToConfirm to assign new uuid
watch(showSlideToConfirm, (value) => {
if (value) {
slideUniqueId.value = uuid()
// Creates local copy within lambda
const oldUniqueId = String(slideUniqueId.value)

setTimeout(() => {
if (slideUniqueId.value === oldUniqueId) {
expiresAction()
}
}, slideExpireIntervalMs)
}
})

const notConfirmedClasses = computed(() => {
if (cancelled.value) {
return 'slide-not-confirmed slide-unlock-denied'
} else if (expired.value) {
return 'slide-not-confirmed slide-unlock-expired'
}

return ''
})

const onSlideConfirmed = (): void => {
confirmed.value = true
// Call action with confirmed
onAction.value?.(true)
slideUniqueId.value = ''

// show success message for 1.5 second
setTimeout(() => {
showSlideToConfirm.value = false
console.log('Slide confirmed!')
}, 1500)
}

const cancelAction = (): void => {
showSlideToConfirm.value = false
confirmed.value = false
console.log('Slide canceled!')
// Call action with confirmed
onAction.value?.(false)
slideUniqueId.value = ''

// show denied message for 1.5 second
cancelled.value = true
sliderText.value = deniedText.value

setTimeout(() => {
cancelled.value = false
showSlideToConfirm.value = false
}, 1500)
}

const expiresAction = (): void => {
// Call action with confirmed
onAction.value?.(false)
slideUniqueId.value = ''

// show expired message for 1.5 second
expired.value = true
sliderText.value = expiredText.value

setTimeout(() => {
expired.value = false
showSlideToConfirm.value = false
}, 1500)
}
</script>

<style scoped>
.slide-not-confirmed {
opacity: 1;
--su-icon-handler: '';
--su-color-text-normal: white;
}
.slide-unlock-denied {
--su-color-bg: #b34a4d;
}
.slide-unlock-expired {
--su-color-bg: #e3a008;
}
</style>
23 changes: 22 additions & 1 deletion src/components/mini-widgets/ArmerButton.vue
@@ -1,7 +1,7 @@
<template>
<button
class="relative flex items-center justify-center w-32 p-1 rounded-md shadow-inner h-9 bg-slate-800/60"
@click="vehicleStore.isArmed ? vehicleStore.disarm() : vehicleStore.arm()"
@click="vehicleStore.isArmed ? disarm() : arm()"
>
<div
class="absolute top-auto flex items-center px-1 rounded-[4px] shadow transition-all w-[70%] h-[80%]"
Expand All @@ -21,7 +21,28 @@
</template>

<script setup lang="ts">
import { canByPassCategory, EventCategory, slideToConfirm } from '@/libs/slide-to-confirm'
import { useMainVehicleStore } from '@/stores/mainVehicle'

const vehicleStore = useMainVehicleStore()

const arm = (): void => {
slideToConfirm(
vehicleStore.arm,
{
command: 'Arm',
},
canByPassCategory(EventCategory.ARM)
)
}

const disarm = (): void => {
slideToConfirm(
vehicleStore.disarm,
{
command: 'Disarm',
},
canByPassCategory(EventCategory.DISARM)
)
}
</script>
19 changes: 18 additions & 1 deletion src/components/mini-widgets/ChangeAltitudeCommander.vue
Expand Up @@ -5,14 +5,31 @@
vehicleStore.flying ? 'bg-slate-800/60 hover:bg-slate-400/60' : 'bg-slate-400/60 cursor-not-allowed',
]"
:disabled="!vehicleStore.flying"
@click="vehicleStore.changeAlt()"
@click="changeAlt()"
>
<span class="inline-block font-extrabold align-middle text-white"> Change Alt </span>
</button>
</template>

<script setup lang="ts">
import { showAltitudeSlider } from '@/libs/altitude-slider'
import { canByPassCategory, EventCategory, slideToConfirm } from '@/libs/slide-to-confirm'
import { useMainVehicleStore } from '@/stores/mainVehicle'

const vehicleStore = useMainVehicleStore()

const changeAlt = (): void => {
showAltitudeSlider.value = true

slideToConfirm(
() => {
showAltitudeSlider.value = false
vehicleStore.changeAlt()
},
{
command: 'Altitude Change',
},
canByPassCategory(EventCategory.ALT_CHANGE)
)
}
</script>
29 changes: 28 additions & 1 deletion src/components/mini-widgets/TakeoffLandCommander.vue
@@ -1,7 +1,7 @@
<template>
<button
class="relative flex items-center justify-center w-32 p-1 rounded-md shadow-inner h-9 bg-slate-800/60 hover:bg-slate-400/60"
@click="vehicleStore.flying ? vehicleStore.land() : vehicleStore.takeoff()"
@click="vehicleStore.flying ? land() : takeoff()"
>
<span class="inline-block font-extrabold align-middle text-white">
{{ vehicleStore.flying === undefined ? '...' : vehicleStore.flying ? 'Land' : 'Takeoff' }}
Expand All @@ -10,7 +10,34 @@
</template>

<script setup lang="ts">
import { showAltitudeSlider } from '@/libs/altitude-slider'
import { canByPassCategory, EventCategory, slideToConfirm } from '@/libs/slide-to-confirm'
import { useMainVehicleStore } from '@/stores/mainVehicle'

const vehicleStore = useMainVehicleStore()

const takeoff = (): void => {
showAltitudeSlider.value = true

slideToConfirm(
() => {
showAltitudeSlider.value = false
vehicleStore.takeoff()
},
{
command: 'Takeoff',
},
canByPassCategory(EventCategory.TAKEOFF)
)
}

const land = (): void => {
slideToConfirm(
vehicleStore.land,
{
command: 'Land',
},
canByPassCategory(EventCategory.LAND)
)
}
</script>
11 changes: 10 additions & 1 deletion src/components/widgets/Map.vue
Expand Up @@ -162,6 +162,7 @@ import {
} from 'vue'

import { datalogger, DatalogVariable } from '@/libs/sensors-logging'
import { canByPassCategory, EventCategory, slideToConfirm } from '@/libs/slide-to-confirm'
import { degrees } from '@/libs/utils'
import { TargetFollower, WhoToFollow } from '@/libs/utils-map'
import { useMainVehicleStore } from '@/stores/mainVehicle'
Expand Down Expand Up @@ -383,7 +384,15 @@ const onMenuOptionSelect = (option: string): void => {
const latitude = clickedLocation.value[0]
const longitude = clickedLocation.value[1]

vehicleStore.goTo(hold, acceptanceRadius, passRadius, yaw, latitude, longitude, altitude)
slideToConfirm(
() => {
vehicleStore.goTo(hold, acceptanceRadius, passRadius, yaw, latitude, longitude, altitude)
},
{
command: 'GoTo',
},
canByPassCategory(EventCategory.GOTO)
)
}
break

Expand Down
22 changes: 19 additions & 3 deletions src/libs/joystick/protocols/cockpit-actions.ts
Expand Up @@ -3,6 +3,7 @@
/* eslint-disable max-len */
import { v4 as uuid4 } from 'uuid'

import { slideToConfirm } from '@/libs/slide-to-confirm'
import { type JoystickProtocolActionsMapping,type JoystickState, type ProtocolAction, JoystickProtocol } from '@/types/joystick'

/**
Expand All @@ -17,6 +18,7 @@ export enum CockpitActionsFunction {
toggle_bottom_bar = 'toggle_bottom_bar',
start_recording_all_streams = 'start_recording_all_streams',
stop_recording_all_streams = 'stop_recording_all_streams',
hold_to_confirm = 'hold_to_confirm',
}

/**
Expand Down Expand Up @@ -44,6 +46,7 @@ export const availableCockpitActions: { [key in CockpitActionsFunction]: Cockpit
[CockpitActionsFunction.toggle_bottom_bar]: new CockpitAction(CockpitActionsFunction.toggle_bottom_bar, 'Toggle bottom bar'),
[CockpitActionsFunction.start_recording_all_streams]: new CockpitAction(CockpitActionsFunction.start_recording_all_streams, 'Start recording all streams'),
[CockpitActionsFunction.stop_recording_all_streams]: new CockpitAction(CockpitActionsFunction.stop_recording_all_streams, 'Stop recording all streams'),
[CockpitActionsFunction.hold_to_confirm]: new CockpitAction(CockpitActionsFunction.hold_to_confirm, 'Hold to confirm'),
}

export type CockpitActionCallback = () => void
Expand Down Expand Up @@ -81,11 +84,18 @@ export class CockpitActionsManager {
joystickState: JoystickState
currentActionsMapping: JoystickProtocolActionsMapping
activeButtonsActions: ProtocolAction[]
actionsJoystickConfirmRequired: Record<string, boolean>

updateControllerData = (state: JoystickState, protocolActionsMapping: JoystickProtocolActionsMapping, activeButtonsActions: ProtocolAction[]): void => {
updateControllerData = (
state: JoystickState,
protocolActionsMapping: JoystickProtocolActionsMapping,
activeButtonsActions: ProtocolAction[],
actionsJoystickConfirmRequired: Record<string, boolean>
): void => {
this.joystickState = state
this.currentActionsMapping = protocolActionsMapping
this.activeButtonsActions = activeButtonsActions
this.actionsJoystickConfirmRequired = actionsJoystickConfirmRequired
}

sendCockpitActions = (): void => {
Expand All @@ -94,8 +104,14 @@ export class CockpitActionsManager {
const actionsToCallback = this.activeButtonsActions.filter((a) => a.protocol === JoystickProtocol.CockpitAction)
Object.values(actionsCallbacks).forEach((entry) => {
if (actionsToCallback.map((a) => a.id).includes(entry.action.id)) {
console.log(entry.action.name)
entry.callback()
console.log('Sending action', entry.action.name, '/ Require confirm:', this.actionsJoystickConfirmRequired[entry.action.id])
slideToConfirm(
entry.callback,
{
command: entry.action.name,
},
!this.actionsJoystickConfirmRequired[entry.action.id]
)
}
})
}
Expand Down