diff --git a/src/components/EditMenu.vue b/src/components/EditMenu.vue index 90ecd3eba..6771b4907 100644 --- a/src/components/EditMenu.vue +++ b/src/components/EditMenu.vue @@ -485,27 +485,22 @@ class="flex items-center justify-between w-full h-full gap-3 overflow-x-auto text-white -mb-1 pr-2 cursor-pointer" >
@@ -648,6 +643,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' @@ -657,6 +653,8 @@ import { type View, type Widget, CustomWidgetElementType, + ExternalWidgetSetupInfo, + InternalWidgetSetupInfo, MiniWidgetType, WidgetType, } from '@/types/widgets' @@ -685,6 +683,8 @@ const toggleDial = (): void => { const forceUpdate = ref(0) +const ExternalWidgetSetupInfos = ref([]) + watch( () => store.currentView.widgets, () => { @@ -712,7 +712,31 @@ watch( } ) -const availableWidgetTypes = computed(() => Object.values(WidgetType)) +const availableWidgetTypes = computed(() => + Object.values(WidgetType).map((widgetType) => { + return { + component: widgetType, + name: widgetType, + icon: widgetImages[widgetType] as string, + options: {}, + } + }) +) + +const allAvailableWidgets = computed(() => { + return [ + ...ExternalWidgetSetupInfos.value.map((widget) => ({ + component: WidgetType.IFrame, + icon: widget.iframe_icon, + name: widget.name, + options: { + source: widget.iframe_url, + }, + })), + ...availableInternalWidgets.value, + ] +}) + const availableMiniWidgetTypes = computed(() => Object.values(MiniWidgetType).map((widgetType) => ({ component: widgetType, @@ -925,6 +949,10 @@ const miniWidgetsContainerOptions = ref({ }) useDraggable(availableMiniWidgetsContainer, availableMiniWidgetTypes, miniWidgetsContainerOptions) +const getExternalWidgetSetupInfos = async (): Promise => { + ExternalWidgetSetupInfos.value = await getWidgetsFromBlueOS() +} + // @ts-ignore: Documentation is not clear on what generic should be passed to 'UseDraggableOptions' const customWidgetElementContainerOptions = ref({ animation: '150', @@ -938,6 +966,7 @@ useDraggable( ) onMounted(() => { + getExternalWidgetSetupInfos() const widgetContainers = [ availableWidgetsContainer.value, availableMiniWidgetsContainer.value, @@ -989,7 +1018,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..3a71680f9 100644 --- a/src/libs/blueos.ts +++ b/src/libs/blueos.ts @@ -1,5 +1,26 @@ import ky, { HTTPError } from 'ky' +import { useMainVehicleStore } from '@/stores/mainVehicle' +import { ExternalWidgetSetupInfo } from '@/types/widgets' + +/** + * Cockpits extra json format. Taken from extensions in BlueOS and (eventually) other places + */ +interface ExtrasJson { + /** + * The version of the cockpit API that the extra json is compatible with + */ + target_cockpit_api_version: string + /** + * The target system that the extra json is compatible with, in our case, "cockpit" + */ + target_system: string + /** + * A list of widgets that the extra json contains. src/types/widgets.ts + */ + widgets: ExternalWidgetSetupInfo[] +} + export const NoPathInBlueOsErrorName = 'NoPathInBlueOS' const defaultTimeout = 10000 @@ -30,6 +51,46 @@ 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) { + await new Promise((r) => setTimeout(r, 1000)) + } + const options = { timeout: defaultTimeout, retry: 0 } + const services = (await ky + .get(`http://${vehicleStore.globalAddress}/helper/v1.0/web_services`, options) + .json()) as Record + // first we gather all the extra jsons with the cockpit key + const extraWidgets = await services.reduce( + async (accPromise: Promise, service: Record) => { + const acc = await accPromise + const worksInRelativePaths = service.metadata?.works_in_relative_paths + if (service.metadata?.extras?.cockpit === undefined) { + return acc + } + const baseUrl = worksInRelativePaths + ? `http://${vehicleStore.globalAddress}/extensionv2/${service.metadata.sanitized_name}` + : `http://${vehicleStore.globalAddress}:${service.port}` + const fullUrl = baseUrl + service.metadata?.extras?.cockpit + + const extraJson: ExtrasJson = await ky.get(fullUrl, options).json() + const widgets: ExternalWidgetSetupInfo[] = extraJson.widgets.map((widget) => { + return { + ...widget, + iframe_url: baseUrl + widget.iframe_url, + iframe_icon: baseUrl + widget.iframe_icon, + } + }) + return acc.concat(widgets) + }, + Promise.resolve([] as ExternalWidgetSetupInfo[]) + ) + + return extraWidgets +} + export const setBagOfHoldingOnVehicle = async ( vehicleAddress: string, bagName: string, diff --git a/src/stores/widgetManager.ts b/src/stores/widgetManager.ts index 551de6da2..8afc40017 100644 --- a/src/stores/widgetManager.ts +++ b/src/stores/widgetManager.ts @@ -36,6 +36,7 @@ import { type Widget, CustomWidgetElement, CustomWidgetElementContainer, + InternalWidgetSetupInfo, MiniWidgetManagerVars, validateProfile, validateView, @@ -555,22 +556,22 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => { /** * Add widget with given type to given view - * @param { WidgetType } widgetType - Type of the widget + * @param { WidgetType } widget - Type of the widget * @param { View } view - View */ - function addWidget(widgetType: WidgetType, view: View): void { + function addWidget(widget: InternalWidgetSetupInfo, view: View): void { const widgetHash = uuid4() - const widget = { + const newWidget = { hash: widgetHash, - name: widgetType, - component: widgetType, + name: widget.name, + component: widget.component, position: { x: 0.4, y: 0.32 }, size: { width: 0.2, height: 0.36 }, - options: {}, + options: widget.options, } - if (widgetType === WidgetType.CustomWidgetBase) { + if (widget.component === WidgetType.CustomWidgetBase) { widget.options = { elementContainers: defaultCustomWidgetContainers, columns: 1, @@ -581,8 +582,8 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => { } } - view.widgets.unshift(widget) - Object.assign(widgetManagerVars(widget.hash), { + view.widgets.unshift(newWidget) + Object.assign(widgetManagerVars(newWidget.hash), { ...defaultWidgetManagerVars, ...{ allowMoving: true }, }) diff --git a/src/types/widgets.ts b/src/types/widgets.ts index 820cfaeb8..967051b8d 100644 --- a/src/types/widgets.ts +++ b/src/types/widgets.ts @@ -2,6 +2,47 @@ import { CockpitAction } from '@/libs/joystick/protocols/cockpit-actions' import type { Point2D, SizeRect2D } from './general' +/** + * Widget configuration object as received from BlueOS or another external source + */ +export interface ExternalWidgetSetupInfo { + /** + * 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 + */ + iframe_url: string + + /** + * The icon of the widget, this is displayed on the widget browser + */ + iframe_icon: string +} + +/** + * Internal data used for setting up a new widget. This includes WidgetType, a custom name, options, and icon + */ export interface InternalWidgetSetupInfo { + /** + * Widget type + */ + component: WidgetType + /** + * Widget name, this will be displayed on edit mode + */ + name: string + /** + * Widget options, this is the configuration that will be passed to the widget when it is created + */ + options: Record + /** + * Widget icon, this is the icon that will be displayed on the widget browser + */ + icon: string +} + /** * Available components to be used in the Widget system * The enum value is equal to the component's filename, without the '.vue' extension