diff --git a/caster-back/operations.gql b/caster-back/operations.gql index b18a4a73..749a352d 100644 --- a/caster-back/operations.gql +++ b/caster-back/operations.gql @@ -222,14 +222,16 @@ subscription stream($graphUuid: UUID!) { checked label key + callbackActions } } buttons { __typename buttonType - sendVariablesOnClick text - sendVariableOnClick + key + callbackActions + value } title } diff --git a/caster-back/schema.gql b/caster-back/schema.gql index 3e0c068e..6089dcf8 100644 --- a/caster-back/schema.gql +++ b/caster-back/schema.gql @@ -124,9 +124,10 @@ union AudioFileUploadResponse = AudioFile | InvalidAudioFile """A button which can also trigger a set of functionality.""" type Button { text: String! + value: String! + key: String! buttonType: ButtonType! - sendVariablesOnClick: Boolean! - sendVariableOnClick: String + callbackActions: [CallbackAction!]! } """An enumeration.""" @@ -139,6 +140,23 @@ enum ButtonType { DANGER } +""" +Allows to add a pre-defined JavaScript callback to a button or a checkbox. + +ACTIVATE_GPS_STREAMING Activates streaming of GPS coordinates + as :class:`~stream.models.StreamVariable` +SEND_VARIABLES Send all variables of the form / dialog to + the server. +SEND_VARIABLE Sends a single :class:`~stream.models.StreamVariable` + with the key/value of the where the callback is + attached to. +""" +enum CallbackAction { + ACTIVATE_GPS_STREAMING + SEND_VARIABLES + SEND_VARIABLE +} + """Choice of foobar""" enum CellType { MARKDOWN @@ -156,6 +174,7 @@ type Checkbox { key: String! label: String! checked: Boolean! + callbackActions: [CallbackAction!]! } union Content = Text | Input | Checkbox diff --git a/caster-back/stream/frontend_types.py b/caster-back/stream/frontend_types.py index 31782d9c..d742f546 100644 --- a/caster-back/stream/frontend_types.py +++ b/caster-back/stream/frontend_types.py @@ -8,8 +8,9 @@ The stream subscription makes it possible to yield the """ +from dataclasses import field from enum import Enum -from typing import List, Optional +from typing import List import strawberry import strawberry.django @@ -17,6 +18,9 @@ @strawberry.enum class ButtonType(Enum): + """Derived from ElementPlus framework, see + `https://element-plus.org/en-US/component/button.html`_.""" + DEFAULT = "default" PRIMARY = "primary" SUCCESS = "success" @@ -25,6 +29,27 @@ class ButtonType(Enum): DANGER = "danger" +@strawberry.enum +class CallbackAction(Enum): + """Allows to add a pre-defined JavaScript callback to a button or a checkbox. + + ACTIVATE_GPS_STREAMING Activates streaming of GPS coordinates + as :class:`~stream.models.StreamVariable`. + If the GPS request succeeds the dialog will be closed, + if not it the user will be forwarded to an error page + which describes the setup procedure for the OS. + SEND_VARIABLES Send all variables of the form / dialog to + the server. + SEND_VARIABLE Sends a single :class:`~stream.models.StreamVariable` + with the key/value of the where the callback is + attached to. + """ + + ACTIVATE_GPS_STREAMING = "activate_gps_streaming" + SEND_VARIABLES = "send_variables" + SEND_VARIABLE = "send_variable" + + @strawberry.type class Text: """Displays plain text.""" @@ -41,6 +66,17 @@ class Checkbox: key: str label: str checked: bool = False + callback_actions: List[CallbackAction] = field(default_factory=lambda: []) + + @classmethod + def gps( + cls, label: str = "Is it OK to access your GPS?", key: str = "gps", **kwargs + ): + return cls( + label=label, + key=key, + callback_actions=[CallbackAction.ACTIVATE_GPS_STREAMING], + ) @strawberry.type @@ -49,7 +85,7 @@ class Input: under the ``key`` as a :class:`~stream.models.StreamVariable`.""" key: str - label: str = "Info" + label: str = "input" placeholder: str = "Please input" @@ -58,27 +94,31 @@ class Button: """A button which can also trigger a set of functionality.""" text: str + value: str + key: str = "button" button_type: ButtonType = ButtonType.DEFAULT - - send_variables_on_click: bool = False - # will be used as key and its value will be set to true - send_variable_on_click: Optional[str] = None + callback_actions: List[CallbackAction] = field( + default_factory=lambda: [CallbackAction.SEND_VARIABLE] + ) @classmethod def ok( cls, text: str = "OK", - send_variables_on_click: bool = True, + value="ok", button_type=ButtonType.PRIMARY, - send_variable_on_click: str = "OK", + callback_actions: List[CallbackAction] = [ + CallbackAction.SEND_VARIABLE, + CallbackAction.SEND_VARIABLES, + ], **kwargs ): """Constructor for a OK button which will""" return cls( text=text, - send_variables_on_click=send_variables_on_click, + value=value, button_type=button_type, - send_variable_on_click=send_variable_on_click, + callback_actions=callback_actions, **kwargs, ) @@ -86,8 +126,11 @@ def ok( def cancel( cls, text: str = "Cancel", + value="cancel", button_type=ButtonType.WARNING, - send_variable_on_click: str = "CANCEL", + callback_actions: List[CallbackAction] = [ + CallbackAction.SEND_VARIABLE, + ], **kwargs ): """Constructor for a cancel button which will simply close @@ -96,8 +139,9 @@ def cancel( """ return cls( text=text, + value=value, + callback_actions=callback_actions, button_type=button_type, - send_variable_on_click=send_variable_on_click, **kwargs, ) @@ -111,5 +155,13 @@ class Dialog: """Triggers a popup on the frontend of the listener.""" title: str - content: List[Content] # type: ignore + content: List[Content] = strawberry.field() # type: ignore buttons: List[Button] + + @classmethod + def gps(cls, title: str = "GPS"): + return cls( + title=title, + content=[Checkbox.gps()], + buttons=[], + ) diff --git a/caster-editor/src/graphql.ts b/caster-editor/src/graphql.ts index bf4ba40d..aa623d30 100644 --- a/caster-editor/src/graphql.ts +++ b/caster-editor/src/graphql.ts @@ -135,9 +135,10 @@ export type AudioFileUploadResponse = AudioFile | InvalidAudioFile; /** A button which can also trigger a set of functionality. */ export type Button = { buttonType: ButtonType; - sendVariableOnClick?: Maybe; - sendVariablesOnClick: Scalars["Boolean"]; + callbackActions: Array; + key: Scalars["String"]; text: Scalars["String"]; + value: Scalars["String"]; }; /** An enumeration. */ @@ -150,6 +151,23 @@ export enum ButtonType { Warning = "WARNING", } +/** + * Allows to add a pre-defined JavaScript callback to a button or a checkbox. + * + * ACTIVATE_GPS_STREAMING Activates streaming of GPS coordinates + * as :class:`~stream.models.StreamVariable` + * SEND_VARIABLES Send all variables of the form / dialog to + * the server. + * SEND_VARIABLE Sends a single :class:`~stream.models.StreamVariable` + * with the key/value of the where the callback is + * attached to. + */ +export enum CallbackAction { + ActivateGpsStreaming = "ACTIVATE_GPS_STREAMING", + SendVariable = "SEND_VARIABLE", + SendVariables = "SEND_VARIABLES", +} + /** Choice of foobar */ export enum CellType { Audio = "AUDIO", @@ -164,6 +182,7 @@ export enum CellType { * saved **as a string** under ``key`` in a :class:`~stream.models.StreamVariable`. */ export type Checkbox = { + callbackActions: Array; checked: Scalars["Boolean"]; key: Scalars["String"]; label: Scalars["String"]; @@ -982,6 +1001,7 @@ export type StreamSubscription = { checked: boolean; label: string; key: string; + callbackActions: Array; } | { __typename: "Input"; @@ -994,9 +1014,10 @@ export type StreamSubscription = { buttons: Array<{ __typename: "Button"; buttonType: ButtonType; - sendVariablesOnClick: boolean; text: string; - sendVariableOnClick?: string | null; + key: string; + callbackActions: Array; + value: string; }>; } | { __typename: "NoStreamAvailable"; error: string } @@ -1517,14 +1538,16 @@ export const StreamDocument = gql` checked label key + callbackActions } } buttons { __typename buttonType - sendVariablesOnClick text - sendVariableOnClick + key + callbackActions + value } title } diff --git a/caster-front/src/components/GraphPlayer.vue b/caster-front/src/components/GraphPlayer.vue index 4a426b56..534f0117 100644 --- a/caster-front/src/components/GraphPlayer.vue +++ b/caster-front/src/components/GraphPlayer.vue @@ -13,11 +13,21 @@ import { } from "element-plus"; import { useRouter } from "vue-router"; import Player from "@/components/PlayerComponent.vue"; -import { type Graph, type StreamSubscription, ButtonType } from "@/graphql"; +import { + type Graph, + type StreamSubscription, + ButtonType, + type Button, + CallbackAction, + type StreamVariableInput, + type Checkbox, +} from "@/graphql"; import { useStreamSubscription } from "@/graphql"; import StreamInfo from "@/components/StreamInfo.vue"; import PlayerButtons from "@/components/PlayerButtons.vue"; import { useSendStreamVariableMutation } from "@/graphql"; +import { storeToRefs } from "pinia"; +import { usePlayerStore } from "@/stores/Player"; interface DialogShow { show: boolean; @@ -29,6 +39,8 @@ const props = defineProps<{ graph: Pick; }>(); +const { streamGPS, gpsError, gpsSuccess } = storeToRefs(usePlayerStore()); + const router = useRouter(); const sendStreamVariable = useSendStreamVariableMutation(); @@ -111,10 +123,64 @@ let formData: Record = reactive>({}); const playerRef: Ref | undefined> = ref(undefined); -const processButton = async ( - sendVariablesOnClick: boolean, - sendVariableOnClick: string | undefined | null = undefined, -) => { +const closeCurrentDialog = () => { + formData = reactive>({}); + if (currentDialog.value) { + currentDialog.value.loading = false; + currentDialog.value.show = false; + } +}; + +// gps stuff +watch(gpsSuccess, () => { + console.log("Received first GPS signal - connection successful"); + closeCurrentDialog(); +}); + +watch(gpsError, () => { + if (gpsError.value) { + console.log( + `Error at obtaining GPS handle: ${gpsError.value}`, + gpsError.value, + ); + if (gpsError.value.PERMISSION_DENIED) ElMessage.error("Plesae allow GPS."); + else if ( + gpsError.value.POSITION_UNAVAILABLE || + gpsError.value.PERMISSION_DENIED + ) + ElMessage.error( + `Could not obtain a GPS position: ${gpsError.value.message}`, + ); + router.push("/gps-error"); + } +}); + +const setupGPS = async () => { + if (gpsSuccess.value) { + // gps is already running so nothing to do + closeCurrentDialog(); + } + // as it makes only sense to have one GPS stream we refer to + // it via the global store. + streamGPS.value = true; + // a watcher which checks the status of the GPS request + const refreshIntervalId = setInterval(async () => { + // i don't have a clue if this works properly b/c I always receive a GPS location first + // but "in theory" it should also help us + const { state } = await navigator.permissions.query({ + name: "geolocation", + }); + console.log("state is", state); + if (state === "granted") { + gpsSuccess.value = true; + clearInterval(refreshIntervalId); + closeCurrentDialog(); + } + }, 100); +}; + +// callback stuff +const processAction = async (button: Button | Checkbox) => { if (!streamInfo.value) { console.log( `Can not send stream variable ${sendStreamVariable} because stream info is missing`, @@ -124,38 +190,48 @@ const processButton = async ( if (currentDialog.value) { currentDialog.value.loading = true; } - if (sendVariableOnClick) { - const { error } = await sendStreamVariable.executeMutation({ - streamVariables: [ - { - streamUuid: streamInfo.value.stream.uuid, - key: sendVariableOnClick, - value: "True", - }, - ], - }); - if (error) { - ElMessage.error( - `Could not transfer button press to server: ${error.message}`, - ); + let gpsAction = false; + const updates: StreamVariableInput[] = []; + button.callbackActions.forEach((action) => { + if (action === CallbackAction.SendVariable) { + updates.push({ + streamUuid: streamInfo.value?.stream.uuid, + key: button.key, + value: "value" in button ? button.value : String(button.checked), + }); } - } - if (sendVariablesOnClick) { - const { error } = await sendStreamVariable.executeMutation({ - streamVariables: Object.keys(formData).map((key) => { - return { + + if (action === CallbackAction.SendVariables) { + Object.keys(formData).forEach((key) => { + updates.push({ streamUuid: streamInfo.value?.stream.uuid, key: key, value: formData[key], - }; - }), + }); + }); + } + + if (action === CallbackAction.ActivateGpsStreaming) { + gpsAction = true; + } + }); + + if (updates.length > 0) { + const { error } = await sendStreamVariable.executeMutation({ + streamVariables: updates, }); if (error) { ElMessage.error( - `Could not transfer input press to server: ${error.message}`, + `Could not transfer variable to server: ${error.message}`, ); } } + + if (gpsAction) { + setupGPS(); + return; + } + // reset form data formData = reactive>({}); if (currentDialog.value) { @@ -191,7 +267,6 @@ const convertButtonType = (b: ButtonType): ElButtonType => { return ElButtonType.Default; } }; -// :type="button.buttonType === 'PRIMARY' ? 'primary' : 'primary'"