feat(stage-tamagotchi): notice before toggling Fade on Hover
This commit is contained in:
@@ -22,6 +22,7 @@ import { setupCaptionWindowManager } from './windows/caption'
|
||||
import { setupChatWindowReusableFunc } from './windows/chat'
|
||||
import { setupInlayWindow } from './windows/inlay'
|
||||
import { setupMainWindow } from './windows/main'
|
||||
import { setupNoticeWindowManager } from './windows/notice'
|
||||
import { setupSettingsWindowReusableFunc } from './windows/settings'
|
||||
import { toggleWindowShow } from './windows/shared/window'
|
||||
import { setupWidgetsWindowManager } from './windows/widgets'
|
||||
@@ -103,13 +104,14 @@ app.whenReady().then(async () => {
|
||||
const channelServerModule = injeca.provide('modules:channel-server', async () => setupChannelServer())
|
||||
const chatWindow = injeca.provide('windows:chat', { build: () => setupChatWindowReusableFunc() })
|
||||
const widgetsManager = injeca.provide('windows:widgets', { build: () => setupWidgetsWindowManager() })
|
||||
const noticeWindow = injeca.provide('windows:notice', { build: () => setupNoticeWindowManager() })
|
||||
|
||||
const settingsWindow = injeca.provide('windows:settings', {
|
||||
dependsOn: { widgetsManager },
|
||||
build: ({ dependsOn }) => setupSettingsWindowReusableFunc(dependsOn),
|
||||
})
|
||||
const mainWindow = injeca.provide('windows:main', {
|
||||
dependsOn: { settingsWindow, chatWindow, widgetsManager },
|
||||
dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow },
|
||||
build: async ({ dependsOn }) => setupMainWindow(dependsOn),
|
||||
})
|
||||
const captionWindow = injeca.provide('windows:caption', {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BrowserWindowConstructorOptions, Rectangle } from 'electron'
|
||||
|
||||
import type { WidgetsWindowManager } from '../widgets'
|
||||
import type { NoticeWindowManager } from '../notice'
|
||||
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { env } from 'node:process'
|
||||
@@ -31,6 +32,7 @@ export async function setupMainWindow(params: {
|
||||
settingsWindow: () => Promise<BrowserWindow>
|
||||
chatWindow: () => Promise<BrowserWindow>
|
||||
widgetsManager: WidgetsWindowManager
|
||||
noticeWindow: NoticeWindowManager
|
||||
}) {
|
||||
const {
|
||||
setup: setupConfig,
|
||||
@@ -131,6 +133,7 @@ export async function setupMainWindow(params: {
|
||||
settingsWindow: params.settingsWindow,
|
||||
chatWindow: params.chatWindow,
|
||||
widgetsManager: params.widgetsManager,
|
||||
noticeWindow: params.noticeWindow,
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { BrowserWindow } from 'electron'
|
||||
|
||||
import type { NoticeWindowManager } from '../../notice'
|
||||
import type { WidgetsWindowManager } from '../../widgets'
|
||||
|
||||
import { defineInvokeHandler } from '@moeru/eventa'
|
||||
import { createContext } from '@moeru/eventa/adapters/electron/main'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { electronOpenChat, electronOpenMainDevtools, electronOpenSettings } from '../../../../shared/eventa'
|
||||
import { electronOpenChat, electronOpenMainDevtools, electronOpenSettings, noticeWindowEventa } from '../../../../shared/eventa'
|
||||
import { createWidgetsService } from '../../../services/airi/widgets'
|
||||
import { createScreenService, createWindowService } from '../../../services/electron'
|
||||
import { toggleWindowShow } from '../../shared'
|
||||
@@ -16,6 +17,7 @@ export function setupMainWindowElectronInvokes(params: {
|
||||
settingsWindow: () => Promise<BrowserWindow>
|
||||
chatWindow: () => Promise<BrowserWindow>
|
||||
widgetsManager: WidgetsWindowManager
|
||||
noticeWindow: NoticeWindowManager
|
||||
}) {
|
||||
// TODO: once we refactored eventa to support window-namespaced contexts,
|
||||
// we can remove the setMaxListeners call below since eventa will be able to dispatch and
|
||||
@@ -31,4 +33,5 @@ export function setupMainWindowElectronInvokes(params: {
|
||||
defineInvokeHandler(context, electronOpenMainDevtools, () => params.window.webContents.openDevTools({ mode: 'detach' }))
|
||||
defineInvokeHandler(context, electronOpenSettings, async () => toggleWindowShow(await params.settingsWindow()))
|
||||
defineInvokeHandler(context, electronOpenChat, async () => toggleWindowShow(await params.chatWindow()))
|
||||
defineInvokeHandler(context, noticeWindowEventa.openWindow, (payload) => params.noticeWindow.open(payload))
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+56
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ControlButton from './ControlButton.vue'
|
||||
import ControlButtonTooltip from './ControlButtonTooltip.vue'
|
||||
|
||||
import { noticeWindowEventa } from '../../../../shared/eventa'
|
||||
import { useElectronEventaInvoke } from '../../../composables/electron-vueuse/use-electron-eventa-context'
|
||||
import { useControlsIslandStore } from '../../../stores/controls-island'
|
||||
|
||||
const uiStore = useControlsIslandStore()
|
||||
const enabled = computed(() => uiStore.fadeOnHoverEnabled)
|
||||
const { t } = useI18n()
|
||||
|
||||
const requestNotice = useElectronEventaInvoke(noticeWindowEventa.openWindow)
|
||||
const NOTICE_WINDOW_ID = 'fade-on-hover'
|
||||
|
||||
async function handleToggle() {
|
||||
if (enabled.value) {
|
||||
uiStore.disableFadeOnHover()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const acknowledged = await requestNotice({
|
||||
id: NOTICE_WINDOW_ID,
|
||||
route: '/notice/fade-on-hover',
|
||||
type: 'fade-on-hover',
|
||||
})
|
||||
if (acknowledged)
|
||||
uiStore.enableFadeOnHover()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to open fade-on-hover notice:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton
|
||||
:class="{ 'border-primary-300/70 shadow-[0_10px_24px_rgba(0,0,0,0.22)]': enabled }"
|
||||
@click="handleToggle"
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="enabled" i-ph:eye size-5 text="primary-700 dark:primary-300" />
|
||||
<div v-else i-ph:eye-slash size-5 text="neutral-800 dark:neutral-300" />
|
||||
</Transition>
|
||||
</ControlButton>
|
||||
|
||||
<template #tooltip>
|
||||
{{ enabled ? t('tamagotchi.stage.controls-island.fade-on-hover.disable') : t('tamagotchi.stage.controls-island.fade-on-hover.enable') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
</template>
|
||||
@@ -7,6 +7,7 @@ import { ref } from 'vue'
|
||||
|
||||
import ControlButton from './ControlButton.vue'
|
||||
import ControlButtonTooltip from './ControlButtonTooltip.vue'
|
||||
import ControlsIslandFadeOnHover from './ControlsIslandFadeOnHover.vue'
|
||||
import ControlsIslandHearingConfig from './ControlsIslandHearingConfig.vue'
|
||||
import IndicatorMicVolume from './IndicatorMicVolume.vue'
|
||||
|
||||
@@ -75,6 +76,8 @@ defineExpose({ hearingDialogOpen })
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
|
||||
<ControlsIslandFadeOnHover />
|
||||
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton cursor-move :class="{ 'drag-region': isLinux }" @mousedown="startDraggingWindow?.()">
|
||||
<div i-ph:arrows-out-cardinal size-5 text="neutral-800 dark:neutral-300" />
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consci
|
||||
import { useHearingSpeechInputPipeline } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { refDebounced, useBroadcastChannel } from '@vueuse/core'
|
||||
import { refDebounced, useBroadcastChannel, watchPausable } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onUnmounted, ref, toRef, watch } from 'vue'
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
useElectronMouseInWindow,
|
||||
useElectronRelativeMouse,
|
||||
} from '../composables/electron-vueuse'
|
||||
import { useControlsIslandStore } from '../stores/controls-island'
|
||||
import { useWindowStore } from '../stores/window'
|
||||
|
||||
const resourceStatusIslandRef = ref<InstanceType<typeof ResourceStatusIsland>>()
|
||||
@@ -53,16 +54,17 @@ const setIgnoreMouseEvents = useElectronEventaInvoke(electron.window.setIgnoreMo
|
||||
|
||||
const { scale, positionInPercentageString } = storeToRefs(useLive2d())
|
||||
const { live2dLookAtX, live2dLookAtY } = storeToRefs(useWindowStore())
|
||||
const { fadeOnHoverEnabled } = storeToRefs(useControlsIslandStore())
|
||||
|
||||
watch(componentStateStage, () => isLoading.value = componentStateStage.value !== 'mounted', { immediate: true })
|
||||
|
||||
const { pause, resume } = watch(isTransparent, (transparent) => {
|
||||
shouldFadeOnCursorWithin.value = !transparent
|
||||
const { pause, resume } = watchPausable(isTransparent, (transparent) => {
|
||||
shouldFadeOnCursorWithin.value = fadeOnHoverEnabled.value && !transparent
|
||||
}, { immediate: true })
|
||||
|
||||
const hearingDialogOpen = computed(() => controlsIslandRef.value?.hearingDialogOpen ?? false)
|
||||
|
||||
watch([isOutsideFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTransparent, hearingDialogOpen], () => {
|
||||
watch([isOutsideFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTransparent, hearingDialogOpen, fadeOnHoverEnabled], () => {
|
||||
if (hearingDialogOpen.value) {
|
||||
// Hearing dialog/drawer is open; keep window interactive
|
||||
isIgnoringMouseEvents.value = false
|
||||
@@ -83,13 +85,14 @@ watch([isOutsideFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTrans
|
||||
pause()
|
||||
}
|
||||
else {
|
||||
// Otherwise allow click-through while we fade UI based on transparency
|
||||
// Otherwise allow click-through while we fade UI based on transparency (when enabled)
|
||||
isIgnoringMouseEvents.value = true
|
||||
if (!isOutsideWindow.value && !isTransparent.value) {
|
||||
shouldFadeOnCursorWithin.value = true
|
||||
}
|
||||
shouldFadeOnCursorWithin.value = fadeOnHoverEnabled.value && !isOutsideWindow.value && !isTransparent.value
|
||||
setIgnoreMouseEvents([true, { forward: true }])
|
||||
resume()
|
||||
if (fadeOnHoverEnabled.value)
|
||||
resume()
|
||||
else
|
||||
pause()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -226,7 +229,9 @@ watch([stream, () => vadLoaded.value], async ([s, loaded]) => {
|
||||
:y-offset="positionInPercentageString.y"
|
||||
mb="<md:18"
|
||||
/>
|
||||
<ControlsIsland ref="controlsIslandRef" />
|
||||
<ControlsIsland
|
||||
ref="controlsIslandRef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="isLoading" h-full w-full>
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@proj-airi/stage-ui/components'
|
||||
import { TransitionVertical } from '@proj-airi/ui'
|
||||
import { refDebounced, useDark, useMouseInElement } from '@vueuse/core'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import VideoTutorialFadeOnHoverDark from '../../assets/videos/tutorial/tutorial-fade-on-hover.dark.mp4'
|
||||
import VideoTutorialFadeOnHoverLight from '../../assets/videos/tutorial/tutorial-fade-on-hover.light.mp4'
|
||||
|
||||
import { noticeWindowEventa } from '../../../shared/eventa'
|
||||
import { useElectronEventaContext, useElectronEventaInvoke } from '../../composables/electron-vueuse'
|
||||
|
||||
const context = useElectronEventaContext()
|
||||
const sendAction = useElectronEventaInvoke(noticeWindowEventa.windowAction, context.value)
|
||||
const notifyMounted = useElectronEventaInvoke(noticeWindowEventa.pageMounted, context.value)
|
||||
const notifyUnmounted = useElectronEventaInvoke(noticeWindowEventa.pageUnmounted, context.value)
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const descriptionContainerRef = ref<HTMLDivElement>()
|
||||
const { isOutside } = useMouseInElement(descriptionContainerRef)
|
||||
const descriptionContainerTitleRef = ref<HTMLDivElement>()
|
||||
const descriptionOpenImmediate = computed(() => !isOutside.value)
|
||||
const descriptionOpen = refDebounced(descriptionOpenImmediate, 80)
|
||||
|
||||
const isDark = useDark({ disableTransition: false })
|
||||
|
||||
const requestId = ref<string | null>(null)
|
||||
const waitingForRequest = computed(() => !requestId.value)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const id = typeof route.query.id === 'string'
|
||||
? route.query.id
|
||||
: Array.isArray(route.query.id)
|
||||
? route.query.id[0]
|
||||
: null
|
||||
const pending = await notifyMounted({ id: id ?? undefined })
|
||||
if (pending?.id && pending.type === 'fade-on-hover')
|
||||
requestId.value = pending.id
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Failed to notify notice window mounted:', error)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(async () => {
|
||||
try {
|
||||
await notifyUnmounted({ id: undefined })
|
||||
}
|
||||
catch {
|
||||
/* noop */
|
||||
}
|
||||
})
|
||||
|
||||
async function handleAction(action: 'confirm' | 'cancel' | 'close') {
|
||||
const id = requestId.value
|
||||
if (!id) {
|
||||
window.close()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await sendAction({ id, action })
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Failed to notify main process of notice action:', error)
|
||||
}
|
||||
finally {
|
||||
window.close()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-100dvh w-100dvw">
|
||||
<div class="relative h-full w-full flex flex-col gap-4 text-neutral-900 dark:text-neutral-100">
|
||||
<div class="absolute inset-0 z-0 h-full w-full overflow-hidden text-xs text-neutral-600 dark:text-neutral-400">
|
||||
<video :src="isDark ? VideoTutorialFadeOnHoverDark : VideoTutorialFadeOnHoverLight" autoplay muted loop class="h-full w-full object-cover" />
|
||||
</div>
|
||||
<div class="relative z-1 h-full w-full flex flex-col">
|
||||
<div class="mb-2 flex items-center justify-between gap-2 px-4 pt-4">
|
||||
<div class="inline-flex items-center gap-2 rounded-full bg-primary-500 px-3 py-1 text-[11px] text-primary-100 font-semibold tracking-[0.14em] uppercase">
|
||||
Tutorial
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-primary-300 shadow-[0_0_12px_rgba(0,0,0,0.35)]" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'w-fit',
|
||||
'pl-4 pr-5 py-4 text-2xl font-semibold leading-tight',
|
||||
'rounded-r-xl',
|
||||
'bg-neutral-100/80 dark:bg-neutral-900/80',
|
||||
'backdrop-blur-sm',
|
||||
]"
|
||||
>
|
||||
{{ t('tamagotchi.stage.notice.fade-on-hover.title') }}
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
<div class="w-full px-4 pb-4">
|
||||
<div
|
||||
ref="descriptionContainerRef"
|
||||
:class="[
|
||||
'flex flex-col overflow-hidden',
|
||||
'bg-neutral-100/90 dark:bg-neutral-900/90',
|
||||
'backdrop-blur-sm',
|
||||
'p-3 sm:p-4',
|
||||
'rounded-lg',
|
||||
]"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<div ref="descriptionContainerTitleRef" class="line-clamp-1 min-h-full flex-1 overflow-hidden text-ellipsis text-lg font-semibold space-y-0.5">
|
||||
<template v-if="!descriptionOpen">
|
||||
<i18n-t keypath="tamagotchi.stage.notice.fade-on-hover.opacity" tag="div">
|
||||
<template #value>
|
||||
<span class="text-primary-800 font-semibold dark:text-primary-100">
|
||||
{{ t('tamagotchi.stage.notice.fade-on-hover.value') }}
|
||||
</span>
|
||||
</template>
|
||||
<template #targets>
|
||||
<span class="text-primary-800 font-semibold dark:text-primary-100">
|
||||
{{ t('tamagotchi.stage.notice.fade-on-hover.targets') }}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<i18n-t keypath="tamagotchi.stage.notice.fade-on-hover.toggle" tag="div">
|
||||
<template #controls>
|
||||
<span class="inline text-nowrap text-primary-800 font-semibold dark:text-primary-100">
|
||||
{{ t('tamagotchi.stage.notice.fade-on-hover.controls-label') }}
|
||||
</span>
|
||||
</template>
|
||||
<template #icon>
|
||||
<div
|
||||
i-ph:eye-slash
|
||||
class="inline-block align-middle"
|
||||
:aria-label="t('tamagotchi.stage.notice.fade-on-hover.icon-label')"
|
||||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</template>
|
||||
</div>
|
||||
<Button
|
||||
v-if="!descriptionOpen"
|
||||
size="sm"
|
||||
:label="t('tamagotchi.stage.notice.fade-on-hover.read-more')"
|
||||
/>
|
||||
</div>
|
||||
<TransitionVertical>
|
||||
<div v-if="descriptionOpen" class="overflow-hidden space-y-2">
|
||||
<div>
|
||||
{{ t('tamagotchi.stage.notice.fade-on-hover.intro') }}
|
||||
</div>
|
||||
<i18n-t keypath="tamagotchi.stage.notice.fade-on-hover.opacity" tag="div">
|
||||
<template #value>
|
||||
<span class="text-primary-800 font-semibold dark:text-primary-100">
|
||||
{{ t('tamagotchi.stage.notice.fade-on-hover.value') }}
|
||||
</span>
|
||||
</template>
|
||||
<template #targets>
|
||||
<span class="text-primary-800 font-semibold dark:text-primary-100">
|
||||
{{ t('tamagotchi.stage.notice.fade-on-hover.targets') }}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<i18n-t keypath="tamagotchi.stage.notice.fade-on-hover.toggle" tag="div">
|
||||
<template #controls>
|
||||
<span class="inline text-nowrap text-primary-800 font-semibold dark:text-primary-100">
|
||||
{{ t('tamagotchi.stage.notice.fade-on-hover.controls-label') }}
|
||||
</span>
|
||||
</template>
|
||||
<template #icon>
|
||||
<div
|
||||
i-ph:eye-slash
|
||||
class="inline-block align-middle"
|
||||
:aria-label="t('tamagotchi.stage.notice.fade-on-hover.icon-label')"
|
||||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
||||
<div class="mt-3 flex flex-col gap-2 sm:flex-row">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
block
|
||||
:label="t('tamagotchi.stage.notice.fade-on-hover.confirm')"
|
||||
:disabled="waitingForRequest"
|
||||
:loading="waitingForRequest"
|
||||
@click="handleAction('confirm')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionVertical>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: plain
|
||||
</route>
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useControlsIslandStore = defineStore('controls-island', () => {
|
||||
// Persist fade-on-hover preference per user
|
||||
const fadeOnHoverEnabled = useLocalStorage<boolean>('controls-island/fade-on-hover-enabled', false)
|
||||
|
||||
function enableFadeOnHover() {
|
||||
fadeOnHoverEnabled.value = true
|
||||
}
|
||||
|
||||
function disableFadeOnHover() {
|
||||
fadeOnHoverEnabled.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
fadeOnHoverEnabled,
|
||||
enableFadeOnHover,
|
||||
disableFadeOnHover,
|
||||
}
|
||||
})
|
||||
@@ -8,3 +8,21 @@ docs:
|
||||
navbar:
|
||||
appearance:
|
||||
title: Appearance
|
||||
'controls-island':
|
||||
'fade-on-hover':
|
||||
enable: Auto hide
|
||||
disable: Always show
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
title: Fade on Hover
|
||||
intro: Automatically fades the character when your cursor roams nearby. It helps reduce distractions while keeping your character visible.
|
||||
opacity: After turning on, the opacity drops to {value} for both {targets} when your cursor is nearby.
|
||||
toggle: You can toggle it off by hovering at {controls}, then clicking the {icon} again.
|
||||
controls-label: Controls Island
|
||||
icon-label: eye-slash icon
|
||||
value: '0'
|
||||
targets: Character and UI
|
||||
confirm: Got it
|
||||
preparing: Preparing…
|
||||
read-more: Read more
|
||||
preview-title: What is it?
|
||||
|
||||
@@ -8,3 +8,21 @@ docs:
|
||||
navbar:
|
||||
appearance:
|
||||
title: Apariencia
|
||||
'controls-island':
|
||||
'fade-on-hover':
|
||||
enable: Ocultar al pasar el cursor
|
||||
disable: Mostrar siempre
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
title: Desvanecer al pasar el cursor
|
||||
intro: Difumina el personaje automáticamente cuando el cursor está cerca. Reduce distracciones sin perder visibilidad.
|
||||
opacity: Al activarlo, la opacidad baja a {value} para {targets} cuando el cursor está cerca.
|
||||
toggle: Puedes desactivarlo pasando el cursor por {controls} y tocando de nuevo el {icon}.
|
||||
controls-label: Isla de controles
|
||||
icon-label: ícono de ojo tachado
|
||||
value: '0'
|
||||
targets: Personaje y UI
|
||||
confirm: Entendido
|
||||
preparing: Preparando…
|
||||
read-more: Leer más
|
||||
preview-title: ¿Qué es esto?
|
||||
|
||||
@@ -8,3 +8,21 @@ docs:
|
||||
navbar:
|
||||
appearance:
|
||||
title: Apparence
|
||||
'controls-island':
|
||||
'fade-on-hover':
|
||||
enable: Masquer au survol
|
||||
disable: Toujours afficher
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
title: Disparaître au survol
|
||||
intro: Fait disparaître légèrement le personnage quand le curseur s’approche. Moins de distractions tout en restant visible.
|
||||
opacity: Une fois activé, l’opacité descend à {value} pour {targets} quand le curseur est à proximité.
|
||||
toggle: Vous pouvez le désactiver en survolant l’{controls}, puis en cliquant à nouveau sur l’{icon}.
|
||||
controls-label: Îlot de commandes
|
||||
icon-label: icône œil barré
|
||||
value: '0'
|
||||
targets: Personnage et UI
|
||||
confirm: Compris
|
||||
preparing: Préparation…
|
||||
read-more: En savoir plus
|
||||
preview-title: C’est quoi ?
|
||||
|
||||
@@ -8,3 +8,21 @@ docs:
|
||||
navbar:
|
||||
appearance:
|
||||
title: Внешний вид
|
||||
'controls-island':
|
||||
'fade-on-hover':
|
||||
enable: Скрывать при наведении
|
||||
disable: Всегда показывать
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
title: Исчезать при наведении
|
||||
intro: Автоматически делает персонажа прозрачнее, когда курсор рядом. Меньше отвлекает, но остаётся видимым.
|
||||
opacity: После включения непрозрачность падает до {value} для {targets}, когда курсор рядом.
|
||||
toggle: Можно выключить, наведя на {controls} и снова нажав на {icon}.
|
||||
controls-label: Остров управления
|
||||
icon-label: значок перечёркнутого глаза
|
||||
value: '0'
|
||||
targets: Персонажа и интерфейса
|
||||
confirm: Понятно
|
||||
preparing: Подготавливаем…
|
||||
read-more: Подробнее
|
||||
preview-title: Что это?
|
||||
|
||||
@@ -8,3 +8,21 @@ docs:
|
||||
navbar:
|
||||
appearance:
|
||||
title: Giao diện
|
||||
'controls-island':
|
||||
'fade-on-hover':
|
||||
enable: Ẩn khi rê chuột
|
||||
disable: Luôn hiển thị
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
title: Mờ dần khi rê chuột
|
||||
intro: Tự làm mờ nhân vật khi con trỏ ở gần. Giảm xao nhãng nhưng vẫn nhìn thấy nhân vật.
|
||||
opacity: Bật lên thì độ mờ giảm xuống {value} cho {targets} khi con trỏ ở gần.
|
||||
toggle: Tắt đi bằng cách rê chuột qua {controls} rồi bấm lại {icon}.
|
||||
controls-label: Đảo Điều Khiển
|
||||
icon-label: biểu tượng mắt gạch
|
||||
value: '0'
|
||||
targets: Nhân vật và giao diện
|
||||
confirm: Đã hiểu
|
||||
preparing: Đang chuẩn bị…
|
||||
read-more: Xem thêm
|
||||
preview-title: Đây là gì?
|
||||
|
||||
@@ -8,3 +8,21 @@ docs:
|
||||
navbar:
|
||||
appearance:
|
||||
title: 外观
|
||||
'controls-island':
|
||||
'fade-on-hover':
|
||||
enable: 悬停时隐藏
|
||||
disable: 总是显示
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
title: 悬停淡出
|
||||
intro: 当光标靠近时自动让角色变淡,减少干扰又保持可见。
|
||||
opacity: 开启后,不透明度会降到 {value} ,作用于 {targets} ,当光标靠近时。
|
||||
toggle: 想关闭,移动到 {controls} 区域,再点击一次 {icon}。
|
||||
controls-label: 控制岛
|
||||
icon-label: 隐藏图标
|
||||
value: '0'
|
||||
targets: 角色和界面
|
||||
confirm: 知道了
|
||||
preparing: 准备中…
|
||||
read-more: 阅读更多
|
||||
preview-title: 这是什么?
|
||||
|
||||
@@ -8,3 +8,21 @@ docs:
|
||||
navbar:
|
||||
appearance:
|
||||
title: 外貌
|
||||
'controls-island':
|
||||
'fade-on-hover':
|
||||
enable: 懸停時隱藏
|
||||
disable: 總是顯示
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
title: 懸停淡出
|
||||
intro: 當游標靠近時自動讓角色變淡,減少干擾同時保持可見。
|
||||
opacity: 開啟後,不透明度會降到 {value} ,作用於 {targets} ,當游標靠近時。
|
||||
toggle: 要關閉時,將游標移到 {controls} 區域,再點擊一次 {icon}。
|
||||
controls-label: 控制島
|
||||
icon-label: 隱藏圖示
|
||||
value: '0'
|
||||
targets: 角色與介面
|
||||
confirm: 知道了
|
||||
preparing: 準備中…
|
||||
read-more: 閱讀更多
|
||||
preview-title: 這是什麼?
|
||||
|
||||
Reference in New Issue
Block a user