fix(stage-tamagotchi): fix window visibility and ozone flags on wayland (#2288)
This commit is contained in:
@@ -51,6 +51,8 @@ coverage/
|
||||
# Build
|
||||
out/
|
||||
bundle/
|
||||
flatpak/
|
||||
flatpak-repo/
|
||||
.flatpak-builder/
|
||||
.flatpak-repo/
|
||||
*.flatpak
|
||||
|
||||
@@ -10,7 +10,9 @@ finish-args:
|
||||
# GUI environment
|
||||
- --share=network
|
||||
- --socket=x11
|
||||
- --socket=fallback-x11
|
||||
- --socket=wayland
|
||||
- --device=dri
|
||||
- --device=all
|
||||
- --socket=pulseaudio
|
||||
- --socket=system-bus
|
||||
@@ -94,4 +96,8 @@ modules:
|
||||
commands:
|
||||
- ulimit -c 0
|
||||
- export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID"
|
||||
- |
|
||||
if [ -n "$WAYLAND_DISPLAY" ]; then
|
||||
export ELECTRON_OZONE_PLATFORM_HINT=auto
|
||||
fi
|
||||
- exec zypak-wrapper /app/lib/airi/airi "$@"
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { resolveIsWayland } from './ozone'
|
||||
|
||||
describe('resolveIsWayland', () => {
|
||||
it('resolves to true when --ozone-platform is explicitly wayland', () => {
|
||||
expect(resolveIsWayland({
|
||||
explicitOzonePlatform: 'wayland',
|
||||
env: {},
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
it('resolves to false when --ozone-platform is explicitly x11 even in Wayland environment', () => {
|
||||
expect(resolveIsWayland({
|
||||
explicitOzonePlatform: 'x11',
|
||||
env: {
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
},
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('treats --ozone-platform=auto as unresolved and falls back to environment', () => {
|
||||
expect(resolveIsWayland({
|
||||
explicitOzonePlatform: 'auto',
|
||||
env: {
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
},
|
||||
})).toBe(true)
|
||||
|
||||
expect(resolveIsWayland({
|
||||
explicitOzonePlatform: 'auto',
|
||||
env: {
|
||||
XDG_SESSION_TYPE: 'x11',
|
||||
},
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('resolves based on --ozone-platform-hint when not auto', () => {
|
||||
expect(resolveIsWayland({
|
||||
ozonePlatformHint: 'wayland',
|
||||
env: {},
|
||||
})).toBe(true)
|
||||
|
||||
expect(resolveIsWayland({
|
||||
ozonePlatformHint: 'x11',
|
||||
env: {
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
},
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('treats --ozone-platform-hint=auto as unresolved and falls back to environment', () => {
|
||||
expect(resolveIsWayland({
|
||||
ozonePlatformHint: 'auto',
|
||||
env: {
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
},
|
||||
})).toBe(true)
|
||||
|
||||
expect(resolveIsWayland({
|
||||
ozonePlatformHint: 'auto',
|
||||
env: {},
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('falls back to environment variables when no flags are present', () => {
|
||||
expect(resolveIsWayland({
|
||||
env: {
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
},
|
||||
})).toBe(true)
|
||||
|
||||
expect(resolveIsWayland({
|
||||
env: {
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
},
|
||||
})).toBe(true)
|
||||
|
||||
expect(resolveIsWayland({
|
||||
env: {
|
||||
XDG_SESSION_TYPE: 'x11',
|
||||
},
|
||||
})).toBe(false)
|
||||
|
||||
expect(resolveIsWayland({
|
||||
env: {},
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Resolves whether the application is running under the Wayland Ozone backend.
|
||||
*
|
||||
* Checks explicit command-line switches before falling back to session environment variables.
|
||||
* Treats 'auto' as an unresolved platform selection and resolves it using session environment variables.
|
||||
*/
|
||||
export function resolveIsWayland(params: {
|
||||
explicitOzonePlatform?: string
|
||||
ozonePlatformHint?: string
|
||||
env?: Record<string, string | undefined>
|
||||
}): boolean {
|
||||
if (params.explicitOzonePlatform && params.explicitOzonePlatform !== 'auto') {
|
||||
return params.explicitOzonePlatform === 'wayland'
|
||||
}
|
||||
|
||||
if (params.ozonePlatformHint && params.ozonePlatformHint !== 'auto') {
|
||||
return params.ozonePlatformHint === 'wayland'
|
||||
}
|
||||
|
||||
return Boolean(params.env?.WAYLAND_DISPLAY || params.env?.XDG_SESSION_TYPE === 'wayland')
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import icon from '../../resources/icon.png?asset'
|
||||
|
||||
import { openDebugger, setupDebugger } from './app/debugger'
|
||||
import { nullFileLoggerHandle, setupFileLogger } from './app/file-logger'
|
||||
import { resolveIsWayland } from './app/ozone'
|
||||
import { installSingleInstanceGuard } from './app/single-instance'
|
||||
import { createArtistryConfig } from './configs/artistry'
|
||||
import { createGlobalAppConfig } from './configs/global'
|
||||
@@ -79,21 +80,53 @@ if (appUserDataPath) {
|
||||
// https://github.com/electron/electron/issues/41763#issuecomment-2051725363
|
||||
// https://github.com/electron/electron/issues/41763#issuecomment-3143338995
|
||||
if (isLinux) {
|
||||
app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer')
|
||||
// NOTICE:
|
||||
// All enabled features must be joined into a single comma-separated string
|
||||
// instead of calling appendSwitch('enable-features', ...) once per feature.
|
||||
// Root cause: Chromium's commandLine stores switches by key, so each
|
||||
// appendSwitch('enable-features', ...) call overwrites the previous value and
|
||||
// only the last feature survives.
|
||||
// Source: Chromium base::CommandLine behavior; see
|
||||
// https://github.com/electron/electron/issues/41763 for the WebGPU setup this supports.
|
||||
// Removal condition: never for the join itself; this block can be deleted once
|
||||
// WebGPU works on Linux Electron without manual feature switches.
|
||||
const enabledFeatures = [
|
||||
'SharedArrayBuffer',
|
||||
]
|
||||
|
||||
app.commandLine.appendSwitch('enable-unsafe-webgpu')
|
||||
app.commandLine.appendSwitch('enable-features', 'Vulkan')
|
||||
|
||||
// NOTICE: we need UseOzonePlatform, WaylandWindowDecorations for working on Wayland.
|
||||
// Partially related to https://github.com/electron/electron/issues/41551, since X11 is deprecating now,
|
||||
// we can safely remove the feature flags for Electron once they made it default supported.
|
||||
// Fixes: https://github.com/moeru-ai/airi/issues/757
|
||||
// Ref: https://github.com/mmaura/poe2linuxcompanion/blob/90664607a147ea5ccea28df6139bd95fb0ebab0e/electron/main/index.ts#L28-L46
|
||||
if (env.XDG_SESSION_TYPE === 'wayland') {
|
||||
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal')
|
||||
// Check explicit command-line switches before falling back to session environment variables.
|
||||
// When running with XWayland (e.g. '--ozone-platform=x11'), session variables like WAYLAND_DISPLAY
|
||||
// are still inherited from the Wayland desktop, but Chromium uses the explicitly specified Ozone backend.
|
||||
// Treat explicit 'auto' as an unresolved platform selection and resolve using session environment variables.
|
||||
const isWayland = resolveIsWayland({
|
||||
explicitOzonePlatform: app.commandLine.getSwitchValue('ozone-platform'),
|
||||
ozonePlatformHint: app.commandLine.getSwitchValue('ozone-platform-hint'),
|
||||
env,
|
||||
})
|
||||
|
||||
app.commandLine.appendSwitch('enable-features', 'UseOzonePlatform')
|
||||
app.commandLine.appendSwitch('enable-features', 'WaylandWindowDecorations')
|
||||
if (isWayland) {
|
||||
enabledFeatures.push('GlobalShortcutsPortal', 'UseOzonePlatform', 'WaylandWindowDecorations')
|
||||
if (!app.commandLine.hasSwitch('ozone-platform-hint')) {
|
||||
app.commandLine.appendSwitch('ozone-platform-hint', 'auto')
|
||||
}
|
||||
}
|
||||
else {
|
||||
// NOTICE:
|
||||
// Vulkan must only be enabled on non-Wayland sessions, otherwise GPU
|
||||
// initialization fails or rendering glitches appear.
|
||||
// Root cause: Vulkan is incompatible with '--ozone-platform=wayland' in
|
||||
// Chromium's surface factory; the Wayland Ozone backend cannot present
|
||||
// Vulkan surfaces.
|
||||
// Source: Chromium Ozone/Wayland surface factory; workaround tracked via
|
||||
// https://github.com/electron/electron/issues/41763 (WebGPU on Linux).
|
||||
// Removal condition: when Chromium/Electron supports Vulkan with the Wayland
|
||||
// Ozone backend, drop the isWayland guard and always push 'Vulkan'.
|
||||
enabledFeatures.push('Vulkan')
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch('enable-features', enabledFeatures.join(','))
|
||||
}
|
||||
|
||||
app.dock?.setIcon(icon)
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
electronWindowSetAlwaysOnTop,
|
||||
} from '../../../shared/eventa'
|
||||
import { onAppBeforeQuit, onAppWindowAllClosed } from '../../libs/bootkit/lifecycle'
|
||||
import { resizeWindowByDelta } from '../../windows/shared/window'
|
||||
import { resizeWindowByDelta, setWindowAlwaysOnTop } from '../../windows/shared/window'
|
||||
|
||||
export function createWindowService(params: { context: ReturnType<typeof createContext>['context'], window: BrowserWindow }) {
|
||||
function getWindowLifecycleState(reason: ElectronWindowLifecycleState['reason']): ElectronWindowLifecycleState {
|
||||
@@ -82,12 +82,7 @@ export function createWindowService(params: { context: ReturnType<typeof createC
|
||||
|
||||
defineInvokeHandler(params.context, electronWindowSetAlwaysOnTop, (flag, options) => {
|
||||
if (params.window.webContents.id === options?.raw.ipcMainEvent.sender.id) {
|
||||
if (flag) {
|
||||
params.window.setAlwaysOnTop(true, 'screen-saver', 1)
|
||||
}
|
||||
else {
|
||||
params.window.setAlwaysOnTop(false)
|
||||
}
|
||||
setWindowAlwaysOnTop(params.window, Boolean(flag))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs
|
||||
import { createConfig } from '../../libs/electron/persistence'
|
||||
import { createReusableWindow } from '../../libs/electron/window-manager'
|
||||
import { mapForBreakpoints, resolutionBreakpoints, widthFrom } from '../shared/display'
|
||||
import { protectPrivilegedWindowNavigation, setupBaseWindowElectronInvokes, transparentWindowConfig } from '../shared/window'
|
||||
import { protectPrivilegedWindowNavigation, setupBaseWindowElectronInvokes, setWindowAlwaysOnTop, transparentWindowConfig } from '../shared/window'
|
||||
|
||||
const captionConfigSchema = object({
|
||||
isFollowing: boolean(),
|
||||
@@ -118,7 +118,7 @@ function createCaptionWindow(options?: BrowserWindowConstructorOptions) {
|
||||
//
|
||||
// https://github.com/electron/electron/issues/10078#issuecomment-3410164802
|
||||
// https://stackoverflow.com/questions/39835282/set-browserwindow-always-on-top-even-other-app-is-in-fullscreen-electron-mac
|
||||
type: 'panel',
|
||||
type: isMacOS ? 'panel' : undefined,
|
||||
...transparentWindowConfig(),
|
||||
...options,
|
||||
})
|
||||
@@ -129,12 +129,12 @@ function createCaptionWindow(options?: BrowserWindowConstructorOptions) {
|
||||
//
|
||||
// https://github.com/electron/electron/issues/10078#issuecomment-3410164802
|
||||
// https://stackoverflow.com/questions/39835282/set-browserwindow-always-on-top-even-other-app-is-in-fullscreen-electron-mac
|
||||
window.setAlwaysOnTop(true, 'screen-saver', 2)
|
||||
window.setFullScreenable(false)
|
||||
window.setVisibleOnAllWorkspaces(true)
|
||||
if (isMacOS) {
|
||||
window.setFullScreenable(false)
|
||||
window.setWindowButtonVisibility(false)
|
||||
}
|
||||
setWindowAlwaysOnTop(window, true, 2)
|
||||
|
||||
window.on('ready-to-show', () => window.show())
|
||||
protectPrivilegedWindowNavigation(window)
|
||||
|
||||
@@ -31,7 +31,7 @@ import { electronStartDraggingWindow } from '../../../shared/eventa'
|
||||
import { onAppBeforeQuit } from '../../libs/bootkit/lifecycle'
|
||||
import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location'
|
||||
import { createConfig } from '../../libs/electron/persistence'
|
||||
import { protectPrivilegedWindowNavigation, transparentWindowConfig } from '../shared'
|
||||
import { protectPrivilegedWindowNavigation, setWindowAlwaysOnTop, transparentWindowConfig } from '../shared'
|
||||
import { setupMainWindowElectronInvokes } from './rpc/index.electron'
|
||||
|
||||
const appConfigSchema = object({
|
||||
@@ -91,7 +91,7 @@ export async function setupMainWindow(params: {
|
||||
//
|
||||
// https://github.com/electron/electron/issues/10078#issuecomment-3410164802
|
||||
// https://stackoverflow.com/questions/39835282/set-browserwindow-always-on-top-even-other-app-is-in-fullscreen-electron-mac
|
||||
type: 'panel',
|
||||
type: isMacOS ? 'panel' : undefined,
|
||||
...transparentWindowConfig(),
|
||||
})
|
||||
|
||||
@@ -161,12 +161,12 @@ export async function setupMainWindow(params: {
|
||||
//
|
||||
// https://github.com/electron/electron/issues/10078#issuecomment-3410164802
|
||||
// https://stackoverflow.com/questions/39835282/set-browserwindow-always-on-top-even-other-app-is-in-fullscreen-electron-mac
|
||||
window.setAlwaysOnTop(true, 'screen-saver', 1)
|
||||
window.setFullScreenable(false)
|
||||
window.setVisibleOnAllWorkspaces(true)
|
||||
if (isMacOS) {
|
||||
window.setFullScreenable(false)
|
||||
window.setWindowButtonVisibility(false)
|
||||
}
|
||||
setWindowAlwaysOnTop(window, true)
|
||||
|
||||
window.on('ready-to-show', () => window!.show())
|
||||
protectPrivilegedWindowNavigation(window)
|
||||
|
||||
@@ -41,6 +41,22 @@ describe('mapForBreakpoints', () => {
|
||||
const val2 = mapForBreakpoints(2000, { 'sm': 100, 'md': 200, '2xl': 500 }) // expected to be lg
|
||||
expect(val2).toBe(500)
|
||||
})
|
||||
|
||||
it('should fall back to the breakpoint with the smallest minimum width when below all breakpoints', () => {
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// When `basedOn` is below every supplied breakpoint (e.g. display height 500
|
||||
// with sm/md/lg sizes), no breakpoint matches and the previous fallback was
|
||||
// `Object.values(sizes)[0]`, which depends on object key insertion order.
|
||||
// Sorting the sizes map (e.g. sm/md/lg -> lg/md/sm by a lint rule) changed
|
||||
// the fallback from the sm formula to the lg formula and moved the inlay on
|
||||
// small displays.
|
||||
//
|
||||
// We fixed this by selecting the breakpoint with the smallest minimum width
|
||||
// explicitly, so the result is stable regardless of key order.
|
||||
expect(mapForBreakpoints(500, { sm: 100, md: 200, lg: 300 })).toBe(100)
|
||||
expect(mapForBreakpoints(500, { lg: 300, md: 200, sm: 100 })).toBe(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe('widthFrom', () => {
|
||||
|
||||
@@ -220,7 +220,7 @@ export function mapForBreakpoints<
|
||||
basedOn: number,
|
||||
sizes: { [key in keyof B]?: number } | number,
|
||||
options?: { breakpoints: B },
|
||||
) {
|
||||
): number {
|
||||
if (typeof sizes === 'number') {
|
||||
return sizes
|
||||
}
|
||||
@@ -244,8 +244,16 @@ export function mapForBreakpoints<
|
||||
.sort((a, b) => b.min - a.min) // Sort descending by min width
|
||||
|
||||
const fallback = sortedSizes.find(s => s.min <= basedOn)
|
||||
if (fallback?.value != null) {
|
||||
return fallback.value
|
||||
}
|
||||
|
||||
return fallback?.value ?? Object.values(sizes)?.[0] ?? 0
|
||||
// `basedOn` is below every supplied breakpoint (e.g. height < 640 with sm/md/lg
|
||||
// sizes): use the breakpoint with the smallest minimum width. Selecting by min
|
||||
// instead of `Object.values(sizes)[0]` keeps the result stable when the sizes
|
||||
// object keys are reordered (e.g. by lint sorting rules).
|
||||
const smallest = sortedSizes[sortedSizes.length - 1]
|
||||
return smallest?.value ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { protectPrivilegedWindowNavigation, toggleWindowShow, transparentWindowConfig } from './window'
|
||||
export { protectPrivilegedWindowNavigation, setWindowAlwaysOnTop, toggleWindowShow, transparentWindowConfig } from './window'
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { setWindowAlwaysOnTop } from './window'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
isMacOS: false,
|
||||
isWindows: false,
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
shell: {
|
||||
openExternal: vi.fn(),
|
||||
},
|
||||
BrowserWindow: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('std-env', () => ({
|
||||
get isMacOS() {
|
||||
return mocks.isMacOS
|
||||
},
|
||||
get isWindows() {
|
||||
return mocks.isWindows
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../services/electron', () => ({
|
||||
createAppService: vi.fn(),
|
||||
createPowerMonitorService: vi.fn(),
|
||||
createScreenService: vi.fn(),
|
||||
createSystemPreferencesService: vi.fn(),
|
||||
createWindowService: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('setWindowAlwaysOnTop', () => {
|
||||
it('disables always-on-top when flag is false', () => {
|
||||
const window = {
|
||||
setAlwaysOnTop: vi.fn(),
|
||||
}
|
||||
|
||||
setWindowAlwaysOnTop(window, false)
|
||||
|
||||
expect(window.setAlwaysOnTop).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('applies standard always-on-top on Linux', () => {
|
||||
mocks.isMacOS = false
|
||||
mocks.isWindows = false
|
||||
|
||||
const window = {
|
||||
setAlwaysOnTop: vi.fn(),
|
||||
}
|
||||
|
||||
setWindowAlwaysOnTop(window, true)
|
||||
|
||||
expect(window.setAlwaysOnTop).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('applies screen-saver level and relative offset on macOS', () => {
|
||||
mocks.isMacOS = true
|
||||
mocks.isWindows = false
|
||||
|
||||
const window = {
|
||||
setAlwaysOnTop: vi.fn(),
|
||||
}
|
||||
|
||||
setWindowAlwaysOnTop(window, true, 1)
|
||||
|
||||
expect(window.setAlwaysOnTop).toHaveBeenCalledWith(true, 'screen-saver', 1)
|
||||
})
|
||||
|
||||
it('applies screen-saver level and relative offset on Windows', () => {
|
||||
mocks.isMacOS = false
|
||||
mocks.isWindows = true
|
||||
|
||||
const window = {
|
||||
setAlwaysOnTop: vi.fn(),
|
||||
}
|
||||
|
||||
setWindowAlwaysOnTop(window, true, 2)
|
||||
|
||||
expect(window.setAlwaysOnTop).toHaveBeenCalledWith(true, 'screen-saver', 2)
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,7 @@ import type { ServerChannel } from '../../services/airi/channel-server'
|
||||
|
||||
import { isRendererUnavailable } from '@proj-airi/electron-vueuse/main'
|
||||
import { shell } from 'electron'
|
||||
import { isMacOS } from 'std-env'
|
||||
import { isMacOS, isWindows } from 'std-env'
|
||||
|
||||
import { createServerChannelService } from '../../services/airi/channel-server'
|
||||
import { createI18nService } from '../../services/airi/i18n'
|
||||
@@ -92,6 +92,30 @@ export function spotlightLikeWindowConfig(): BrowserWindowConstructorOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the window always-on-top level according to the host platform.
|
||||
*
|
||||
* macOS and Windows support the screen-saver level and relative level layering,
|
||||
* while Linux (X11/Wayland) works reliably with standard always-on-top.
|
||||
*/
|
||||
export function setWindowAlwaysOnTop(
|
||||
window: Pick<BrowserWindow, 'setAlwaysOnTop'>,
|
||||
flag: boolean,
|
||||
relativeLevel = 1,
|
||||
): void {
|
||||
if (!flag) {
|
||||
window.setAlwaysOnTop(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (isMacOS || isWindows) {
|
||||
window.setAlwaysOnTop(true, 'screen-saver', relativeLevel)
|
||||
return
|
||||
}
|
||||
|
||||
window.setAlwaysOnTop(true)
|
||||
}
|
||||
|
||||
export function resizeWindowByDelta(params: {
|
||||
window: BrowserWindow
|
||||
deltaX: number
|
||||
|
||||
@@ -27,7 +27,7 @@ import { normalizeWidgetWindowSize } from '../../../shared/utils/electron/window
|
||||
import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location'
|
||||
import { createConfig } from '../../libs/electron/persistence'
|
||||
import { createReusableWindow } from '../../libs/electron/window-manager'
|
||||
import { protectPrivilegedWindowNavigation, spotlightLikeWindowConfig, transparentWindowConfig } from '../shared/window'
|
||||
import { protectPrivilegedWindowNavigation, setWindowAlwaysOnTop, spotlightLikeWindowConfig, transparentWindowConfig } from '../shared/window'
|
||||
import { createWidgetIframeRequestCoordinator } from './iframe-request-coordinator'
|
||||
import { setupWidgetsWindowInvokes } from './rpc/index.electron'
|
||||
|
||||
@@ -231,7 +231,7 @@ function createWidgetsWindow() {
|
||||
sandbox: false,
|
||||
},
|
||||
// Top-level overlay style like other overlay windows
|
||||
type: 'panel',
|
||||
type: isMacOS ? 'panel' : undefined,
|
||||
...transparentWindowConfig(),
|
||||
...spotlightLikeWindowConfig(),
|
||||
})
|
||||
@@ -247,15 +247,6 @@ function createWidgetsWindow() {
|
||||
return window
|
||||
}
|
||||
|
||||
function applyAlwaysOnTop(window: BrowserWindow, enabled: boolean) {
|
||||
if (enabled) {
|
||||
window.setAlwaysOnTop(true, 'screen-saver', 1)
|
||||
return
|
||||
}
|
||||
|
||||
window.setAlwaysOnTop(false)
|
||||
}
|
||||
|
||||
interface WidgetRecord extends WidgetSnapshot {
|
||||
timer?: ReturnType<typeof setTimeout>
|
||||
}
|
||||
@@ -512,7 +503,7 @@ export function setupWidgetsWindowManager(params: {
|
||||
const window = await getWindowFromContext(context)
|
||||
pendingRoute = undefined
|
||||
applyWindowLayout(window, snapshot)
|
||||
applyAlwaysOnTop(window, snapshot?.alwaysOnTop ?? false)
|
||||
setWindowAlwaysOnTop(window, snapshot?.alwaysOnTop ?? false)
|
||||
if (currentRoute !== route)
|
||||
await loadWithRoute(window, route)
|
||||
window.show()
|
||||
@@ -623,7 +614,7 @@ export function setupWidgetsWindowManager(params: {
|
||||
const window = context?.window
|
||||
if (window && !window.isDestroyed()) {
|
||||
applyWindowLayout(window, nextSnapshot)
|
||||
applyAlwaysOnTop(window, nextSnapshot.alwaysOnTop)
|
||||
setWindowAlwaysOnTop(window, nextSnapshot.alwaysOnTop)
|
||||
}
|
||||
|
||||
eventaContext?.emit(widgetsUpdateEvent, {
|
||||
|
||||
@@ -21,6 +21,8 @@ export default defineConfig({
|
||||
'apps/stage-tamagotchi/src/bindings/**',
|
||||
'apps/stage-tamagotchi-electron/out/**',
|
||||
'apps/stage-tamagotchi-electron/src/renderer/bindings/**',
|
||||
'**/flatpak/**',
|
||||
'**/flatpak-repo/**',
|
||||
'apps/stage-pocket/ios/**',
|
||||
'apps/stage-pocket/android/**',
|
||||
'**/drizzle/**',
|
||||
|
||||
@@ -98,6 +98,13 @@ export async function placeBlock(
|
||||
west: new Vec3(-1, 0, 0),
|
||||
}
|
||||
|
||||
// Fallback search priority when the requested face is unavailable. Kept as an
|
||||
// explicit array because the first fallback decides which face the bot attaches
|
||||
// to, which can change the placed block's orientation; relying on
|
||||
// `Object.values(dirMap)` would silently change this priority if the dirMap
|
||||
// keys are ever reordered (e.g. by sorting).
|
||||
const fallbackDirs = [dirMap.top, dirMap.bottom, dirMap.north, dirMap.south, dirMap.east, dirMap.west]
|
||||
|
||||
const dirs: Vec3[] = []
|
||||
if (placeOn === 'side') {
|
||||
dirs.push(dirMap.north, dirMap.south, dirMap.east, dirMap.west)
|
||||
@@ -111,7 +118,7 @@ export async function placeBlock(
|
||||
}
|
||||
|
||||
// Add remaining directions
|
||||
dirs.push(...Object.values(dirMap).filter(d => !dirs.includes(d)))
|
||||
dirs.push(...fallbackDirs.filter(d => !dirs.includes(d)))
|
||||
|
||||
let buildOffBlock: Block | null = null
|
||||
let faceVec: Vec3 | null = null
|
||||
|
||||
@@ -287,6 +287,13 @@ function findPlacementSpot(mineflayer: Mineflayer, targetDest: Vec3, placeOn: Bl
|
||||
}
|
||||
|
||||
function getPlacementDirections(placeOn: BlockFace, dirMap: Record<string, Vec3>): Vec3[] {
|
||||
// Fallback search priority when the requested face is unavailable. Kept as an
|
||||
// explicit array because the first fallback decides which face the bot attaches
|
||||
// to, which can change the placed block's orientation; relying on
|
||||
// `Object.values(dirMap)` would silently change this priority if the dirMap
|
||||
// keys are ever reordered (e.g. by sorting).
|
||||
const fallbackDirs = [dirMap.top, dirMap.bottom, dirMap.north, dirMap.south, dirMap.east, dirMap.west]
|
||||
|
||||
const directions: Vec3[] = []
|
||||
if (placeOn === 'side') {
|
||||
directions.push(dirMap.north, dirMap.south, dirMap.east, dirMap.west)
|
||||
@@ -298,7 +305,7 @@ function getPlacementDirections(placeOn: BlockFace, dirMap: Record<string, Vec3>
|
||||
directions.push(dirMap.bottom)
|
||||
}
|
||||
|
||||
directions.push(...Object.values(dirMap).filter(d => !directions.includes(d)))
|
||||
directions.push(...fallbackDirs.filter(d => !directions.includes(d)))
|
||||
return directions
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user