diff --git a/src/stores/controller.ts b/src/stores/controller.ts index a198d41ec..4256110ef 100644 --- a/src/stores/controller.ts +++ b/src/stores/controller.ts @@ -5,10 +5,12 @@ import Swal from 'sweetalert2' import { ref } from 'vue' import { availableGamepadToCockpitMaps, cockpitStandardToProtocols } from '@/assets/joystick-profiles' +import { getKeyDataFromCockpitVehicleStorage, setKeyDataOnCockpitVehicleStorage } from '@/libs/blueos' import { type JoystickEvent, EventType, joystickManager, JoystickModel } from '@/libs/joystick/manager' import { allAvailableAxes, allAvailableButtons } from '@/libs/joystick/protocols' import { modifierKeyActions, otherAvailableActions } from '@/libs/joystick/protocols/other' import { + type GamepadToCockpitStdMapping, type JoystickProtocolActionsMapping, type JoystickState, type ProtocolAction, @@ -24,11 +26,14 @@ export type controllerUpdateCallback = ( activeButtonActions: ProtocolAction[] ) => void +const protocolMappingKey = 'cockpit-protocol-mapping-v4' +const cockpitStdMappingsKey = 'cockpit-standard-mappings' + export const useControllerStore = defineStore('controller', () => { const joysticks = ref>(new Map()) const updateCallbacks = ref([]) - const protocolMapping = useStorage('cockpit-protocol-mapping-v4', cockpitStandardToProtocols) - const cockpitStdMappings = useStorage('cockpit-standard-mappings', availableGamepadToCockpitMaps) + const protocolMapping = useStorage(protocolMappingKey, cockpitStandardToProtocols) + const cockpitStdMappings = useStorage(cockpitStdMappingsKey, availableGamepadToCockpitMaps) const availableAxesActions = allAvailableAxes const availableButtonActions = allAvailableButtons const enableForwarding = ref(true) @@ -128,7 +133,7 @@ export const useControllerStore = defineStore('controller', () => { }) protocolMapping.value.buttonsCorrespondencies[v.modKey][v.button].action = otherAvailableActions.no_function }) - }, 1000) + }, 500) // If there's a mapping in our database that is not on the user storage, add it to the user // This will happen whenever a new joystick profile is added to Cockpit's database @@ -137,19 +142,19 @@ export const useControllerStore = defineStore('controller', () => { cockpitStdMappings.value[k as JoystickModel] = v }) - const downloadJoystickProfile = (joystick: Joystick): void => { + const exportJoystickMapping = (joystick: Joystick): void => { const blob = new Blob([JSON.stringify(joystick.gamepadToCockpitMap)], { type: 'text/plain;charset=utf-8' }) saveAs(blob, `cockpit-std-profile-joystick-${joystick.model}.json`) } - const loadJoystickProfile = async (joystick: Joystick, e: Event): Promise => { + const importJoystickMapping = async (joystick: Joystick, e: Event): Promise => { const reader = new FileReader() reader.onload = (event: Event) => { // @ts-ignore: We know the event type and need refactor of the event typing const contents = event.target.result const maybeProfile = JSON.parse(contents) if (!maybeProfile['name'] || !maybeProfile['axes'] || !maybeProfile['buttons']) { - Swal.fire({ icon: 'error', text: 'Invalid profile file.', timer: 3000 }) + Swal.fire({ icon: 'error', text: 'Invalid joystick mapping file.', timer: 3000 }) return } cockpitStdMappings.value[joystick.model] = maybeProfile @@ -158,6 +163,85 @@ export const useControllerStore = defineStore('controller', () => { reader.readAsText(e.target.files[0]) } + const exportJoysticksMappingsToVehicle = async ( + vehicleAddress: string, + joystickMappings: { [key in JoystickModel]: GamepadToCockpitStdMapping } + ): Promise => { + await setKeyDataOnCockpitVehicleStorage(vehicleAddress, cockpitStdMappingsKey, joystickMappings) + Swal.fire({ icon: 'success', text: 'Joystick mapping exported to vehicle.', timer: 3000 }) + } + + const importJoysticksMappingsFromVehicle = async (vehicleAddress: string): Promise => { + const newMapping = await getKeyDataFromCockpitVehicleStorage(vehicleAddress, cockpitStdMappingsKey) + if (!newMapping) { + Swal.fire({ icon: 'error', text: 'No joystick mappings to import from vehicle.', timer: 3000 }) + return + } + try { + Object.values(newMapping).forEach((mapping) => { + if (!mapping['name'] || !mapping['axes'] || !mapping['buttons']) { + throw Error('Invalid joystick mapping inside vehicle.') + } + }) + } catch (error) { + Swal.fire({ icon: 'error', text: `Could not import joystick mapping from vehicle. ${error}`, timer: 3000 }) + return + } + + // @ts-ignore: We check for the necessary fields in the if before + cockpitStdMappings.value = newMapping + Swal.fire({ icon: 'success', text: 'Joystick mapping imported from vehicle.', timer: 3000 }) + } + + const exportFunctionsMapping = (protocolActionsMapping: JoystickProtocolActionsMapping): void => { + const blob = new Blob([JSON.stringify(protocolActionsMapping)], { type: 'text/plain;charset=utf-8' }) + saveAs(blob, `cockpit-std-profile-joystick-${protocolActionsMapping.name}.json`) + } + + const importFunctionsMapping = async (e: Event): Promise => { + const reader = new FileReader() + reader.onload = (event: Event) => { + // @ts-ignore: We know the event type and need refactor of the event typing + const contents = event.target.result + const maybeFunctionsMapping = JSON.parse(contents) + if ( + !maybeFunctionsMapping['name'] || + !maybeFunctionsMapping['axesCorrespondencies'] || + !maybeFunctionsMapping['buttonsCorrespondencies'] + ) { + Swal.fire({ icon: 'error', text: 'Invalid functions mapping file.', timer: 3000 }) + return + } + protocolMapping.value = maybeFunctionsMapping + } + // @ts-ignore: We know the event type and need refactor of the event typing + reader.readAsText(e.target.files[0]) + } + + const exportFunctionsMappingToVehicle = async ( + vehicleAddress: string, + functionsMapping: JoystickProtocolActionsMapping + ): Promise => { + await setKeyDataOnCockpitVehicleStorage(vehicleAddress, protocolMappingKey, functionsMapping) + Swal.fire({ icon: 'success', text: 'Joystick functions mapping exported to vehicle.', timer: 3000 }) + } + + const importFunctionsMappingFromVehicle = async (vehicleAddress: string): Promise => { + const newMapping = await getKeyDataFromCockpitVehicleStorage(vehicleAddress, protocolMappingKey) + if ( + !newMapping || + !newMapping['name'] || + !newMapping['axesCorrespondencies'] || + !newMapping['buttonsCorrespondencies'] + ) { + Swal.fire({ icon: 'error', text: 'Could not import functions mapping from vehicle. Invalid data.', timer: 3000 }) + return + } + // @ts-ignore: We check for the necessary fields in the if before + protocolMapping.value = newMapping + Swal.fire({ icon: 'success', text: 'Joystick functions mapping imported from vehicle.', timer: 3000 }) + } + return { registerControllerUpdateCallback, enableForwarding, @@ -166,7 +250,13 @@ export const useControllerStore = defineStore('controller', () => { cockpitStdMappings, availableAxesActions, availableButtonActions, - downloadJoystickProfile, - loadJoystickProfile, + exportJoystickMapping, + importJoystickMapping, + exportJoysticksMappingsToVehicle, + importJoysticksMappingsFromVehicle, + exportFunctionsMapping, + importFunctionsMapping, + exportFunctionsMappingToVehicle, + importFunctionsMappingFromVehicle, } }) diff --git a/src/stores/widgetManager.ts b/src/stores/widgetManager.ts index 2b41b9d47..a9fa0ba9f 100644 --- a/src/stores/widgetManager.ts +++ b/src/stores/widgetManager.ts @@ -23,13 +23,15 @@ import { type Profile, type View, type Widget, isProfile, isView, WidgetType } f import { useMainVehicleStore } from './mainVehicle' +const savedProfilesKey = 'cockpit-saved-profiles-v8' + export const useWidgetManagerStore = defineStore('widget-manager', () => { const vehicleStore = useMainVehicleStore() const editingMode = ref(false) const showGrid = ref(true) const gridInterval = ref(0.01) const currentMiniWidgetsProfile = useStorage('cockpit-mini-widgets-profile-v4', miniWidgetsProfile) - const savedProfiles = useStorage('cockpit-saved-profiles-v8', []) + const savedProfiles = useStorage(savedProfilesKey, []) const currentViewIndex = useStorage('cockpit-current-view-index', 0) const currentProfileIndex = useStorage('cockpit-current-profile-index', 0) @@ -164,10 +166,7 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => { } const importProfilesFromVehicle = async (): Promise => { - const newProfiles = await getKeyDataFromCockpitVehicleStorage( - vehicleStore.globalAddress, - 'cockpit-saved-profiles-v7' - ) + const newProfiles = await getKeyDataFromCockpitVehicleStorage(vehicleStore.globalAddress, savedProfilesKey) if (!Array.isArray(newProfiles) || !newProfiles.every((profile) => isProfile(profile))) { Swal.fire({ icon: 'error', text: 'Could not import profiles from vehicle. Invalid data.', timer: 3000 }) return @@ -177,11 +176,7 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => { } const exportProfilesToVehicle = async (): Promise => { - await setKeyDataOnCockpitVehicleStorage( - vehicleStore.globalAddress, - 'cockpit-saved-profiles-v7', - savedProfiles.value - ) + await setKeyDataOnCockpitVehicleStorage(vehicleStore.globalAddress, savedProfilesKey, savedProfiles.value) Swal.fire({ icon: 'success', text: 'Cockpit profiles exported to vehicle.', timer: 3000 }) } diff --git a/src/views/ConfigurationJoystickView.vue b/src/views/ConfigurationJoystickView.vue index e687257ed..caafd5b62 100644 --- a/src/views/ConfigurationJoystickView.vue +++ b/src/views/ConfigurationJoystickView.vue @@ -86,22 +86,73 @@ @click="(e) => setCurrentInputs(joystick, e)" /> -
- - +
+
+ Joystick mapping +
+ + + + +
+
+
+ Functions mapping +
+ + + + +
+
@@ -139,7 +190,7 @@ {{ protocol }}