feat(stage-tamagotchi): popup controls island and better tray with window opts (#1106)
This commit is contained in:
@@ -2,12 +2,14 @@ import type { createContext } from '@moeru/eventa/adapters/electron/main'
|
||||
import type { BrowserWindow } from 'electron'
|
||||
|
||||
import { defineInvokeHandler } from '@moeru/eventa'
|
||||
import { app } from 'electron'
|
||||
import { isLinux, isMacOS, isWindows } from 'std-env'
|
||||
|
||||
import { electron } from '../../../shared/eventa'
|
||||
import { electron, electronAppQuit } from '../../../shared/eventa'
|
||||
|
||||
export function createAppService(params: { context: ReturnType<typeof createContext>['context'], window: BrowserWindow }) {
|
||||
defineInvokeHandler(params.context, electron.app.isMacOS, () => isMacOS)
|
||||
defineInvokeHandler(params.context, electron.app.isWindows, () => isWindows)
|
||||
defineInvokeHandler(params.context, electron.app.isLinux, () => isLinux)
|
||||
defineInvokeHandler(params.context, electronAppQuit, () => app.quit())
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { defineInvokeHandler } from '@moeru/eventa'
|
||||
import { bounds, startLoopGetBounds } from '@proj-airi/electron-eventa'
|
||||
import { createRendererLoop } from '@proj-airi/electron-vueuse/main'
|
||||
|
||||
import { electron } from '../../../shared/eventa'
|
||||
import { electron, electronWindowClose } from '../../../shared/eventa'
|
||||
import { onAppBeforeQuit, onAppWindowAllClosed } from '../../libs/bootkit/lifecycle'
|
||||
import { resizeWindowByDelta } from '../../windows/shared/window'
|
||||
|
||||
@@ -70,4 +70,10 @@ export function createWindowService(params: { context: ReturnType<typeof createC
|
||||
direction: payload.direction,
|
||||
})
|
||||
})
|
||||
|
||||
defineInvokeHandler(params.context, electronWindowClose, (_, options) => {
|
||||
if (params.window.webContents.id === options?.raw.ipcMainEvent.sender.id) {
|
||||
params.window.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import type { WidgetsWindowManager } from '../windows/widgets'
|
||||
import { env } from 'node:process'
|
||||
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { app, Menu, nativeImage, Tray } from 'electron'
|
||||
import { once } from 'es-toolkit'
|
||||
import { app, Menu, nativeImage, screen, Tray } from 'electron'
|
||||
import { debounce, once } from 'es-toolkit'
|
||||
import { isMacOS } from 'std-env'
|
||||
|
||||
import icon from '../../../resources/icon.png?asset'
|
||||
@@ -18,6 +18,61 @@ import { onAppBeforeQuit } from '../libs/bootkit/lifecycle'
|
||||
import { setupInlayWindow } from '../windows/inlay'
|
||||
import { toggleWindowShow } from '../windows/shared/window'
|
||||
|
||||
const RECOMMENDED_WIDTH = 450
|
||||
const RECOMMENDED_HEIGHT = 600
|
||||
const ASPECT_RATIO = RECOMMENDED_WIDTH / RECOMMENDED_HEIGHT
|
||||
|
||||
function applyWindowSize(window: BrowserWindow, width: number, height: number, x?: number, y?: number): void {
|
||||
window.setResizable(true)
|
||||
const bounds = {
|
||||
width: Math.round(width),
|
||||
height: Math.round(height),
|
||||
} as any
|
||||
if (x !== undefined && y !== undefined) {
|
||||
bounds.x = Math.round(x)
|
||||
bounds.y = Math.round(y)
|
||||
}
|
||||
window.setBounds(bounds)
|
||||
if (x === undefined || y === undefined) {
|
||||
window.center()
|
||||
}
|
||||
window.show()
|
||||
}
|
||||
|
||||
function alignWindow(window: BrowserWindow, position: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'): void {
|
||||
const { width: windowWidth, height: windowHeight } = window.getBounds()
|
||||
const { x: areaX, y: areaY, width: areaWidth, height: areaHeight } = screen.getPrimaryDisplay().workArea
|
||||
|
||||
switch (position) {
|
||||
case 'center':
|
||||
window.center()
|
||||
break
|
||||
case 'top-left':
|
||||
window.setPosition(areaX, areaY)
|
||||
break
|
||||
case 'top-right':
|
||||
window.setPosition(areaX + areaWidth - windowWidth, areaY)
|
||||
break
|
||||
case 'bottom-left':
|
||||
window.setPosition(areaX, areaY + areaHeight - windowHeight)
|
||||
break
|
||||
case 'bottom-right':
|
||||
window.setPosition(areaX + areaWidth - windowWidth, areaY + areaHeight - windowHeight)
|
||||
break
|
||||
}
|
||||
window.show()
|
||||
}
|
||||
|
||||
function isSizeMatch(window: BrowserWindow, targetWidth: number, targetHeight: number): boolean {
|
||||
const { width, height } = window.getBounds()
|
||||
return Math.abs(width - Math.round(targetWidth)) <= 2 && Math.abs(height - Math.round(targetHeight)) <= 2
|
||||
}
|
||||
|
||||
function isPositionMatch(window: BrowserWindow, targetX: number, targetY: number): boolean {
|
||||
const { x, y } = window.getBounds()
|
||||
return Math.abs(x - targetX) <= 5 && Math.abs(y - targetY) <= 5
|
||||
}
|
||||
|
||||
export function setupTray(params: {
|
||||
mainWindow: BrowserWindow
|
||||
settingsWindow: () => Promise<BrowserWindow>
|
||||
@@ -33,44 +88,117 @@ export function setupTray(params: {
|
||||
const appTray = new Tray(trayImage)
|
||||
onAppBeforeQuit(() => appTray.destroy())
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Show', click: () => toggleWindowShow(params.mainWindow) },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Center and Reset Size',
|
||||
click: () => {
|
||||
params.mainWindow.setSize(450, 600)
|
||||
params.mainWindow.center()
|
||||
params.mainWindow.show()
|
||||
const rebuildContextMenu = debounce((): void => {
|
||||
const { x: areaX, y: areaY, width: areaWidth, height: areaHeight } = screen.getPrimaryDisplay().workArea
|
||||
const { width: windowWidth, height: windowHeight } = params.mainWindow.getBounds()
|
||||
|
||||
const fullHeightTarget = areaHeight
|
||||
const fullWidthTarget = Math.floor(areaHeight * ASPECT_RATIO)
|
||||
const halfHeightTarget = Math.floor(areaHeight / 2)
|
||||
const halfWidthTarget = Math.floor(halfHeightTarget * ASPECT_RATIO)
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Show', click: () => toggleWindowShow(params.mainWindow) },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Adjust Sizes',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Recommended (450x600)',
|
||||
type: 'checkbox',
|
||||
checked: isSizeMatch(params.mainWindow, RECOMMENDED_WIDTH, RECOMMENDED_HEIGHT),
|
||||
click: () => applyWindowSize(params.mainWindow, RECOMMENDED_WIDTH, RECOMMENDED_HEIGHT),
|
||||
},
|
||||
{
|
||||
label: 'Full Height',
|
||||
type: 'checkbox',
|
||||
checked: isSizeMatch(params.mainWindow, fullWidthTarget, fullHeightTarget),
|
||||
click: () => applyWindowSize(params.mainWindow, fullWidthTarget, fullHeightTarget),
|
||||
},
|
||||
{
|
||||
label: 'Half Height',
|
||||
type: 'checkbox',
|
||||
checked: isSizeMatch(params.mainWindow, halfWidthTarget, halfHeightTarget),
|
||||
click: () => applyWindowSize(params.mainWindow, halfWidthTarget, halfHeightTarget),
|
||||
},
|
||||
{
|
||||
label: 'Full Screen',
|
||||
type: 'checkbox',
|
||||
checked: isSizeMatch(params.mainWindow, areaWidth, areaHeight),
|
||||
click: () => applyWindowSize(params.mainWindow, areaWidth, areaHeight, areaX, areaY),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Settings...', click: () => params.settingsWindow().then(window => toggleWindowShow(window)) },
|
||||
{ label: 'About...', click: () => params.aboutWindow().then(window => toggleWindowShow(window)) },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Open Inlay...', click: () => setupInlayWindow() },
|
||||
{ label: 'Open Widgets...', click: () => params.widgetsWindow.getWindow().then(window => toggleWindowShow(window)) },
|
||||
{ label: 'Open Caption...', click: () => params.captionWindow.getWindow().then(window => toggleWindowShow(window)) },
|
||||
{
|
||||
type: 'submenu',
|
||||
label: 'Caption Overlay',
|
||||
submenu: Menu.buildFromTemplate([
|
||||
{ type: 'checkbox', label: 'Follow window', checked: params.captionWindow.getIsFollowingWindow(), click: async menuItem => await params.captionWindow.setFollowWindow(Boolean(menuItem.checked)) },
|
||||
{ label: 'Reset position', click: async () => await params.captionWindow.resetToSide() },
|
||||
]),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
...is.dev || env.MAIN_APP_DEBUG || env.APP_DEBUG
|
||||
? [
|
||||
{ type: 'header', label: 'DevTools' },
|
||||
{ label: 'Troubleshoot BeatSync...', click: () => params.beatSyncBgWindow.webContents.openDevTools() },
|
||||
{
|
||||
label: 'Align to',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Center',
|
||||
type: 'checkbox',
|
||||
checked: isPositionMatch(params.mainWindow, areaX + Math.floor((areaWidth - windowWidth) / 2), areaY + Math.floor((areaHeight - windowHeight) / 2)),
|
||||
click: () => alignWindow(params.mainWindow, 'center'),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
] as const // :(
|
||||
: [],
|
||||
{ label: 'Quit', click: () => app.quit() },
|
||||
])
|
||||
{
|
||||
label: 'Top Left',
|
||||
type: 'checkbox',
|
||||
checked: isPositionMatch(params.mainWindow, areaX, areaY),
|
||||
click: () => alignWindow(params.mainWindow, 'top-left'),
|
||||
},
|
||||
{
|
||||
label: 'Top Right',
|
||||
type: 'checkbox',
|
||||
checked: isPositionMatch(params.mainWindow, areaX + areaWidth - windowWidth, areaY),
|
||||
click: () => alignWindow(params.mainWindow, 'top-right'),
|
||||
},
|
||||
{
|
||||
label: 'Bottom Left',
|
||||
type: 'checkbox',
|
||||
checked: isPositionMatch(params.mainWindow, areaX, areaY + areaHeight - windowHeight),
|
||||
click: () => alignWindow(params.mainWindow, 'bottom-left'),
|
||||
},
|
||||
{
|
||||
label: 'Bottom Right',
|
||||
type: 'checkbox',
|
||||
checked: isPositionMatch(params.mainWindow, areaX + areaWidth - windowWidth, areaY + areaHeight - windowHeight),
|
||||
click: () => alignWindow(params.mainWindow, 'bottom-right'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Settings...', click: () => params.settingsWindow().then(window => toggleWindowShow(window)) },
|
||||
{ label: 'About...', click: () => params.aboutWindow().then(window => toggleWindowShow(window)) },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Open Inlay...', click: () => setupInlayWindow() },
|
||||
{ label: 'Open Widgets...', click: () => params.widgetsWindow.getWindow().then(window => toggleWindowShow(window)) },
|
||||
{ label: 'Open Caption...', click: () => params.captionWindow.getWindow().then(window => toggleWindowShow(window)) },
|
||||
{
|
||||
type: 'submenu',
|
||||
label: 'Caption Overlay',
|
||||
submenu: Menu.buildFromTemplate([
|
||||
{ type: 'checkbox', label: 'Follow window', checked: params.captionWindow.getIsFollowingWindow(), click: async menuItem => await params.captionWindow.setFollowWindow(Boolean(menuItem.checked)) },
|
||||
{ label: 'Reset position', click: async () => await params.captionWindow.resetToSide() },
|
||||
]),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
...is.dev || env.MAIN_APP_DEBUG || env.APP_DEBUG
|
||||
? [
|
||||
{ type: 'header', label: 'DevTools' },
|
||||
{ label: 'Troubleshoot BeatSync...', click: () => params.beatSyncBgWindow.webContents.openDevTools() },
|
||||
{ type: 'separator' },
|
||||
] as const
|
||||
: [],
|
||||
{ label: 'Quit', click: () => app.quit() },
|
||||
])
|
||||
|
||||
appTray.setContextMenu(contextMenu)
|
||||
}, 50)
|
||||
|
||||
params.mainWindow.on('resize', rebuildContextMenu)
|
||||
params.mainWindow.on('move', rebuildContextMenu)
|
||||
|
||||
rebuildContextMenu()
|
||||
|
||||
appTray.setContextMenu(contextMenu)
|
||||
appTray.setToolTip('Project AIRI')
|
||||
appTray.addListener('click', () => toggleWindowShow(params.mainWindow))
|
||||
|
||||
|
||||
+130
-75
@@ -1,11 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { defineInvoke } from '@moeru/eventa'
|
||||
import { useElectronEventaContext, useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useElectronEventaContext, useElectronEventaInvoke, useElectronMouseInElement } from '@proj-airi/electron-vueuse'
|
||||
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'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ControlButtonTooltip from './control-button-tooltip.vue'
|
||||
@@ -14,7 +13,7 @@ import ControlsIslandFadeOnHover from './controls-island-fade-on-hover.vue'
|
||||
import ControlsIslandHearingConfig from './controls-island-hearing-config.vue'
|
||||
import IndicatorMicVolume from './indicator-mic-volume.vue'
|
||||
|
||||
import { electron, electronOpenChat, electronOpenSettings, electronStartDraggingWindow } from '../../../../shared/eventa'
|
||||
import { electron, electronOpenChat, electronOpenSettings, electronStartDraggingWindow, electronWindowClose } from '../../../../shared/eventa'
|
||||
|
||||
const { isDark, toggleDark } = useTheme()
|
||||
const { t } = useI18n()
|
||||
@@ -27,13 +26,36 @@ const { controlsIslandIconSize } = storeToRefs(settingsStore)
|
||||
const openSettings = useElectronEventaInvoke(electronOpenSettings)
|
||||
const openChat = useElectronEventaInvoke(electronOpenChat)
|
||||
const isLinux = useElectronEventaInvoke(electron.app.isLinux)
|
||||
const closeWindow = useElectronEventaInvoke(electronWindowClose)
|
||||
|
||||
// Responsive icon & button sizing based on window height
|
||||
const { height: windowHeight } = useWindowSize()
|
||||
// Constants: assume each icon placeholder occupies 50px and there are 7 buttons
|
||||
const ICON_PLACEHOLDER_PX = 50
|
||||
const BUTTON_COUNT = 7
|
||||
const LARGE_THRESHOLD = ICON_PLACEHOLDER_PX * BUTTON_COUNT
|
||||
const expanded = ref(false)
|
||||
const islandRef = ref<HTMLElement | null>(null)
|
||||
const { isOutside } = useElectronMouseInElement(islandRef)
|
||||
|
||||
let collapseTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
watch(isOutside, (val) => {
|
||||
if (val) {
|
||||
if (expanded.value) {
|
||||
collapseTimer = setTimeout(() => {
|
||||
expanded.value = false
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (collapseTimer) {
|
||||
clearTimeout(collapseTimer)
|
||||
collapseTimer = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(expanded, (isExpanded) => {
|
||||
if (!isExpanded && collapseTimer) {
|
||||
clearTimeout(collapseTimer)
|
||||
collapseTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
// Grouped classes for icon / border / padding and combined style class
|
||||
const adjustStyleClasses = computed(() => {
|
||||
@@ -49,7 +71,9 @@ const adjustStyleClasses = computed(() => {
|
||||
break
|
||||
case 'auto':
|
||||
default:
|
||||
isLarge = windowHeight.value > LARGE_THRESHOLD
|
||||
// Fixed to large for better visibility in the new layout,
|
||||
// can be changed to windowHeight based check if absolutely needed.
|
||||
isLarge = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -77,81 +101,112 @@ function refreshWindow() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div fixed bottom-2 right-2>
|
||||
<div flex flex-col gap-1>
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton :button-style="adjustStyleClasses.button" @click="openSettings">
|
||||
<div i-solar:settings-minimalistic-outline :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
|
||||
</ControlButton>
|
||||
<div ref="islandRef" fixed bottom-2 right-2>
|
||||
<div flex flex-col items-end gap-1>
|
||||
<!-- iOS Style Drawer Panel -->
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-500 cubic-bezier(0.32, 0.72, 0, 1)"
|
||||
leave-active-class="transition-all duration-400 cubic-bezier(0.32, 0.72, 0, 1)"
|
||||
enter-from-class="opacity-0 translate-y-8 scale-90 blur-sm"
|
||||
leave-to-class="opacity-0 translate-y-8 scale-90 blur-sm"
|
||||
>
|
||||
<div v-if="expanded" border="1 neutral-200 dark:neutral-800" mb-2 flex flex-col gap-1 rounded-2xl p-2 backdrop-blur-xl class="bg-neutral-100/80 shadow-2xl shadow-black/20 dark:bg-neutral-900/80">
|
||||
<div grid grid-cols-3 gap-2>
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton :button-style="adjustStyleClasses.button" @click="openSettings">
|
||||
<div i-solar:settings-minimalistic-outline :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
|
||||
</ControlButton>
|
||||
<template #tooltip>
|
||||
{{ t('tamagotchi.stage.controls-island.open-settings') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
|
||||
<template #tooltip>
|
||||
{{ t('tamagotchi.stage.controls-island.open-settings') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton :button-style="adjustStyleClasses.button" @click="openChat">
|
||||
<div i-solar:chat-line-line-duotone :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
|
||||
</ControlButton>
|
||||
<template #tooltip>
|
||||
{{ t('tamagotchi.stage.controls-island.open-chat') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton :button-style="adjustStyleClasses.button" @click="openChat">
|
||||
<div i-solar:chat-line-line-duotone :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
|
||||
</ControlButton>
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton :button-style="adjustStyleClasses.button" @click="refreshWindow">
|
||||
<div i-solar:refresh-linear :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
|
||||
</ControlButton>
|
||||
<template #tooltip>
|
||||
{{ t('tamagotchi.stage.controls-island.refresh') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
|
||||
<template #tooltip>
|
||||
{{ t('tamagotchi.stage.controls-island.open-chat') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton :button-style="adjustStyleClasses.button" @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" />
|
||||
</Transition>
|
||||
</ControlButton>
|
||||
<template #tooltip>
|
||||
{{ isDark ? t('tamagotchi.stage.controls-island.switch-to-light-mode') : t('tamagotchi.stage.controls-island.switch-to-dark-mode') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton :button-style="adjustStyleClasses.button" @click="refreshWindow">
|
||||
<div i-solar:refresh-linear :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
|
||||
</ControlButton>
|
||||
<ControlButtonTooltip>
|
||||
<ControlsIslandHearingConfig v-model:show="hearingDialogOpen">
|
||||
<div class="relative">
|
||||
<ControlButton :button-style="adjustStyleClasses.button">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<IndicatorMicVolume v-if="enabled" :class="adjustStyleClasses.icon" />
|
||||
<div v-else i-ph:microphone-slash :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
|
||||
</Transition>
|
||||
</ControlButton>
|
||||
</div>
|
||||
</ControlsIslandHearingConfig>
|
||||
<template #tooltip>
|
||||
{{ t('tamagotchi.stage.controls-island.open-hearing-controls') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
|
||||
<template #tooltip>
|
||||
{{ t('tamagotchi.stage.controls-island.refresh') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
<ControlsIslandFadeOnHover :icon-class="adjustStyleClasses.icon" :button-style="adjustStyleClasses.button" />
|
||||
|
||||
<ControlButtonTooltip>
|
||||
<ControlsIslandHearingConfig v-model:show="hearingDialogOpen">
|
||||
<div class="relative">
|
||||
<ControlButton :button-style="adjustStyleClasses.button">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<IndicatorMicVolume v-if="enabled" :class="adjustStyleClasses.icon" />
|
||||
<div v-else i-ph:microphone-slash :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
|
||||
</Transition>
|
||||
</ControlButton>
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton :button-style="adjustStyleClasses.button" hover:bg-red-500 hover:text-white @click="closeWindow()">
|
||||
<div i-solar:close-circle-outline :class="adjustStyleClasses.icon" />
|
||||
</ControlButton>
|
||||
<template #tooltip>
|
||||
{{ t('tamagotchi.stage.controls-island.close') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
</div>
|
||||
</ControlsIslandHearingConfig>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<template #tooltip>
|
||||
{{ t('tamagotchi.stage.controls-island.open-hearing-controls') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
<!-- Main Controls -->
|
||||
<div flex flex-col gap-1>
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton :button-style="adjustStyleClasses.button" @click="expanded = !expanded">
|
||||
<div
|
||||
|
||||
<ControlsIslandFadeOnHover :icon-class="adjustStyleClasses.icon" :button-style="adjustStyleClasses.button" />
|
||||
:class="[adjustStyleClasses.icon, expanded ? 'rotate-180' : 'rotate-0']"
|
||||
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton :button-style="adjustStyleClasses.button" cursor-move :class="{ 'drag-region': isLinux }" @mousedown="startDraggingWindow?.()">
|
||||
<div i-ph:arrows-out-cardinal :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
|
||||
</ControlButton>
|
||||
i-solar:alt-arrow-up-line-duotone scale-110 transition-all duration-300
|
||||
text="neutral-800 dark:neutral-300"
|
||||
/>
|
||||
</ControlButton>
|
||||
<template #tooltip>
|
||||
{{ expanded ? t('tamagotchi.stage.controls-island.collapse') : t('tamagotchi.stage.controls-island.expand') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
|
||||
<template #tooltip>
|
||||
{{ t('tamagotchi.stage.controls-island.drag-to-move-window') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
|
||||
<ControlButtonTooltip>
|
||||
<!-- Recommended to use `toggleDark()` instead of `toggleDark` -->
|
||||
<!-- See: https://vueuse.org/shared/useToggle/#usage -->
|
||||
<ControlButton :button-style="adjustStyleClasses.button" @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" />
|
||||
</Transition>
|
||||
</ControlButton>
|
||||
|
||||
<template #tooltip>
|
||||
{{ isDark ? t('tamagotchi.stage.controls-island.switch-to-light-mode') : t('tamagotchi.stage.controls-island.switch-to-dark-mode') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
<ControlButtonTooltip>
|
||||
<ControlButton :button-style="adjustStyleClasses.button" cursor-move :class="{ 'drag-region': isLinux }" @mousedown="startDraggingWindow?.()">
|
||||
<div i-ph:arrows-out-cardinal :class="adjustStyleClasses.icon" text="neutral-800 dark:neutral-300" />
|
||||
</ControlButton>
|
||||
<template #tooltip>
|
||||
{{ t('tamagotchi.stage.controls-island.drag-to-move-window') }}
|
||||
</template>
|
||||
</ControlButtonTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -194,6 +194,9 @@ export const widgetsUpdate = defineInvokeEventa<void, { id: string, componentPro
|
||||
export const widgetsFetch = defineInvokeEventa<WidgetSnapshot | void, { id: string }>('eventa:invoke:electron:windows:widgets:fetch')
|
||||
export const widgetsPrepareWindow = defineInvokeEventa<string | undefined, { id?: string }>('eventa:invoke:electron:windows:widgets:prepare')
|
||||
|
||||
export const electronWindowClose = defineInvokeEventa<void>('eventa:invoke:electron:window:close')
|
||||
export const electronAppQuit = defineInvokeEventa<void>('eventa:invoke:electron:app:quit')
|
||||
|
||||
// Internal event from main -> widgets renderer when a widget should render
|
||||
export const widgetsRenderEvent = defineEventa<WidgetSnapshot>('eventa:event:electron:windows:widgets:render')
|
||||
export const widgetsRemoveEvent = defineEventa<{ id: string }>('eventa:event:electron:windows:widgets:remove')
|
||||
|
||||
@@ -3,9 +3,11 @@ import { defineInvokeEventa } from '@moeru/eventa'
|
||||
const isMacOS = defineInvokeEventa<boolean>('eventa:invoke:electron:app:is-macos')
|
||||
const isWindows = defineInvokeEventa<boolean>('eventa:invoke:electron:app:is-windows')
|
||||
const isLinux = defineInvokeEventa<boolean>('eventa:invoke:electron:app:is-linux')
|
||||
const quit = defineInvokeEventa<void>('eventa:invoke:electron:app:quit')
|
||||
|
||||
export const app = {
|
||||
isMacOS,
|
||||
isWindows,
|
||||
isLinux,
|
||||
quit,
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const setIgnoreMouseEvents = defineInvokeEventa<void, [boolean, { forward: boole
|
||||
const setVibrancy = defineInvokeEventa<void, Parameters<BrowserWindow['setVibrancy']> | [null]>('eventa:invoke:electron:window:set-vibrancy')
|
||||
const setBackgroundMaterial = defineInvokeEventa<void, Parameters<BrowserWindow['setBackgroundMaterial']>>('eventa:invoke:electron:window:set-background-material')
|
||||
const resize = defineInvokeEventa<void, { deltaX: number, deltaY: number, direction: ResizeDirection }>('eventa:invoke:electron:window:resize')
|
||||
const close = defineInvokeEventa<void>('eventa:invoke:electron:window:close')
|
||||
|
||||
export type VibrancyType = Parameters<BrowserWindow['setVibrancy']>[0]
|
||||
export type BackgroundMaterialType = Parameters<BrowserWindow['setBackgroundMaterial']>[0]
|
||||
@@ -23,4 +24,5 @@ export const window = {
|
||||
setVibrancy,
|
||||
setBackgroundMaterial,
|
||||
resize,
|
||||
close,
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ docs:
|
||||
'drag-to-move-window': Drag to move window
|
||||
'switch-to-light-mode': Switch to light mode
|
||||
'switch-to-dark-mode': Switch to dark mode
|
||||
'close': Close
|
||||
'expand': Expand
|
||||
'collapse': Collapse
|
||||
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
|
||||
@@ -19,6 +19,7 @@ docs:
|
||||
'drag-to-move-window': Drag to move window
|
||||
'switch-to-light-mode': Switch to light mode
|
||||
'switch-to-dark-mode': Switch to dark mode
|
||||
'close': Fermer
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
title: Disparaître au survol
|
||||
|
||||
@@ -19,6 +19,9 @@ docs:
|
||||
'drag-to-move-window': ウィンドウをドラッグして移動
|
||||
'switch-to-light-mode': ライトモードに切り替え
|
||||
'switch-to-dark-mode': ダークモードに切り替え
|
||||
'close': 閉じる
|
||||
'expand': 展開
|
||||
'collapse': 折りたたむ
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
title: ホバー時にフェード
|
||||
|
||||
@@ -19,6 +19,9 @@ docs:
|
||||
'drag-to-move-window': 拖动以移动窗口
|
||||
'switch-to-light-mode': 切换到亮色模式
|
||||
'switch-to-dark-mode': 切换到暗色模式
|
||||
'close': 关闭
|
||||
'expand': 展开
|
||||
'collapse': 收起
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
title: 悬停淡出
|
||||
|
||||
@@ -18,7 +18,11 @@ docs:
|
||||
'open-hearing-controls': 開啟聽力控制
|
||||
'drag-to-move-window': 長按以移動視窗
|
||||
'switch-to-light-mode': 切換至亮色模式
|
||||
'switch-to-dark-mode': 切換至深色模式
|
||||
'switch-to-dark-mode': 切換到暗色模式
|
||||
'close': 關閉
|
||||
'expand': 展開
|
||||
'collapse': 收起
|
||||
|
||||
notice:
|
||||
'fade-on-hover':
|
||||
title: 懸停淡出
|
||||
|
||||
Reference in New Issue
Block a user