Compare commits
2 Commits
17b5068644
...
c48b8d72da
| Author | SHA1 | Date | |
|---|---|---|---|
| c48b8d72da | |||
| 5c1ea30e5d |
@ -25,7 +25,6 @@ InstallDirRegKey HKLM "Software\Tom Butcher\Farm Control" "InstallDir"
|
||||
RequestExecutionLevel admin
|
||||
|
||||
!define MUI_ABORTWARNING
|
||||
!define MUI_BRANDINGTEXT "Farm Control v${VERSION}-b${BUILD_NUMBER} Installer"
|
||||
|
||||
!ifndef INSTALLER_ICON
|
||||
!define INSTALLER_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico"
|
||||
@ -43,6 +42,8 @@ UninstallIcon "${INSTALLER_ICON}"
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
!define MUI_BRANDINGTEXT "Farm Control v${VERSION}-b${BUILD_NUMBER} Installer"
|
||||
|
||||
Function .onInit
|
||||
${If} ${Silent}
|
||||
SetAutoClose true
|
||||
|
||||
@ -28,15 +28,40 @@
|
||||
done_uninstall:
|
||||
!macroend
|
||||
|
||||
!macro fixShortcutWorkingDir SHORTCUT_PATH WORKING_DIR
|
||||
Push $0
|
||||
Push $1
|
||||
Push $2
|
||||
StrCpy $1 "${SHORTCUT_PATH}"
|
||||
StrCpy $2 "${WORKING_DIR}"
|
||||
InitPluginsDir
|
||||
FileOpen $0 "$PLUGINSDIR\fix-shortcut.ps1" w
|
||||
FileWrite $0 '$$s = (New-Object -COM WScript.Shell).CreateShortcut("'
|
||||
FileWrite $0 $1
|
||||
FileWrite $0 '")$\r$\n'
|
||||
FileWrite $0 '$$s.WorkingDirectory = "'
|
||||
FileWrite $0 $2
|
||||
FileWrite $0 '"$\r$\n'
|
||||
FileWrite $0 '$$s.Save()$\r$\n'
|
||||
FileClose $0
|
||||
ExecWait '"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "$PLUGINSDIR\fix-shortcut.ps1"'
|
||||
Delete "$PLUGINSDIR\fix-shortcut.ps1"
|
||||
Pop $2
|
||||
Pop $1
|
||||
Pop $0
|
||||
!macroend
|
||||
|
||||
!macro createDesktopShortcut
|
||||
SetShellVarContext all
|
||||
CreateShortCut "$DESKTOP\Farm Control.lnk" "$INSTDIR\bin\launcher.exe"
|
||||
CreateShortCut "$DESKTOP\Farm Control.lnk" "$INSTDIR\bin\launcher.exe" "" "$INSTDIR\bin\launcher.exe" 0 SW_SHOWNORMAL "" "Farm Control"
|
||||
!insertmacro fixShortcutWorkingDir "$DESKTOP\Farm Control.lnk" "$INSTDIR\bin"
|
||||
!macroend
|
||||
|
||||
!macro createStartMenuShortcut
|
||||
SetShellVarContext all
|
||||
CreateDirectory "$SMPROGRAMS\Farm Control"
|
||||
CreateShortCut "$SMPROGRAMS\Farm Control\Farm Control.lnk" "$INSTDIR\bin\launcher.exe"
|
||||
CreateShortCut "$SMPROGRAMS\Farm Control\Farm Control.lnk" "$INSTDIR\bin\launcher.exe" "" "$INSTDIR\bin\launcher.exe" 0 SW_SHOWNORMAL "" "Farm Control"
|
||||
!insertmacro fixShortcutWorkingDir "$SMPROGRAMS\Farm Control\Farm Control.lnk" "$INSTDIR\bin"
|
||||
!macroend
|
||||
|
||||
!macro removeDesktopShortcut
|
||||
|
||||
@ -277,6 +277,38 @@ if (existsSync(rceditSrc)) {
|
||||
cpSync(rceditSrc, rceditDest, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
function patchNativeWrapperPath(nativeTsPath) {
|
||||
if (!existsSync(nativeTsPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
let source = readFileSync(nativeTsPath, 'utf8')
|
||||
const needle =
|
||||
'const nativeWrapperPath = join(process.cwd(), `libNativeWrapper.${suffix}`);'
|
||||
const replacement =
|
||||
'const nativeWrapperPath = join(dirname(process.execPath), `libNativeWrapper.${suffix}`);'
|
||||
|
||||
if (!source.includes(needle)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!source.includes('dirname')) {
|
||||
source = source.replace(
|
||||
'import { join } from "path";',
|
||||
'import { dirname, join } from "path";'
|
||||
)
|
||||
}
|
||||
|
||||
source = source.replace(needle, replacement)
|
||||
writeFileSync(nativeTsPath, source)
|
||||
}
|
||||
|
||||
for (const distDir of ['dist', 'dist-macos-arm64', 'dist-win-x64']) {
|
||||
patchNativeWrapperPath(
|
||||
path.join(electrobunDir, distDir, 'api/bun/proc/native.ts')
|
||||
)
|
||||
}
|
||||
|
||||
const markerPath = path.join(electrobunDir, '.farmcontrol-electrobun-patched')
|
||||
writeFileSync(markerPath, `patched-at=${new Date().toISOString()}\n`)
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ if (!existsSync(launcherPath)) {
|
||||
}
|
||||
|
||||
const child = spawn(launcherPath, [], {
|
||||
cwd: dirname(launcherPath),
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true
|
||||
|
||||
@ -1,9 +1,19 @@
|
||||
import { chdirSync } from 'node:fs'
|
||||
import { dirname } from 'node:path'
|
||||
import {
|
||||
captureLaunchUrl,
|
||||
closeSingleInstanceServer,
|
||||
ensureSingleInstanceLock
|
||||
} from '../desktop/single-instance.js'
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
chdirSync(dirname(process.execPath))
|
||||
} catch {
|
||||
// Best effort; some launchers may not allow changing directory.
|
||||
}
|
||||
}
|
||||
|
||||
const launchUrl = captureLaunchUrl()
|
||||
const gotSingleInstanceLock = await ensureSingleInstanceLock({ launchUrl })
|
||||
|
||||
|
||||
@ -5,6 +5,10 @@ import {
|
||||
MAC_TRAFFIC_LIGHT_OFFSET
|
||||
} from './macos-window-effects.js'
|
||||
import { sendToRenderer, setMessageSender } from './notify.js'
|
||||
import {
|
||||
clampWindowToWorkArea,
|
||||
isWindowWorkAreaMaximized
|
||||
} from './windows-work-area.js'
|
||||
|
||||
const isMacOS = process.platform === 'darwin'
|
||||
const isWindows = process.platform === 'win32'
|
||||
@ -167,12 +171,20 @@ function syncWindowsWebviewLayout(window) {
|
||||
window.setSize(width, height)
|
||||
}
|
||||
|
||||
function handleWindowsWindowChange(window) {
|
||||
if (clampWindowToWorkArea(window)) {
|
||||
syncWindowsWebviewLayout(window)
|
||||
}
|
||||
|
||||
broadcastWindowState()
|
||||
}
|
||||
|
||||
function applyWindowsStartupWindowState(window) {
|
||||
if (!window) return
|
||||
|
||||
setTimeout(() => {
|
||||
window.maximize?.()
|
||||
syncWindowsWebviewLayout(window)
|
||||
handleWindowsWindowChange(window)
|
||||
setTimeout(() => syncWindowsWebviewLayout(window), 0)
|
||||
setTimeout(() => syncWindowsWebviewLayout(window), 100)
|
||||
setTimeout(broadcastWindowState, 100)
|
||||
@ -181,9 +193,13 @@ function applyWindowsStartupWindowState(window) {
|
||||
|
||||
function setupWindowEvents(window) {
|
||||
// Electrobun emits resize/focus, not Electron's maximize/fullscreen events.
|
||||
window.on?.('resize', broadcastWindowState)
|
||||
const onWindowChange = isWindows
|
||||
? () => handleWindowsWindowChange(window)
|
||||
: broadcastWindowState
|
||||
|
||||
window.on?.('resize', onWindowChange)
|
||||
window.on?.('focus', broadcastWindowState)
|
||||
window.on?.('move', broadcastWindowState)
|
||||
window.on?.('move', onWindowChange)
|
||||
}
|
||||
|
||||
export function setupMainWindowMessaging(window = mainWindow) {
|
||||
@ -314,7 +330,9 @@ export function getWindowState() {
|
||||
|
||||
return {
|
||||
isFullScreen: mainWindow.isFullScreen?.() ?? false,
|
||||
isMaximized: mainWindow.isMaximized?.() ?? false
|
||||
isMaximized: isWindows
|
||||
? isWindowWorkAreaMaximized(mainWindow)
|
||||
: (mainWindow.isMaximized?.() ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,6 +349,10 @@ export function handleWindowControl(action) {
|
||||
} else {
|
||||
mainWindow.maximize?.()
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
handleWindowsWindowChange(mainWindow)
|
||||
}
|
||||
break
|
||||
case 'fullscreen':
|
||||
if (mainWindow.isFullScreen?.()) {
|
||||
|
||||
83
src/desktop/windows-work-area.js
Normal file
83
src/desktop/windows-work-area.js
Normal file
@ -0,0 +1,83 @@
|
||||
import { Screen } from 'electrobun/bun'
|
||||
|
||||
const FRAME_TOLERANCE_PX = 4
|
||||
|
||||
function framesMatch(a, b, tolerance = FRAME_TOLERANCE_PX) {
|
||||
return (
|
||||
Math.abs(a.x - b.x) <= tolerance &&
|
||||
Math.abs(a.y - b.y) <= tolerance &&
|
||||
Math.abs(a.width - b.width) <= tolerance &&
|
||||
Math.abs(a.height - b.height) <= tolerance
|
||||
)
|
||||
}
|
||||
|
||||
function getFrameCenter(frame) {
|
||||
return {
|
||||
x: frame.x + frame.width / 2,
|
||||
y: frame.y + frame.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
function getDisplayForFrame(frame) {
|
||||
const { x, y } = getFrameCenter(frame)
|
||||
const displays = Screen.getAllDisplays()
|
||||
|
||||
const match = displays.find(({ bounds }) => {
|
||||
return (
|
||||
x >= bounds.x &&
|
||||
x < bounds.x + bounds.width &&
|
||||
y >= bounds.y &&
|
||||
y < bounds.y + bounds.height
|
||||
)
|
||||
})
|
||||
|
||||
return match ?? Screen.getPrimaryDisplay()
|
||||
}
|
||||
|
||||
function frameCoversMonitor(frame, bounds) {
|
||||
return (
|
||||
frame.x <= bounds.x + FRAME_TOLERANCE_PX &&
|
||||
frame.y <= bounds.y + FRAME_TOLERANCE_PX &&
|
||||
frame.x + frame.width >= bounds.x + bounds.width - FRAME_TOLERANCE_PX &&
|
||||
frame.y + frame.height >= bounds.y + bounds.height - FRAME_TOLERANCE_PX
|
||||
)
|
||||
}
|
||||
|
||||
export function isWindowWorkAreaMaximized(window) {
|
||||
if (!window?.getFrame) {
|
||||
return false
|
||||
}
|
||||
|
||||
const frame = window.getFrame()
|
||||
const { workArea } = getDisplayForFrame(frame)
|
||||
return framesMatch(frame, workArea)
|
||||
}
|
||||
|
||||
export function clampWindowToWorkArea(window) {
|
||||
if (!window?.getFrame || !window?.setFrame) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (window.isFullScreen?.()) {
|
||||
return false
|
||||
}
|
||||
|
||||
const frame = window.getFrame()
|
||||
if (!frame.width || !frame.height) {
|
||||
return false
|
||||
}
|
||||
|
||||
const display = getDisplayForFrame(frame)
|
||||
const { bounds, workArea } = display
|
||||
|
||||
if (framesMatch(frame, workArea)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!frameCoversMonitor(frame, bounds)) {
|
||||
return false
|
||||
}
|
||||
|
||||
window.setFrame(workArea.x, workArea.y, workArea.width, workArea.height)
|
||||
return true
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user