feat(stage-tamagotchi): add icon size settings for tamagotchi (#940)

This commit is contained in:
leafyy
2026-01-13 03:33:40 +08:00
committed by GitHub
parent bcafcac7c1
commit 0e543b50ce
6 changed files with 134 additions and 64 deletions
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { defineInvoke } from '@moeru/eventa'
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
import { useTheme } from '@proj-airi/ui'
import { useWindowSize } from '@vueuse/core'
import { storeToRefs } from 'pinia'
@@ -21,8 +21,10 @@ const { isDark, toggleDark } = useTheme()
const { t } = useI18n()
const settingsAudioDeviceStore = useSettingsAudioDevice()
const settingsStore = useSettings()
const context = useElectronEventaContext()
const { enabled } = storeToRefs(settingsAudioDeviceStore)
const { controlsIslandIconSize } = storeToRefs(settingsStore)
const openSettings = useElectronEventaInvoke(electronOpenSettings)
const openChat = useElectronEventaInvoke(electronOpenChat)
@@ -35,7 +37,22 @@ const LARGE_THRESHOLD = ICON_PLACEHOLDER_PX * BUTTON_COUNT
// Grouped classes for icon / border / padding and combined style class
const adjustStyleClasses = computed(() => {
const isLarge = windowHeight.value > LARGE_THRESHOLD
let isLarge: boolean
// Determine size based on setting
switch (controlsIslandIconSize.value) {
case 'large':
isLarge = true
break
case 'small':
isLarge = false
break
case 'auto':
default:
isLarge = windowHeight.value > LARGE_THRESHOLD
break
}
const icon = isLarge ? 'size-5' : 'size-3'
const border = isLarge ? 'border-2' : 'border-0'
const padding = isLarge ? 'p-2' : 'p-0.5'
@@ -37,6 +37,13 @@ language:
description: >
Change the language of the AIRI interface. This will not affect the language
of the character's responses.
controls-island:
icon-size:
title: Controls Island Icon Size
description: Adjust the size of the controls island icons
auto: Auto (responsive)
large: Large (default)
small: Small
live2d:
change-model:
from-file: Load from File
@@ -34,6 +34,13 @@ language:
title: 语言
description: >
切换显示界面的语言
controls-island:
icon-size:
title: 侧边控制栏图标大小
description: 调整侧边控制栏图标的大小
auto: 自动(响应式)
large: 默认
small: 小图标
live2d:
change-model:
from-file: 从文件加载
@@ -5,6 +5,14 @@ import { FieldCheckbox, FieldSelect, useTheme } from '@proj-airi/ui'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = withDefaults(defineProps<{
needscontrolsIslandIconSizeSetting?: boolean
}>(), {
needscontrolsIslandIconSizeSetting: import.meta.env.RUNTIME_ENVIRONMENT === 'electron',
})
const showControlsIsland = computed(() => props.needscontrolsIslandIconSizeSetting)
const settings = useSettings()
const { t } = useI18n()
@@ -40,7 +48,26 @@ const languages = computed(() => {
transition="all ease-in-out duration-250"
:label="t('settings.language.title')"
:description="t('settings.language.description')"
:options="languages"
:options="languages as Array<{ value: string; label: string }>"
/>
<!-- Controls Island Icon Size -->
<FieldSelect
v-if="showControlsIsland"
v-model="settings.controlsIslandIconSize"
v-motion
:initial="{ opacity: 0, y: 10 }"
:enter="{ opacity: 1, y: 0 }"
:duration="250 + (4 * 10)"
:delay="4 * 50"
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') },
]"
/>
<div
+4
View File
@@ -115,6 +115,8 @@ export const useSettings = defineStore('settings', () => {
const [allowVisibleOnAllWorkspaces, resetAllowVisibleOnAllWorkspaces] = createResettableLocalStorage('settings/allow-visible-on-all-workspaces', true)
const [controlsIslandIconSize, resetControlsIslandIconSize] = createResettableLocalStorage<'auto' | 'large' | 'small'>('settings/controls-island/icon-size', 'auto')
function getLanguage() {
let language = localStorage.getItem('settings/language')
@@ -186,6 +188,7 @@ export const useSettings = defineStore('settings', () => {
resetThemeColorsHueDynamic()
resetAllowVisibleOnAllWorkspaces()
resetControlsIslandIconSize()
await updateStageModel()
}
@@ -212,6 +215,7 @@ export const useSettings = defineStore('settings', () => {
themeColorsHueDynamic,
allowVisibleOnAllWorkspaces,
controlsIslandIconSize,
setThemeColorsHue,
applyPrimaryColorFrom,
@@ -10,6 +10,7 @@ import {
ComboboxItem,
ComboboxItemIndicator,
ComboboxLabel,
ComboboxPortal,
ComboboxRoot,
ComboboxSeparator,
ComboboxTrigger,
@@ -62,69 +63,76 @@ function toDisplayValue(value: T): string {
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxContent
:avoid-collisions="true"
:class="[
'absolute z-10 w-full mt-1 min-w-[160px] overflow-hidden rounded-xl shadow-sm border will-change-[opacity,transform] max-h-50dvh',
'data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade',
'bg-white dark:bg-neutral-900',
'border-neutral-200 dark:border-neutral-800 border-solid border-2 focus:border-neutral-300 dark:focus:border-neutral-600',
]"
>
<ComboboxViewport class="p-[2px]">
<ComboboxEmpty
:class="[
'font-medium py-2 px-2',
'text-xs text-neutral-700 dark:text-neutral-200',
'transition-colors duration-200 ease-in-out',
]"
/>
<ComboboxPortal>
<ComboboxContent
position="popper"
side="bottom"
align="start"
:side-offset="4"
:avoid-collisions="true"
:class="[
'z-150 w-full min-w-[160px] overflow-hidden rounded-xl shadow-sm border will-change-[opacity,transform]',
'data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade',
'bg-white dark:bg-neutral-900',
'border-neutral-200 dark:border-neutral-800 border-solid border-2 focus:border-neutral-300 dark:focus:border-neutral-600',
]"
:style="{ width: 'var(--reka-combobox-trigger-width)' }"
>
<ComboboxViewport :class="['p-[2px]', 'max-h-50dvh', 'overflow-y-auto']">
<ComboboxEmpty
:class="[
'font-medium py-2 px-2',
'text-xs text-neutral-700 dark:text-neutral-200',
'transition-colors duration-200 ease-in-out',
]"
/>
<template
v-for="(group, index) in options"
:key="group.groupLabel"
>
<ComboboxGroup :class="['overflow-x-hidden']">
<ComboboxSeparator
v-if="index !== 0"
:class="['m-[5px]', 'h-[1px]', 'bg-neutral-400']"
/>
<template
v-for="(group, index) in options"
:key="group.groupLabel"
>
<ComboboxGroup :class="['overflow-x-hidden']">
<ComboboxSeparator
v-if="index !== 0"
:class="['m-[5px]', 'h-[1px]', 'bg-neutral-400']"
/>
<ComboboxLabel
:class="[
'px-[25px] text-xs leading-[25px]',
'text-neutral-500 dark:text-neutral-400',
'transition-colors duration-200 ease-in-out',
]"
>
{{ group.groupLabel }}
</ComboboxLabel>
<ComboboxItem
v-for="option in group.children"
:key="option.label"
:text-value="option.label"
:value="option.value"
:class="[
'leading-none rounded-lg flex items-center h-8 pr-[0.5rem] pl-[1.5rem] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none',
'data-[highlighted]:bg-neutral-100 dark:data-[highlighted]:bg-neutral-800',
'text-sm text-neutral-700 dark:text-neutral-200 data-[disabled]:text-neutral-400 dark:data-[disabled]:text-neutral-600 data-[highlighted]:text-grass1',
'transition-colors duration-200 ease-in-out',
'cursor-pointer',
]"
>
<ComboboxItemIndicator
:class="['absolute', 'left-0', 'w-[25px]', 'inline-flex', 'items-center', 'justify-center', 'opacity-30']"
<ComboboxLabel
:class="[
'px-[25px] text-xs leading-[25px]',
'text-neutral-500 dark:text-neutral-400',
'transition-colors duration-200 ease-in-out',
]"
>
<div i-solar:alt-arrow-right-outline />
</ComboboxItemIndicator>
<span :class="['line-clamp-1', 'overflow-hidden', 'text-ellipsis', 'whitespace-nowrap']">
{{ option.label }}
</span>
</ComboboxItem>
</ComboboxGroup>
</template>
</ComboboxViewport>
</ComboboxContent>
{{ group.groupLabel }}
</ComboboxLabel>
<ComboboxItem
v-for="option in group.children"
:key="option.label"
:text-value="option.label"
:value="option.value"
:class="[
'leading-normal rounded-lg flex items-center h-8 pr-[0.5rem] pl-[1.5rem] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none',
'data-[highlighted]:bg-neutral-100 dark:data-[highlighted]:bg-neutral-800',
'text-sm text-neutral-700 dark:text-neutral-200 data-[disabled]:text-neutral-400 dark:data-[disabled]:text-neutral-600 data-[highlighted]:text-grass1',
'transition-colors duration-200 ease-in-out',
'cursor-pointer',
]"
>
<ComboboxItemIndicator
:class="['absolute', 'left-0', 'w-[25px]', 'inline-flex', 'items-center', 'justify-center', 'opacity-30']"
>
<div i-solar:alt-arrow-right-outline />
</ComboboxItemIndicator>
<span :class="['line-clamp-1', 'overflow-hidden', 'text-ellipsis', 'whitespace-nowrap']">
{{ option.label }}
</span>
</ComboboxItem>
</ComboboxGroup>
</template>
</ComboboxViewport>
</ComboboxContent>
</ComboboxPortal>
</ComboboxRoot>
</template>