feat(stage): add global button analytics directive (#2146)

This commit is contained in:
RainbowBird
2026-07-29 15:59:53 +08:00
committed by GitHub
parent 2ae95253d9
commit 742bb80ca5
16 changed files with 415 additions and 36 deletions
+2
View File
@@ -6,6 +6,7 @@ import NProgress from 'nprogress'
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
import { isEnvTruthy } from '@proj-airi/stage-shared'
import { trackButtonPlugin } from '@proj-airi/stage-ui/directives/track-button'
import { MotionPlugin } from '@vueuse/motion'
import { createPinia } from 'pinia'
import { setupLayouts } from 'virtual:generated-layouts'
@@ -60,6 +61,7 @@ createApp(App)
.use(pinia)
.use(i18n)
.use(Tres)
.use(trackButtonPlugin)
.mount('#app')
if (import.meta.env.DEV && !import.meta.env.SSR) {
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
import { useAnalytics } from '@proj-airi/stage-ui/composables/use-analytics'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -21,18 +22,30 @@ const props = withDefaults(defineProps<Props>(), {
const uiStore = useControlsIslandStore()
const enabled = computed(() => uiStore.fadeOnHoverEnabled)
const { t } = useI18n()
const { trackControlsIslandAction } = useAnalytics()
const requestNotice = useElectronEventaInvoke(noticeWindowEventa.openWindow)
const NOTICE_WINDOW_ID = 'fade-on-hover'
function setFadeOnHoverEnabled(value: boolean) {
if (value)
uiStore.enableFadeOnHover()
else
uiStore.disableFadeOnHover()
trackControlsIslandAction({
action: value ? 'enable_fade_on_hover' : 'disable_fade_on_hover',
})
}
async function handleToggle() {
if (enabled.value) {
uiStore.disableFadeOnHover()
setFadeOnHoverEnabled(false)
return
}
if (uiStore.dontShowItAgainNoticeFadeOnHover) {
uiStore.enableFadeOnHover()
setFadeOnHoverEnabled(true)
return
}
@@ -42,8 +55,9 @@ async function handleToggle() {
route: '/notice/fade-on-hover',
type: 'fade-on-hover',
})
if (acknowledged)
uiStore.enableFadeOnHover()
if (acknowledged) {
setFadeOnHoverEnabled(true)
}
}
catch (error) {
console.error('Failed to open fade-on-hover notice:', error)
@@ -55,6 +69,7 @@ async function handleToggle() {
<ControlButtonTooltip>
<ControlButton
:button-style="props.buttonStyle"
:aria-label="enabled ? t('tamagotchi.stage.controls-island.fade-on-hover.disable') : t('tamagotchi.stage.controls-island.fade-on-hover.enable')"
:class="{ 'border-primary-300/70 shadow-[0_10px_24px_rgba(0,0,0,0.22)]': enabled }"
@click="handleToggle"
>
@@ -38,7 +38,7 @@ const { alwaysOnTop, controlsIslandIconSize } = storeToRefs(settingsStore)
const openSettings = useElectronEventaInvoke(electronOpenSettings)
const openChat = useElectronEventaInvoke(electronOpenChat)
const isLinux = useElectronEventaInvoke(electron.app.isLinux)
const closeWindow = useElectronEventaInvoke(electronAppQuit)
const quitApp = useElectronEventaInvoke(electronAppQuit)
const setAlwaysOnTop = useElectronEventaInvoke(electronWindowSetAlwaysOnTop)
const centerMainWindow = useElectronEventaInvoke(electronCenterMainWindow)
@@ -94,6 +94,10 @@ function toggleAlwaysOnTop() {
alwaysOnTop.value = !alwaysOnTop.value
}
function toggleControls() {
expanded.value = !expanded.value
}
// Grouped classes for icon / border / padding and combined style class
const adjustStyleClasses = computed(() => {
let isLarge: boolean
@@ -158,7 +162,12 @@ function resetMainWindowPosition() {
<div grid grid-cols-3 gap-2>
<ControlButtonTooltip disable-hoverable-content>
<ControlButton :button-style="adjustStyleClasses.button" @click="openSettings({ route: '/settings' })">
<ControlButton
v-track-button="{ name: 'controls_island_action', action: 'toggle_settings' }"
:button-style="adjustStyleClasses.button"
:aria-label="t('tamagotchi.stage.controls-island.open-settings')"
@click="openSettings({ route: '/settings' })"
>
<div i-solar:settings-minimalistic-outline :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
</ControlButton>
<template #tooltip>
@@ -169,7 +178,12 @@ function resetMainWindowPosition() {
<ControlButtonTooltip disable-hoverable-content>
<ControlsIslandProfilePicker placement="up" :open="blockingOverlays.has('profile-picker')" @update:open="setOverlay('profile-picker', $event)">
<template #default="{ toggle }">
<ControlButton :button-style="adjustStyleClasses.button" @click="toggle">
<ControlButton
v-track-button="{ name: 'controls_island_action', action: 'toggle_profile_picker' }"
:button-style="adjustStyleClasses.button"
:aria-label="t('tamagotchi.stage.controls-island.switch-profile')"
@click="toggle"
>
<div i-solar:emoji-funny-square-broken :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
</ControlButton>
</template>
@@ -180,7 +194,12 @@ function resetMainWindowPosition() {
</ControlButtonTooltip>
<ControlButtonTooltip disable-hoverable-content>
<ControlButton :button-style="adjustStyleClasses.button" @click="refreshWindow">
<ControlButton
v-track-button="{ name: 'controls_island_action', action: 'refresh_window' }"
:button-style="adjustStyleClasses.button"
:aria-label="t('tamagotchi.stage.controls-island.refresh')"
@click="refreshWindow"
>
<div i-solar:refresh-linear :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
</ControlButton>
<template #tooltip>
@@ -189,7 +208,12 @@ function resetMainWindowPosition() {
</ControlButtonTooltip>
<ControlButtonTooltip disable-hoverable-content>
<ControlButton :button-style="adjustStyleClasses.button" @click="resetMainWindowPosition()">
<ControlButton
v-track-button="{ name: 'controls_island_action', action: 'center_main_window' }"
:button-style="adjustStyleClasses.button"
:aria-label="t('tamagotchi.stage.controls-island.center-main-window')"
@click="resetMainWindowPosition"
>
<div i-solar:target-linear :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
</ControlButton>
<template #tooltip>
@@ -198,7 +222,15 @@ function resetMainWindowPosition() {
</ControlButtonTooltip>
<ControlButtonTooltip disable-hoverable-content>
<ControlButton :button-style="adjustStyleClasses.button" @click="toggleDark()">
<ControlButton
v-track-button="{
name: 'controls_island_action',
action: isDark ? 'switch_to_light_mode' : 'switch_to_dark_mode',
}"
:button-style="adjustStyleClasses.button"
:aria-label="isDark ? t('tamagotchi.stage.controls-island.switch-to-light-mode') : t('tamagotchi.stage.controls-island.switch-to-dark-mode')"
@click="() => toggleDark()"
>
<Transition name="fade" mode="out-in">
<div v-if="isDark" i-solar:moon-outline :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
<div v-else i-solar:sun-2-outline :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
@@ -210,7 +242,15 @@ function resetMainWindowPosition() {
</ControlButtonTooltip>
<ControlButtonTooltip disable-hoverable-content>
<ControlButton :button-style="adjustStyleClasses.button" @click="toggleAlwaysOnTop()">
<ControlButton
v-track-button="{
name: 'controls_island_action',
action: alwaysOnTop ? 'unpin_from_top' : 'pin_on_top',
}"
:button-style="adjustStyleClasses.button"
:aria-label="alwaysOnTop ? t('tamagotchi.stage.controls-island.unpin-from-top') : t('tamagotchi.stage.controls-island.pin-on-top')"
@click="toggleAlwaysOnTop"
>
<div v-if="alwaysOnTop" i-solar:pin-bold :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
<div v-else i-solar:pin-linear :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300 opacity-50" />
</ControlButton>
@@ -222,7 +262,14 @@ function resetMainWindowPosition() {
<ControlsIslandFadeOnHover :icon-class="adjustStyleClasses.icon" :button-style="adjustStyleClasses.button" />
<ControlButtonTooltip disable-hoverable-content>
<ControlButton :button-style="adjustStyleClasses.button" hover:bg-red-500 hover:text-white @click="closeWindow()">
<ControlButton
v-track-button="{ name: 'controls_island_action', action: 'close_app' }"
:button-style="adjustStyleClasses.button"
:aria-label="t('tamagotchi.stage.controls-island.close')"
hover:bg-red-500
hover:text-white
@click="() => quitApp()"
>
<div i-solar:close-circle-outline :class="adjustStyleClasses.icon" />
</ControlButton>
<template #tooltip>
@@ -236,7 +283,15 @@ function resetMainWindowPosition() {
<!-- Main Controls -->
<div flex flex-col gap-1>
<ControlButtonTooltip side="left">
<ControlButton :button-style="adjustStyleClasses.button" @click="expanded = !expanded">
<ControlButton
v-track-button="{
name: 'controls_island_action',
action: expanded ? 'collapse_controls' : 'expand_controls',
}"
:button-style="adjustStyleClasses.button"
:aria-label="expanded ? t('tamagotchi.stage.controls-island.collapse') : t('tamagotchi.stage.controls-island.expand')"
@click="toggleControls"
>
<div
:class="[adjustStyleClasses.icon, expanded ? 'rotate-180' : 'rotate-0']"
i-solar:alt-arrow-up-line-duotone scale-110 transition-all duration-300
@@ -249,7 +304,12 @@ function resetMainWindowPosition() {
</ControlButtonTooltip>
<ControlButtonTooltip side="left">
<ControlButton :button-style="adjustStyleClasses.button" @click="openChat">
<ControlButton
v-track-button="{ name: 'controls_island_action', action: 'toggle_chat' }"
:button-style="adjustStyleClasses.button"
:aria-label="t('tamagotchi.stage.controls-island.open-chat')"
@click="() => openChat()"
>
<div i-solar:chat-line-line-duotone :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
</ControlButton>
<template #tooltip>
@@ -5,6 +5,7 @@ import Tres from '@tresjs/core'
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
import { PiniaColada } from '@pinia/colada'
import { trackButtonPlugin } from '@proj-airi/stage-ui/directives/track-button'
import { MotionPlugin } from '@vueuse/motion'
import { createPinia } from 'pinia'
import { setupLayouts } from 'virtual:generated-layouts'
@@ -53,4 +54,5 @@ createApp(App)
.use(PiniaColada)
.use(i18n)
.use(Tres)
.use(trackButtonPlugin)
.mount('#app')
@@ -33,7 +33,6 @@ const {
} = useElectronAutoUpdater()
const {
trackUpdateCheckClicked,
trackUpdateDownloaded,
trackUpdateInstallClicked,
trackSettingsChanged,
@@ -126,11 +125,6 @@ const isDowngradeUpdate = computed(() => {
const getUpdaterPreferences = useElectronEventaInvoke(electronGetUpdaterPreferences)
const setUpdaterPreferences = useElectronEventaInvoke(electronSetUpdaterPreferences)
function handleCheckForUpdates() {
trackUpdateCheckClicked({ channel: selectedUpdateChannel.value })
return checkForUpdates()
}
function handleQuitAndInstall() {
trackUpdateInstallClicked({ channel: selectedUpdateChannel.value, version: updateState.value.info?.version })
quitAndInstall()
@@ -384,6 +378,7 @@ onMounted(() => {
<div :class="['flex flex-wrap gap-2']">
<Button
v-track-button="{ name: 'update_check_clicked', channel: selectedUpdateChannel }"
:variant="isError ? 'caution' : 'secondary'"
:loading="isBusy"
:disabled="isDisabled"
@@ -397,7 +392,7 @@ onMounted(() => {
: isError
? t('tamagotchi.stage.about.update.actions.retry-check')
: t('tamagotchi.stage.about.update.actions.check-for-updates')"
@click="handleCheckForUpdates()"
@click="checkForUpdates()"
/>
</div>
</div>
@@ -42,7 +42,7 @@ import {
} from './mcp-config'
const { t } = useI18n()
const { trackMcpServerAdded, trackMcpServerRemoved, trackMcpConnectionTestRun } = useAnalytics()
const { trackMcpServerRemoved, trackMcpConnectionTestRun } = useAnalytics()
const tn = (key: string, params?: Record<string, unknown>) => t(`settings.pages.modules.mcp-server.${key}`, params ?? {})
const invokeOpenConfigFile = useElectronEventaInvoke(electronMcpOpenConfigFile)
@@ -215,7 +215,6 @@ function formatJsonDraft() {
function addServer() {
const server = createServerForm()
servers.value.push(server)
trackMcpServerAdded()
if (!testRowId.value)
testRowId.value = server.rowId
}
@@ -476,6 +475,7 @@ onMounted(async () => {
</article>
<Button
v-track-button="{ name: 'mcp_server_added' }"
variant="secondary-muted" size="md" block :disabled="isBusy"
icon="i-solar:add-circle-bold-duotone" :label="tn('actions.add-server')"
@click="addServer"
+2
View File
@@ -7,6 +7,7 @@ import NProgress from 'nprogress'
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
import { PiniaColada } from '@pinia/colada'
import { isEnvTruthy } from '@proj-airi/stage-shared'
import { trackButtonPlugin } from '@proj-airi/stage-ui/directives/track-button'
import { MotionPlugin } from '@vueuse/motion'
import { createPinia } from 'pinia'
import { setupLayouts } from 'virtual:generated-layouts'
@@ -55,6 +56,7 @@ createApp(App)
.use(PiniaColada)
.use(i18n)
.use(Tres)
.use(trackButtonPlugin)
.mount('#app')
if (import.meta.env.DEV && !import.meta.env.SSR) {