diff --git a/src/components/EditMenu.vue b/src/components/EditMenu.vue index 2b6025c24..5c6c36b6c 100644 --- a/src/components/EditMenu.vue +++ b/src/components/EditMenu.vue @@ -485,19 +485,19 @@ class="flex items-center justify-between w-full h-full gap-3 overflow-x-auto text-white -mb-1 pr-2 cursor-pointer" >
@@ -659,6 +659,7 @@ import URLVideoPlayerImg from '@/assets/widgets/URLVideoPlayer.png' import VideoPlayerImg from '@/assets/widgets/VideoPlayer.png' import VirtualHorizonImg from '@/assets/widgets/VirtualHorizon.png' import { useInteractionDialog } from '@/composables/interactionDialog' +import { getWidgetsFromBlueOS } from '@/libs/blueos' import { MavType } from '@/libs/connection/m2r/messages/mavlink2rest-enum' import { isHorizontalScroll } from '@/libs/utils' import { useAppInterfaceStore } from '@/stores/appInterface' @@ -668,6 +669,8 @@ import { type View, type Widget, CustomWidgetElementType, + BlueOsWidget, + ExtendedWidget, MiniWidgetType, WidgetType, } from '@/types/widgets' @@ -695,6 +698,8 @@ const toggleDial = (): void => { const forceUpdate = ref(0) +const blueosWidgets = ref([]) + watch( () => store.currentView.widgets, () => { @@ -714,7 +719,29 @@ const emit = defineEmits<{ (e: 'update:editMode', editMode: boolean): void }>() -const availableWidgetTypes = computed(() => Object.values(WidgetType)) +const availableWidgetTypes = computed(() => + Object.values(WidgetType).map((widgetType) => { + return { + component: widgetType, + name: widgetType, + options: {}, + } + }) +) + +const allAvailableWidgets = computed(() => { + return [ + ...blueosWidgets.value.map((widget) => ({ + component: WidgetType.IFrame, + name: widget.name, + options: { + source: widget.url, + }, + })), + ...availableWidgetTypes.value, + ] +}) + const availableMiniWidgetTypes = computed(() => Object.values(MiniWidgetType).map((widgetType) => ({ component: widgetType, @@ -927,6 +954,11 @@ const miniWidgetsContainerOptions = ref({ }) useDraggable(availableMiniWidgetsContainer, availableMiniWidgetTypes, miniWidgetsContainerOptions) +const getBlueosWidgets = async (): Promise => { + blueosWidgets.value = await getWidgetsFromBlueOS() +} + + // @ts-ignore: Documentation is not clear on what generic should be passed to 'UseDraggableOptions' const customWidgetElementContainerOptions = ref({ animation: '150', @@ -940,6 +972,7 @@ useDraggable( ) onMounted(() => { + getBlueosWidgets() const widgetContainers = [ availableWidgetsContainer.value, availableMiniWidgetsContainer.value, @@ -991,7 +1024,7 @@ const onRegularWidgetDragStart = (event: DragEvent): void => { } } -const onRegularWidgetDragEnd = (widgetType: WidgetType): void => { +const onRegularWidgetDragEnd = (widgetType: ExtendedWidget): void => { store.addWidget(widgetType, store.currentView) const widgetCards = document.querySelectorAll('[draggable="true"]') diff --git a/src/libs/blueos.ts b/src/libs/blueos.ts index 0eaba093a..95c7105a9 100644 --- a/src/libs/blueos.ts +++ b/src/libs/blueos.ts @@ -1,5 +1,8 @@ import ky, { HTTPError } from 'ky' +import { useMainVehicleStore } from '@/stores/mainVehicle' +import { BlueOsWidget } from '@/types/widgets' + export const NoPathInBlueOsErrorName = 'NoPathInBlueOS' const defaultTimeout = 10000 @@ -30,6 +33,38 @@ export const getKeyDataFromCockpitVehicleStorage = async ( return await getBagOfHoldingFromVehicle(vehicleAddress, `cockpit/${storageKey}`) } +export const getWidgetsFromBlueOS = async (): Promise => { + const vehicleStore = useMainVehicleStore() + + // Wait until we have a global address + while (vehicleStore.globalAddress === undefined) { + console.debug('Waiting for vehicle global address on BlueOS sync routine.') + await new Promise((r) => setTimeout(r, 1000)) + } + try { + const options = { timeout: defaultTimeout, retry: 0 } + const data = (await ky + .get(`http://${vehicleStore.globalAddress}/helper/v1.0/web_services`, options) + .json()) as Record + // include the vehicle base address into the relative urls provided with the widgets + return Object.keys(data).reduce((widgets: BlueOsWidget[], key) => { + const value = data[key] + if (typeof value === 'object' && value.metadata?.cockpit_widget) { + const newWidgets: BlueOsWidget[] = value.metadata.cockpit_widget.map((widget: BlueOsWidget) => ({ + ...widget, + url: `http://${vehicleStore.globalAddress}:${value.port}${widget.url}`, + })) + return [...widgets, ...newWidgets] + } + return widgets + }, []) + } catch (error) { + const errorBody = await (error as HTTPError).response.json() + console.error(errorBody) + } + return [] +} + export const setBagOfHoldingOnVehicle = async ( vehicleAddress: string, bagName: string, diff --git a/src/stores/widgetManager.ts b/src/stores/widgetManager.ts index 4d83a07e9..811d2a0bc 100644 --- a/src/stores/widgetManager.ts +++ b/src/stores/widgetManager.ts @@ -36,6 +36,7 @@ import { type Widget, CustomWidgetElement, CustomWidgetElementContainer, + ExtendedWidget, MiniWidgetManagerVars, validateProfile, validateView, @@ -558,16 +559,16 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => { * @param { WidgetType } widgetType - Type of the widget * @param { View } view - View */ - function addWidget(widgetType: WidgetType, view: View): void { + function addWidget(widgetType: ExtendedWidget, view: View): void { const widgetHash = uuid4() const widget = { hash: widgetHash, - name: widgetType, - component: widgetType, + name: widgetType.name, + component: widgetType.component, position: { x: 0.4, y: 0.32 }, size: { width: 0.2, height: 0.36 }, - options: {}, + options: widgetType.options, } if (widgetType === WidgetType.CustomWidgetBase) { diff --git a/src/types/widgets.ts b/src/types/widgets.ts index 0924bd394..b95aab39d 100644 --- a/src/types/widgets.ts +++ b/src/types/widgets.ts @@ -2,6 +2,39 @@ import { CockpitAction } from '@/libs/joystick/protocols/cockpit-actions' import type { Point2D, SizeRect2D } from './general' +/** + * Widget configuration object as received from BlueOS + */ +export interface BlueOsWidget { + /** + * Name of the widget, this is displayed on edit mode widget browser + */ + name: string + /** + * The URL at which the widget is located + * This is expected to be an absolute url + */ + url: string +} + +/** + * Widget type on steroids. This is the WidgetType with added Custom name and options + */ +export interface ExtendedWidget { + /** + * + */ + component: WidgetType + /** + * + */ + name: string + /** + * + */ + options: Record +} + /** * Available components to be used in the Widget system * The enum value is equal to the component's filename, without the '.vue' extension