diff --git a/src/components/ExternalFeaturesDiscoveryModal.vue b/src/components/ExternalFeaturesDiscoveryModal.vue index d2ed78ab03..49d9458fea 100644 --- a/src/components/ExternalFeaturesDiscoveryModal.vue +++ b/src/components/ExternalFeaturesDiscoveryModal.vue @@ -1,16 +1,21 @@ @@ -1095,11 +1176,35 @@ const ignoredJoystickSuggestionsByExtension = computed(() => { .filter((ext) => ext.suggestionGroups.length > 0) }) +/** + * Whether there are new actions pending user action + */ +const hasPendingActions = computed(() => filteredActions.value.length > 0) + +/** + * Whether there are new joystick suggestions pending user action + */ +const hasPendingJoystickSuggestions = computed(() => filteredJoystickSuggestionsByExtension.value.length > 0) + /** * Whether there are new extension features that still need user action */ const hasPendingBlueOSFeatures = computed(() => { - return filteredActions.value.length > 0 || filteredJoystickSuggestionsByExtension.value.length > 0 + return hasPendingActions.value || hasPendingJoystickSuggestions.value +}) + +/** + * Flat list of pending joystick suggestions with their extension names + */ +const pendingJoystickSuggestions = computed((): JoystickSuggestionWithExtensionName[] => { + return filteredJoystickSuggestionsByExtension.value.flatMap((ext) => + ext.suggestionGroups.flatMap((group) => + group.buttonMappingSuggestions.map((suggestion) => ({ + ...suggestion, + extensionName: ext.extensionName, + })) + ) + ) }) /** @@ -1437,6 +1542,11 @@ const restoreIgnoredSuggestion = (suggestion: JoystickMapSuggestion): void => { }) } +/** + * Controls visibility of the close confirmation dialog + */ +const closeConfirmationDialog = ref(false) + /** * Close the modal */ @@ -1445,6 +1555,25 @@ const closeModal = (): void => { emit('close') } +/** + * Request to close the modal. If there are still pending items to decide upon, ask the user to confirm first. + */ +const requestCloseModal = (): void => { + if (hasPendingBlueOSFeatures.value) { + closeConfirmationDialog.value = true + return + } + closeModal() +} + +/** + * Confirm closing the modal from the confirmation dialog + */ +const confirmCloseModal = (): void => { + closeConfirmationDialog.value = false + closeModal() +} + /** * Check for available actions from BlueOS. */ @@ -1523,6 +1652,32 @@ watch(activeTab, () => { transition: width 0.2s ease; } +.tab-blink { + position: relative; +} + +.tab-blink::before { + content: ''; + position: absolute; + inset: 0; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + background-color: #ffffff22; + animation: tab-blink 1.2s ease-in-out infinite; + pointer-events: none; + z-index: 0; +} + +@keyframes tab-blink { + 0%, + 100% { + opacity: 0; + } + 50% { + opacity: 1; + } +} + .features-modal:has(.v-expansion-panels) { width: 760px; } diff --git a/src/components/GlassModal.vue b/src/components/GlassModal.vue index 3c403b3beb..dcb332b617 100644 --- a/src/components/GlassModal.vue +++ b/src/components/GlassModal.vue @@ -41,6 +41,12 @@ const props = defineProps<{ * If true, modal will not close by pressing 'esc' or by an outside click. */ isPersistent?: boolean + /** + * If true, the modal will not emit `outside-click` when the user clicks outside of it. + * Useful when the modal hosts Vuetify dialogs that are teleported to the document body, + * whose clicks would otherwise be interpreted as outside clicks of the modal. + */ + noCloseOnOutsideClick?: boolean /** * The overflow property of the modal. */ @@ -177,7 +183,7 @@ const closeModal = (): void => { } onClickOutside(modal, () => { - if (!isPersistent.value) { + if (!isPersistent.value && !props.noCloseOnOutsideClick) { closeModal() } if (!isAlwaysOnTop.value) {