Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5d39a93
feat(stage-tamagotchi): add auto-hide controls island settings
leaft Mar 29, 2026
bcdbad7
feat(stage-tamagotchi): watch show delay and hide delay
leaft Mar 29, 2026
2f5562b
feat(settings): add auto-hide opacity setting for controls island
leaft Mar 31, 2026
98981fd
feat(settings): remove controls island icon size setting and simplify…
leaft Mar 31, 2026
91c6ad2
feat(settings): update auto-hide controls island setting to use isSta…
leaft Mar 31, 2026
a2b65bc
feat(tests): add unit tests for settings-controls-island store
leaft Mar 31, 2026
a41440a
feat(settings): refactor auto-hide logic using refDebounced for impro…
leaft Mar 31, 2026
b944bd8
feat(settings): update auto-hide logic to use computed for dynamic de…
leaft Mar 31, 2026
0597d3f
feat(settings): update auto-hide delay logic to use configurable sett…
leaft Mar 31, 2026
c29304c
feat(stage-ui): remove redundant background color class from island div
leaft Apr 1, 2026
577b945
feat(controls-island): refactor auto-hide logic to use useTimeoutFn f…
leaft Apr 1, 2026
a0e185e
feat(controls-island): implement auto-hide and auto-collapse composab…
leaft Apr 1, 2026
72492c1
fix(controls-island): correct import name for auto-hide composable
leaft Apr 1, 2026
4c4ea5c
feat(tests): add unit tests for useControlsIslandAutoHide and useCont…
leaft Apr 1, 2026
33ea17a
refactor(useControlsIslandAutoHide): remove unused variables and stre…
leaft Apr 1, 2026
6018619
feat(controls-island): add useControlsIslandCollapse composable and r…
leaft Apr 1, 2026
a76fb38
[claudesquad] update from 'beautify-feat' on 02 Apr 26 15:47 CST (pau…
leaft Apr 2, 2026
9d35be2
feat(settings): expose auto-hide controls island settings in deprecat…
leaft Apr 2, 2026
7773b30
fix(controls-island): prevent flicker during quick mouse movements
leaft Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script setup lang="ts">
import { defineInvoke } from '@moeru/eventa'
import { useElectronEventaContext, useElectronEventaInvoke, useElectronMouseInElement } from '@proj-airi/electron-vueuse'
import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
import { useControlsIslandAutohide, useControlsIslandCollapse } from '@proj-airi/stage-ui/composables'
import { useSettings, useSettingsAudioDevice, useSettingsControlsIsland } from '@proj-airi/stage-ui/stores/settings'
import { useTheme } from '@proj-airi/ui'
import { refDebounced, useIntervalFn } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
Expand All @@ -29,9 +29,11 @@ const { t } = useI18n()

const settingsAudioDeviceStore = useSettingsAudioDevice()
const settingsStore = useSettings()
const settingsControlsIslandStore = useSettingsControlsIsland()
const context = useElectronEventaContext()
const { enabled } = storeToRefs(settingsAudioDeviceStore)
const { alwaysOnTop, controlsIslandIconSize } = storeToRefs(settingsStore)
const { alwaysOnTop } = storeToRefs(settingsStore)
const { autoHideControlsIsland, autoHideDelay, autoShowDelay, autoHideOpacity } = storeToRefs(settingsControlsIslandStore)
const openSettings = useElectronEventaInvoke(electronOpenSettings)
const openChat = useElectronEventaInvoke(electronOpenChat)
const isLinux = useElectronEventaInvoke(electron.app.isLinux)
Expand All @@ -46,7 +48,10 @@ const blockingOverlays = reactive(new Set<string>())
const isBlocked = computed(() => blockingOverlays.size > 0)

function setOverlay(key: string, active: boolean) {
active ? blockingOverlays.add(key) : blockingOverlays.delete(key)
if (active)
blockingOverlays.add(key)
else
blockingOverlays.delete(key)
}

// Expose for parent (e.g. to disable click-through when a dialog is open)
Expand All @@ -56,12 +61,31 @@ defineExpose({
})

const { isOutside } = useElectronMouseInElement(islandRef)
const isOutsideAfter2seconds = refDebounced(isOutside, 1500)

watch(isOutsideAfter2seconds, (outside) => {
if (outside && expanded.value && !isBlocked.value) {
expanded.value = false
}
// Auto-hide / Auto-show behavior
const { isHidden, hiddenOpacity } = useControlsIslandAutohide({
autoHideControlsIsland,
autoHideDelay,
autoShowDelay,
autoHideOpacity,
isOutside,
isBlocked,
expanded,
})

// Auto-collapse behavior
const { startCollapse, stopCollapse } = useControlsIslandCollapse({
autoHideDelay,
autoHideControlsIsland,
expanded,
isBlocked,
})

// Watch mouse position to trigger collapse
watch(isOutside, (val) => {
stopCollapse()
if (val)
startCollapse()
})

watch(expanded, (isExpanded) => {
Expand All @@ -70,12 +94,6 @@ watch(expanded, (isExpanded) => {
}
})

useIntervalFn(() => {
if (expanded.value && isOutside.value && !isBlocked.value) {
expanded.value = false
}
}, 1500)

// Apply alwaysOnTop on mount and when it changes
watch(alwaysOnTop, (val) => {
setAlwaysOnTop(val)
Expand All @@ -85,31 +103,13 @@ function toggleAlwaysOnTop() {
alwaysOnTop.value = !alwaysOnTop.value
}

// Grouped classes for icon / border / padding and combined style class
const adjustStyleClasses = computed(() => {
let isLarge: boolean

// Determine size based on setting
switch (controlsIslandIconSize.value) {
case 'large':
isLarge = true
break
case 'small':
isLarge = false
break
case 'auto':
default:
// Fixed to large for better visibility in the new layout,
// can be changed to windowHeight based check if absolutely needed.
isLarge = true
break
}

const icon = isLarge ? 'size-5' : 'size-3'
const border = isLarge ? 'border-2' : 'border-0'
const padding = isLarge ? 'p-2' : 'p-0.5'
return { icon, border, padding, button: `${border} ${padding}` }
})
// Static style classes for icon / border / padding
const adjustStyleClasses = {
icon: 'size-5',
border: 'border-2',
padding: 'p-2',
button: 'border-2 p-2',
}

/**
* This is a know issue (or expected behavior maybe) to Electron.
Expand All @@ -125,7 +125,14 @@ function refreshWindow() {
</script>

<template>
<div ref="islandRef" fixed bottom-2 right-2>
<div
ref="islandRef"
fixed bottom-2 right-2
:style="autoHideControlsIsland ? { opacity: isHidden ? hiddenOpacity : 1 } : {}"
:class="[
autoHideControlsIsland ? 'transition-opacity duration-300' : '',
]"
>
<div flex flex-col items-end gap-1>
<!-- iOS Style Drawer Panel -->
<Transition
Expand Down
76 changes: 58 additions & 18 deletions packages/stage-pages/src/components/settings-general-fields.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
<script setup lang="ts">
import { all } from '@proj-airi/i18n'
import { isStageTamagotchi } from '@proj-airi/stage-shared'
import { useAnalytics } from '@proj-airi/stage-ui/composables/use-analytics'
import { isPosthogAvailableInBuild } from '@proj-airi/stage-ui/stores/analytics'
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
import { FieldCheckbox, FieldCombobox, useTheme } from '@proj-airi/ui'
import { useSettings, useSettingsControlsIsland } from '@proj-airi/stage-ui/stores/settings'
import { FieldCheckbox, FieldCombobox, FieldRange, useTheme } from '@proj-airi/ui'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

const props = withDefaults(defineProps<{
needsControlsIslandIconSizeSetting?: boolean
needsAutoHideControlsIslandSetting?: boolean
}>(), {
needsControlsIslandIconSizeSetting: import.meta.env.RUNTIME_ENVIRONMENT === 'electron',
needsAutoHideControlsIslandSetting: isStageTamagotchi(),
})

const settings = useSettings()
const settingsControlsIsland = useSettingsControlsIsland()

const showControlsIsland = computed(() => props.needsControlsIslandIconSizeSetting)
const showAutoHideControlsIsland = computed(() => props.needsAutoHideControlsIslandSetting)
const showAnalyticsSettings = computed(() => isPosthogAvailableInBuild())
const analyticsToggleValue = computed({
get: () => showAnalyticsSettings.value ? settings.analyticsEnabled : false,
Expand Down Expand Up @@ -59,24 +61,62 @@ const languages = computed(() => {
:options="languages"
/>

<FieldCombobox
v-if="showControlsIsland"
v-model="settings.controlsIslandIconSize"
<FieldCheckbox
v-if="showAutoHideControlsIsland"
v-model="settingsControlsIsland.autoHideControlsIsland"
v-motion
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (4 * 10)"
:delay="4 * 50"
:class="['transition-all', 'ease-in-out', 'duration-250']"
:label="t('settings.controls-island.icon-size.title')"
:description="t('settings.controls-island.icon-size.description')"
:options="[
{ value: 'auto', label: t('settings.controls-island.icon-size.auto') },
{ value: 'large', label: t('settings.controls-island.icon-size.large') },
{ value: 'small', label: t('settings.controls-island.icon-size.small') },
]"
:duration="250 + (5 * 10)"
:delay="5 * 50"
label="Auto-hide controls island"
description="Hide controls island after mouse leaves, show after mouse enters"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

此处使用了硬编码的字符串。为了保持项目的一致性并支持多语言,请使用 t() 函数配合 i18n 翻译键。

      :label="t('settings.controls-island.auto-hide.title')"
      :description="t('settings.controls-island.auto-hide.description')"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这里的 labeldescription 使用了硬编码的英文文本。建议使用 t() 函数并引用语言包中的键值。但请注意,如果这些硬编码值是临时性的且未来可能独立修改,根据项目规则,建议保持它们独立以方便后续调整,避免过早进行抽象。

References
  1. Avoid abstracting duplicated hardcoded values into a constant if they are temporary and intended to be replaced individually. Keeping them separate can improve visibility and simplify future modifications.

/>

<template v-if="showAutoHideControlsIsland && settingsControlsIsland.autoHideControlsIsland">
<FieldRange
v-model.number="settingsControlsIsland.autoHideDelay"
v-motion
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (6 * 10)"
:delay="6 * 50"
:min="0"
:max="5"
:step="0.1"
:format-value="v => `${v}s`"
label="Hide delay"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

此处使用了硬编码的字符串。请使用 i18n 翻译键。

        :label="t('settings.controls-island.auto-hide.hide-delay')"

/>

<FieldRange
v-model.number="settingsControlsIsland.autoShowDelay"
v-motion
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (7 * 10)"
:delay="7 * 50"
:min="0"
:max="5"
:step="0.1"
:format-value="v => `${v}s`"
label="Show delay"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

此处使用了硬编码的字符串。请使用 i18n 翻译键。

        :label="t('settings.controls-island.auto-hide.show-delay')"

/>

<FieldRange
v-model.number="settingsControlsIsland.autoHideOpacity"
v-motion
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (8 * 10)"
:delay="8 * 50"
:min="0"
:max="100"
:step="1"
:format-value="v => `${v}%`"
label="Hidden opacity"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

此处使用了硬编码的字符串。请使用 i18n 翻译键。

        :label="t('settings.controls-island.auto-hide.opacity')"

/>
</template>

<FieldCheckbox
v-model="analyticsToggleValue"
v-motion
Expand Down
1 change: 1 addition & 0 deletions packages/stage-ui/src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './use-async-state'
export * from './use-breakpoints'
export * from './use-build-info'
export * from './use-chat-session/summary'
export * from './use-controls-island-autohide'
export * from './use-optimistic'
export * from './use-scroll-to-hash'
export * from './vision'
Expand Down
Loading
Loading