Skip to content

Add feature: auto hide controls island#1534

Draft
leaft wants to merge 19 commits intomoeru-ai:mainfrom
leaft:add-settings-auto-hide-controls-island
Draft

Add feature: auto hide controls island#1534
leaft wants to merge 19 commits intomoeru-ai:mainfrom
leaft:add-settings-auto-hide-controls-island

Conversation

@leaft
Copy link
Copy Markdown
Collaborator

@leaft leaft commented Mar 31, 2026

Description

  • 为控制岛添加自动隐藏功能,可以设置隐藏delay、展示delay、隐藏时的透明度
  • 同时删除了暂时无用的 controlslandIconSetting
  • 提取hooks,使用 setTimeout 处理自动隐藏延迟和菜单折叠

Linked Issues

#1405

Additional Context

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

⏳ Approval required for deploying to Cloudflare Workers (Preview) for stage-web.

Name Link
🔭 Waiting for approval For maintainers, approve here

Hey, @nekomeowww, @sumimakito, @luoling8192, @LemonNekoGH, kindly take some time to review and approve this deployment when you are available. Thank you! 🙏

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces several significant updates, including a version bump to 0.9.0-beta.2, the addition of a power monitor service for Electron, and a new expression controller for Live2D models. It also refactors the speech pipeline to support concurrent TTS generation and implements a consumer registration system for event delivery. My feedback focuses on potential performance and architectural concerns: the global Cache-Control middleware may be too broad, the speech pipeline's concurrency management could be optimized, and the consumer selection logic in the server runtime should be monitored for scalability as the number of connected modules increases.

Comment thread apps/server/src/app.ts
Comment on lines +77 to +86
.use('*', async (c, next) => {
await next()

// NOTICE: All API responses should be non-cacheable. Auth responses can
// carry session state through redirects, and stale API payloads are not
// safe to serve from edge caches after user/account mutations.
c.res.headers.set('Cache-Control', 'no-store, no-cache, private, max-age=0')
c.res.headers.set('Pragma', 'no-cache')
c.res.headers.set('Expires', '0')
})
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

Setting Cache-Control headers globally on all routes might be too aggressive. Consider if this should be applied to specific API routes instead of the entire application, as it could negatively impact performance for static assets or public data.

Comment on lines +177 to +179
while (!intent.controller.signal.aborted && inFlightTasks.size >= ttsMaxConcurrent) {
await Promise.race(inFlightTasks)
}
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

The while loop condition inFlightTasks.size >= ttsMaxConcurrent might cause a busy-wait if Promise.race resolves quickly. Consider using a more efficient semaphore or queue-based approach to manage concurrency.

Comment on lines +809 to +845
const selectedConsumer = selectConsumer(event, peer.id, delivery)
if (delivery && (delivery.mode === 'consumer' || delivery.mode === 'consumer-group')) {
if (!selectedConsumer) {
logger.withFields({ peer: peer.id, peerName: p.name, event, delivery }).warn('no consumer registered for event delivery')
if (delivery.required) {
send(peer, RESPONSES.error(ServerErrorMessages.noConsumerRegistered, instanceId, event.metadata?.event.id))
}
return
}

try {
logger.withFields({
fromPeer: peer.id,
fromPeerName: p.name,
toPeer: selectedConsumer.peer.id,
toPeerName: selectedConsumer.name,
event,
delivery,
}).debug('sending event to selected consumer')

selectedConsumer.peer.send(payload)
}
catch (err) {
logger.withFields({
fromPeer: peer.id,
fromPeerName: p.name,
toPeer: selectedConsumer.peer.id,
toPeerName: selectedConsumer.name,
event,
delivery,
}).withError(err).error('failed to send event to selected consumer, removing peer')

peers.delete(selectedConsumer.peer.id)
unregisterModulePeer(selectedConsumer, 'consumer send failed')
}
return
}
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

The selectConsumer logic is called within the main request loop. Ensure that the consumerRegistry and peers maps are efficiently indexed to prevent performance degradation as the number of connected modules grows.

@leaft leaft force-pushed the add-settings-auto-hide-controls-island branch from 6a149c7 to c6035c9 Compare March 31, 2026 17:08
leaft and others added 6 commits April 1, 2026 01:10
Add configurable auto-hide feature for controls island:
- autoHideControlsIsland: toggle to enable/disable
- autoHideDelay: delay before hiding (0-60s, default 5s)
- autoShowDelay: delay before showing after mouse enters (0-5s, default 0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@leaft leaft force-pushed the add-settings-auto-hide-controls-island branch from c6035c9 to a2b65bc Compare March 31, 2026 17:10
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements an auto-hide feature for the controls island, allowing users to configure show/hide delays and hidden opacity levels. The changes include refactoring the settings store, adding unit tests, and updating the settings UI. Feedback highlights a logic flaw where the island collapses immediately upon mouse exit, disregarding the configured delay, and suggests utilizing refDebounced for a more robust implementation. Additionally, the reviewer pointed out several hardcoded strings in the UI that should be migrated to i18n translation keys to maintain localization standards.

Comment on lines 117 to 124
watch(isOutside, (outside) => {
lastUpdateTime = Date.now()
timeSinceOutside.value = 0
timeSinceInside.value = 0

if (outside && expanded.value && !isBlocked.value) {
expanded.value = false
}
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.

high

此处的 watch 逻辑在鼠标移出时立即将 expanded 设为 false,这会导致菜单立即折叠,忽略了用户设置的 autoHideDelay 延迟。建议移除此处的立即折叠逻辑,并恢复使用 refDebounced 来实现符合预期的延迟折叠行为。

watch(isOutside, () => {
  lastUpdateTime = Date.now()
  timeSinceOutside.value = 0
  timeSinceInside.value = 0
})

const isOutsideAfterDelay = refDebounced(isOutside, autoHideDelayMs)
watch(isOutsideAfterDelay, (outside) => {
  if (outside && expanded.value && !isBlocked.value) {
    expanded.value = false
  }
})

import { useSettings, useSettingsAudioDevice, useSettingsControlsIsland } from '@proj-airi/stage-ui/stores/settings'
import { useTheme } from '@proj-airi/ui'
import { refDebounced, useIntervalFn } from '@vueuse/core'
import { useIntervalFn } from '@vueuse/core'
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

建议保留 refDebounced 的导入。它在处理基于延迟的状态变化(如自动折叠菜单)时,比手动计时或轮询更简洁且精确。

import { refDebounced, useIntervalFn } from '@vueuse/core'

expanded.value = false
}
}, 1500)
}, () => autoHideDelayMs.value || 5000)
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

如果采用了基于 refDebounced 的监听逻辑,这个 useIntervalFn 将变得冗余。此外,使用轮询来实现延迟逻辑不够精确,建议清理。

Comment on lines +72 to +73
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')"

: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')"

: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')"

: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')"

@leaft leaft force-pushed the add-settings-auto-hide-controls-island branch from 1f30a0d to b944bd8 Compare March 31, 2026 18:53
@leaft
Copy link
Copy Markdown
Collaborator Author

leaft commented Apr 1, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements auto-hide and auto-collapse functionality for the controls island, replacing manual debounce logic with dedicated composables (useControlsIslandAutohide and useControlsIslandCollapse). The settings store and UI are updated to support configurable hide/show delays and opacity, while the previous icon size setting is removed in favor of static styling. Review feedback recommends using internationalization for new UI labels and exposing the new settings in the main settings store for consistency.

Comment on lines +72 to +73
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

这里的 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.

Comment on lines 83 to 84
allowVisibleOnAllWorkspaces: controlsIslandRefs.allowVisibleOnAllWorkspaces,
alwaysOnTop: controlsIslandRefs.alwaysOnTop,
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

虽然 useSettings 已被标记为弃用,但作为统一的设置入口,建议也将新增加的自动隐藏相关设置项(autoHideControlsIsland, autoHideDelay, autoShowDelay, autoHideOpacity)同步暴露出来,以保持接口的一致性。在处理这些 UI 隐藏相关的设置时,请注意根据背景类型进行审美适配。

Suggested change
allowVisibleOnAllWorkspaces: controlsIslandRefs.allowVisibleOnAllWorkspaces,
alwaysOnTop: controlsIslandRefs.alwaysOnTop,
allowVisibleOnAllWorkspaces: controlsIslandRefs.allowVisibleOnAllWorkspaces,
alwaysOnTop: controlsIslandRefs.alwaysOnTop,
autoHideControlsIsland: controlsIslandRefs.autoHideControlsIsland,
autoHideDelay: controlsIslandRefs.autoHideDelay,
autoShowDelay: controlsIslandRefs.autoShowDelay,
autoHideOpacity: controlsIslandRefs.autoHideOpacity,
References
  1. Conditionally hide UI elements that clash aesthetically with certain background types, such as custom images.

@leaft
Copy link
Copy Markdown
Collaborator Author

leaft commented Apr 2, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces an auto-hide and auto-collapse feature for the Tamagotchi stage controls island. It includes new Pinia store settings for hide/show delays and opacity, two new composables (useControlsIslandAutoHide and useControlsIslandCollapse) to manage the logic, and corresponding UI updates in the settings panel. A potential issue was identified in the auto-hide logic where rapid mouse movements could cause visual flickering due to immediate state resets in the watcher; a refactoring of the timer logic is suggested to ensure smoother transitions.

Comment on lines +44 to +78
const isOutsideDelayed = ref(isOutside.value)
const isInsideDelayed = ref(!isOutside.value)

// --- Auto-hide island (only works when autoHideControlsIsland = true) ---
const { start: startOutside, stop: stopOutside } = useTimeoutFn(() => {
isOutsideDelayed.value = true
}, () => autoHideDelay.value * 1000, { immediate: false })

const { start: startInside, stop: stopInside } = useTimeoutFn(() => {
isInsideDelayed.value = true
}, () => autoShowDelay.value * 1000, { immediate: false })

// Watch mouse position changes
watch(isOutside, (val) => {
if (!autoHideControlsIsland.value) {
// Not in auto-hide mode, reset states
stopOutside()
stopInside()
isOutsideDelayed.value = val
isInsideDelayed.value = !val
return
}
stopOutside()
stopInside()
if (val) {
// Mouse left - start hide delay timer
isInsideDelayed.value = false
startOutside()
}
else {
// Mouse entered - start show delay timer
isOutsideDelayed.value = false
startInside()
}
})
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

The current implementation of the auto-hide logic can cause the island to flicker (briefly hide and then show) when the mouse moves quickly out and back into the island area. This happens because isInsideDelayed and isOutsideDelayed are reset immediately in the isOutside watcher, which affects the isHidden computed property before the timers have a chance to fire.

Refactoring the logic to only update these states when the respective delays are met (or when the feature is disabled) provides a smoother user experience and avoids unnecessary state transitions during quick mouse movements.

  const isOutsideDelayed = ref(isOutside.value)
  const isInsideDelayed = ref(!isOutside.value)

  // --- Auto-hide island (only works when autoHideControlsIsland = true) ---
  const { start: startOutside, stop: stopOutside } = useTimeoutFn(() => {
    isOutsideDelayed.value = true
    isInsideDelayed.value = false
  }, () => autoHideDelay.value * 1000, { immediate: false })

  const { start: startInside, stop: stopInside } = useTimeoutFn(() => {
    isInsideDelayed.value = true
    isOutsideDelayed.value = false
  }, () => autoShowDelay.value * 1000, { immediate: false })

  // Watch mouse position changes
  watch(isOutside, (val) => {
    stopOutside()
    stopInside()
    if (!autoHideControlsIsland.value) {
      // Not in auto-hide mode, reset states
      isOutsideDelayed.value = val
      isInsideDelayed.value = !val
      return
    }

    if (val) {
      // Mouse left - start hide delay timer if it was shown
      if (isInsideDelayed.value)
        startOutside()
    }
    else {
      // Mouse entered - start show delay timer if it was hidden
      if (isOutsideDelayed.value)
        startInside()
    }
  })

Only update delayed states when the respective delays are met (or when
delay is 0), avoiding unnecessary state transitions during rapid mouse
movements in/out of the island area.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nekomeowww nekomeowww added the scope/ui Scope related to UI/UX, or interface improve, perf, and bugs label Apr 7, 2026
@nekomeowww nekomeowww added feature Related to feature priority/nice-to-have Issue, or Pull Request that nice to have but can be handled later labels Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Related to feature priority/nice-to-have Issue, or Pull Request that nice to have but can be handled later scope/ui Scope related to UI/UX, or interface improve, perf, and bugs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants