Enhance macOS window effects and improve Electrobun integration
- Adjusted CSS for macOS vibrancy, refining background opacity for light and dark modes. - Introduced new styles for selected menu items in dark mode to enhance UI consistency. - Added traffic light positioning and window chrome observer in native macOS window effects for better window management. - Refactored Electron context to utilize the Electrobun bridge for improved API interactions and error handling. - Streamlined the initialization process for the Electrobun bridge in the main application entry point.
This commit is contained in:
parent
f6978f44b2
commit
805361516e
@ -125,11 +125,11 @@ html.macos-vibrancy .main-layout {
|
||||
}
|
||||
|
||||
html.macos-vibrancy .light-mode .ant-layout-sider {
|
||||
background: rgba(255, 255, 255, 0.75) !important;
|
||||
background: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
html.macos-vibrancy .dark-mode .ant-layout-sider {
|
||||
background: rgba(10, 10, 10, 0.65) !important;
|
||||
background: rgba(5, 5, 5, 0.7) !important;
|
||||
}
|
||||
|
||||
html.macos-vibrancy .light-mode .ant-layout-content {
|
||||
@ -145,6 +145,15 @@ html.macos-vibrancy .electron-sider .ant-menu-light {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
html.macos-vibrancy
|
||||
.dark-mode
|
||||
.electron-sider
|
||||
.ant-menu-light
|
||||
.ant-menu-item-selected {
|
||||
background-color: color-mix(in srgb, var(--color-primary) 30%, #00000010);
|
||||
color: color-mix(in srgb, var(--color-primary) 75%, #ffffff);
|
||||
}
|
||||
|
||||
.electron-navigation {
|
||||
line-height: 40px;
|
||||
}
|
||||
@ -153,6 +162,10 @@ html.macos-vibrancy .electron-sider .ant-menu-light {
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
.electron-navigation.ant-menu-horizontal .ant-menu-item {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.electron-sidebar .ant-menu-item,
|
||||
.electron-sidebar .ant-menu-submenu-title {
|
||||
height: 32.5px !important;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
static NSString *const kElectrobunVibrancyViewIdentifier =
|
||||
@"ElectrobunVibrancyView";
|
||||
@ -8,6 +9,10 @@ static NSString *const kElectrobunWindowBorderViewIdentifier =
|
||||
@"ElectrobunWindowBorderView";
|
||||
|
||||
static CGFloat gWindowCornerRadius = 15.0;
|
||||
static CGFloat gTrafficLightsX = 16.0;
|
||||
static CGFloat gTrafficLightsY = 12.0;
|
||||
|
||||
static const void *kElectrobunChromeObserverKey = &kElectrobunChromeObserverKey;
|
||||
|
||||
@interface ElectrobunWindowBorderView : NSView
|
||||
@property(nonatomic) CGFloat cornerRadius;
|
||||
@ -94,6 +99,58 @@ static CGFloat gWindowCornerRadius = 15.0;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface ElectrobunWindowChromeObserver : NSObject
|
||||
@property(nonatomic, weak) NSWindow *window;
|
||||
@end
|
||||
|
||||
static bool applyTrafficLightsPosition(NSWindow *window, CGFloat x,
|
||||
CGFloat yFromTop);
|
||||
static void refreshWindowChrome(NSWindow *window);
|
||||
|
||||
@implementation ElectrobunWindowChromeObserver
|
||||
|
||||
- (instancetype)initWithWindow:(NSWindow *)window {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_window = window;
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
[center addObserver:self
|
||||
selector:@selector(refreshChrome)
|
||||
name:NSWindowDidResizeNotification
|
||||
object:window];
|
||||
[center addObserver:self
|
||||
selector:@selector(refreshChrome)
|
||||
name:NSWindowDidEndLiveResizeNotification
|
||||
object:window];
|
||||
[center addObserver:self
|
||||
selector:@selector(refreshChrome)
|
||||
name:NSWindowDidEnterFullScreenNotification
|
||||
object:window];
|
||||
[center addObserver:self
|
||||
selector:@selector(refreshChrome)
|
||||
name:NSWindowDidExitFullScreenNotification
|
||||
object:window];
|
||||
[center addObserver:self
|
||||
selector:@selector(refreshChrome)
|
||||
name:NSWindowDidBecomeKeyNotification
|
||||
object:window];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)refreshChrome {
|
||||
if (self.window != nil) {
|
||||
refreshWindowChrome(self.window);
|
||||
applyTrafficLightsPosition(self.window, gTrafficLightsX, gTrafficLightsY);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static NSVisualEffectView *findVibrancyView(NSView *contentView) {
|
||||
for (NSView *subview in [contentView subviews]) {
|
||||
if ([subview isKindOfClass:[NSVisualEffectView class]] &&
|
||||
@ -130,8 +187,13 @@ static ElectrobunWindowBorderView *findWindowBorderView(NSView *contentView) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
static void applyWindowCornerRadius(NSWindow *window, CGFloat radius) {
|
||||
gWindowCornerRadius = MAX(0.0, radius);
|
||||
static BOOL isWindowFullScreen(NSWindow *window) {
|
||||
return (window.styleMask & NSWindowStyleMaskFullScreen) != 0;
|
||||
}
|
||||
|
||||
static void refreshWindowChrome(NSWindow *window) {
|
||||
BOOL fullScreen = isWindowFullScreen(window);
|
||||
CGFloat radius = fullScreen ? 0.0 : gWindowCornerRadius;
|
||||
|
||||
NSView *contentView = [window contentView];
|
||||
if (contentView == nil) {
|
||||
@ -139,22 +201,30 @@ static void applyWindowCornerRadius(NSWindow *window, CGFloat radius) {
|
||||
}
|
||||
|
||||
contentView.wantsLayer = YES;
|
||||
contentView.layer.cornerRadius = gWindowCornerRadius;
|
||||
contentView.layer.cornerRadius = radius;
|
||||
contentView.layer.masksToBounds = YES;
|
||||
|
||||
NSVisualEffectView *effectView = findVibrancyView(contentView);
|
||||
if (effectView != nil) {
|
||||
effectView.wantsLayer = YES;
|
||||
effectView.layer.cornerRadius = gWindowCornerRadius;
|
||||
effectView.layer.cornerRadius = radius;
|
||||
effectView.layer.masksToBounds = YES;
|
||||
}
|
||||
|
||||
ElectrobunWindowBorderView *borderView = findWindowBorderView(contentView);
|
||||
if (borderView != nil) {
|
||||
borderView.hidden = fullScreen;
|
||||
if (!fullScreen) {
|
||||
borderView.cornerRadius = gWindowCornerRadius;
|
||||
[borderView setNeedsDisplay:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void applyWindowCornerRadius(NSWindow *window, CGFloat radius) {
|
||||
gWindowCornerRadius = MAX(0.0, radius);
|
||||
refreshWindowChrome(window);
|
||||
}
|
||||
|
||||
static void ensureWindowBorder(NSWindow *window) {
|
||||
NSView *contentView = [window contentView];
|
||||
@ -171,7 +241,6 @@ static void ensureWindowBorder(NSWindow *window) {
|
||||
setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
|
||||
}
|
||||
|
||||
borderView.cornerRadius = gWindowCornerRadius;
|
||||
[borderView setFrame:[contentView bounds]];
|
||||
|
||||
if ([borderView superview] == nil) {
|
||||
@ -185,7 +254,76 @@ static void ensureWindowBorder(NSWindow *window) {
|
||||
relativeTo:nil];
|
||||
}
|
||||
|
||||
[borderView setNeedsDisplay:YES];
|
||||
refreshWindowChrome(window);
|
||||
}
|
||||
|
||||
static bool applyTrafficLightsPosition(NSWindow *window, CGFloat x,
|
||||
CGFloat yFromTop) {
|
||||
NSButton *closeButton = [window standardWindowButton:NSWindowCloseButton];
|
||||
NSButton *minimizeButton =
|
||||
[window standardWindowButton:NSWindowMiniaturizeButton];
|
||||
NSButton *zoomButton = [window standardWindowButton:NSWindowZoomButton];
|
||||
|
||||
if (closeButton == nil || minimizeButton == nil || zoomButton == nil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NSView *buttonContainer = [closeButton superview];
|
||||
if (buttonContainer == nil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CGFloat spacing = NSMinX(minimizeButton.frame) - NSMinX(closeButton.frame);
|
||||
if (spacing <= 0) {
|
||||
spacing = closeButton.frame.size.width + 6.0;
|
||||
}
|
||||
|
||||
BOOL flipped = [buttonContainer isFlipped];
|
||||
CGFloat targetY = yFromTop;
|
||||
if (!flipped) {
|
||||
targetY = buttonContainer.frame.size.height - yFromTop -
|
||||
closeButton.frame.size.height;
|
||||
}
|
||||
targetY = MAX(0.0, targetY);
|
||||
|
||||
CGFloat currentX = x;
|
||||
NSArray *buttons = @[ closeButton, minimizeButton, zoomButton ];
|
||||
for (NSButton *button in buttons) {
|
||||
[button setAutoresizingMask:NSViewNotSizable];
|
||||
[button setFrameOrigin:NSMakePoint(currentX, targetY)];
|
||||
currentX += spacing;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ensureTrafficLightsObserver(NSWindow *window) {
|
||||
ElectrobunWindowChromeObserver *existingObserver =
|
||||
objc_getAssociatedObject(window, kElectrobunChromeObserverKey);
|
||||
if (existingObserver != nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
ElectrobunWindowChromeObserver *observer =
|
||||
[[ElectrobunWindowChromeObserver alloc] initWithWindow:window];
|
||||
objc_setAssociatedObject(window, kElectrobunChromeObserverKey, observer,
|
||||
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
static void scheduleTrafficLightsPosition(NSWindow *window, NSInteger attempt) {
|
||||
if (applyTrafficLightsPosition(window, gTrafficLightsX, gTrafficLightsY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempt >= 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_after(
|
||||
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(50 * NSEC_PER_MSEC)),
|
||||
dispatch_get_main_queue(), ^{
|
||||
scheduleTrafficLightsPosition(window, attempt + 1);
|
||||
});
|
||||
}
|
||||
|
||||
extern "C" bool enableWindowVibrancy(void *windowPtr, double cornerRadius) {
|
||||
@ -223,7 +361,7 @@ extern "C" bool enableWindowVibrancy(void *windowPtr, double cornerRadius) {
|
||||
}
|
||||
|
||||
if (@available(macOS 10.14, *)) {
|
||||
[effectView setMaterial:NSVisualEffectMaterialUnderWindowBackground];
|
||||
[effectView setMaterial:NSVisualEffectMaterialHUDWindow];
|
||||
} else {
|
||||
[effectView setMaterial:NSVisualEffectMaterialSidebar];
|
||||
}
|
||||
@ -243,6 +381,8 @@ extern "C" bool enableWindowVibrancy(void *windowPtr, double cornerRadius) {
|
||||
|
||||
ensureWindowBorder(window);
|
||||
applyWindowCornerRadius(window, gWindowCornerRadius);
|
||||
ensureTrafficLightsObserver(window);
|
||||
scheduleTrafficLightsPosition(window, 0);
|
||||
|
||||
[window invalidateShadow];
|
||||
success = YES;
|
||||
@ -307,45 +447,13 @@ extern "C" bool setWindowTrafficLightsPosition(void *windowPtr, double x,
|
||||
return;
|
||||
}
|
||||
|
||||
NSButton *closeButton =
|
||||
[window standardWindowButton:NSWindowCloseButton];
|
||||
NSButton *minimizeButton =
|
||||
[window standardWindowButton:NSWindowMiniaturizeButton];
|
||||
NSButton *zoomButton = [window standardWindowButton:NSWindowZoomButton];
|
||||
|
||||
if (closeButton == nil || minimizeButton == nil || zoomButton == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSView *buttonContainer = [closeButton superview];
|
||||
if (buttonContainer == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGFloat spacing = NSMinX(minimizeButton.frame) - NSMinX(closeButton.frame);
|
||||
if (spacing <= 0) {
|
||||
spacing = closeButton.frame.size.width + 6.0;
|
||||
}
|
||||
|
||||
BOOL flipped = [buttonContainer isFlipped];
|
||||
CGFloat targetY = yFromTop;
|
||||
if (!flipped) {
|
||||
targetY = buttonContainer.frame.size.height - yFromTop -
|
||||
closeButton.frame.size.height;
|
||||
}
|
||||
targetY = MAX(0.0, targetY);
|
||||
|
||||
CGFloat currentX = x;
|
||||
NSArray *buttons = @[ closeButton, minimizeButton, zoomButton ];
|
||||
for (NSButton *button in buttons) {
|
||||
[button setFrameOrigin:NSMakePoint(currentX, targetY)];
|
||||
currentX += spacing;
|
||||
}
|
||||
|
||||
[buttonContainer setNeedsLayout:YES];
|
||||
[buttonContainer layoutSubtreeIfNeeded];
|
||||
gTrafficLightsX = (CGFloat)x;
|
||||
gTrafficLightsY = (CGFloat)yFromTop;
|
||||
ensureTrafficLightsObserver(window);
|
||||
success = applyTrafficLightsPosition(window, gTrafficLightsX, gTrafficLightsY);
|
||||
if (success) {
|
||||
[window invalidateShadow];
|
||||
success = YES;
|
||||
}
|
||||
});
|
||||
|
||||
return success;
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
import { createAppRpc } from "../desktop/rpc.js";
|
||||
import { registerGlobalShortcuts, unregisterGlobalShortcuts } from "../desktop/spotlight.js";
|
||||
import { createAppRpc } from '../desktop/rpc.js'
|
||||
import {
|
||||
registerGlobalShortcuts,
|
||||
unregisterGlobalShortcuts
|
||||
} from '../desktop/spotlight.js'
|
||||
import {
|
||||
createMainWindow,
|
||||
handleDeepLinkFromArgv,
|
||||
setupDevAuthServer,
|
||||
setupNavigationGestures,
|
||||
} from "../desktop/window.js";
|
||||
setupNavigationGestures
|
||||
} from '../desktop/window.js'
|
||||
|
||||
const rpc = createAppRpc();
|
||||
const mainWindow = await createMainWindow(rpc);
|
||||
const rpc = createAppRpc()
|
||||
const mainWindow = await createMainWindow(rpc)
|
||||
|
||||
setupNavigationGestures(mainWindow);
|
||||
registerGlobalShortcuts(rpc);
|
||||
setupDevAuthServer();
|
||||
handleDeepLinkFromArgv();
|
||||
setupNavigationGestures(mainWindow)
|
||||
registerGlobalShortcuts(rpc)
|
||||
setupDevAuthServer()
|
||||
handleDeepLinkFromArgv()
|
||||
|
||||
process.on("exit", () => {
|
||||
unregisterGlobalShortcuts();
|
||||
});
|
||||
process.on('exit', () => {
|
||||
unregisterGlobalShortcuts()
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useContext } from 'react'
|
||||
import { Flex, Button } from 'antd'
|
||||
import { Flex, Button, Divider } from 'antd'
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
||||
import MinusIcon from '../../Icons/MinusIcon'
|
||||
@ -36,7 +36,13 @@ const DashboardWindowButtons = () => {
|
||||
<Flex align='center'>
|
||||
{platform == 'darwin' ? (
|
||||
isFullScreen == false ? (
|
||||
<div style={{ width: '80px' }} />
|
||||
<>
|
||||
<div style={{ width: '82px' }} />
|
||||
<Divider
|
||||
type='vertical'
|
||||
style={{ height: '14px', margin: '3px 6px 0 0' }}
|
||||
/>
|
||||
</>
|
||||
) : null
|
||||
) : (
|
||||
<div
|
||||
|
||||
@ -1,27 +1,16 @@
|
||||
import { createContext, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import desktopBridge, {
|
||||
isElectrobunDesktop
|
||||
} from '../../../electrobun-bridge.js'
|
||||
|
||||
const electron = window.require ? window.require('electron') : null
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null
|
||||
|
||||
function getDesktopAPI() {
|
||||
return window.electronAPI
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function isElectron() {
|
||||
const desktopAPI = getDesktopAPI()
|
||||
|
||||
if (desktopAPI?.isDesktop) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.__electrobunWebviewId &&
|
||||
window.__electrobunRpcSocketPort
|
||||
) {
|
||||
if (isElectrobunDesktop()) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -66,12 +55,7 @@ const ElectronProvider = ({ children }) => {
|
||||
const [isMaximized, setIsMaximized] = useState(false)
|
||||
const [isFullScreen, setIsFullScreen] = useState(false)
|
||||
const [electronAvailable] = useState(isElectron())
|
||||
const useElectrobun = Boolean(
|
||||
getDesktopAPI()?.isDesktop ||
|
||||
(typeof window !== 'undefined' &&
|
||||
window.__electrobunWebviewId &&
|
||||
window.__electrobunRpcSocketPort)
|
||||
)
|
||||
const useElectrobun = isElectrobunDesktop()
|
||||
const navigate = useNavigate()
|
||||
const lastNavigationAtRef = useRef(0)
|
||||
|
||||
@ -101,7 +85,9 @@ const ElectronProvider = ({ children }) => {
|
||||
|
||||
const openExternalUrl = (url) => {
|
||||
if (useElectrobun) {
|
||||
getDesktopAPI().openExternalUrl(url)
|
||||
void desktopBridge.openExternalUrl(url).catch((error) => {
|
||||
console.warn('[ElectronContext] Failed to open external url:', error)
|
||||
})
|
||||
return true
|
||||
}
|
||||
if (electronAvailable && ipcRenderer) {
|
||||
@ -113,7 +99,9 @@ const ElectronProvider = ({ children }) => {
|
||||
|
||||
const openInternalUrl = (url) => {
|
||||
if (useElectrobun) {
|
||||
getDesktopAPI().openInternalUrl(url)
|
||||
void desktopBridge.openInternalUrl(url).catch((error) => {
|
||||
console.warn('[ElectronContext] Failed to open internal url:', error)
|
||||
})
|
||||
return true
|
||||
}
|
||||
if (electronAvailable && ipcRenderer) {
|
||||
@ -127,7 +115,7 @@ const ElectronProvider = ({ children }) => {
|
||||
if (!electronAvailable) return
|
||||
|
||||
if (useElectrobun) {
|
||||
getDesktopAPI().getOsInfo().then((info) => {
|
||||
desktopBridge.getOsInfo().then((info) => {
|
||||
if (info?.platform) {
|
||||
setPlatform(info.platform)
|
||||
if (info.platform === 'darwin') {
|
||||
@ -136,16 +124,16 @@ const ElectronProvider = ({ children }) => {
|
||||
}
|
||||
})
|
||||
|
||||
getDesktopAPI().getWindowState().then(applyWindowState)
|
||||
desktopBridge.getWindowState().then(applyWindowState)
|
||||
|
||||
const unsubWindowState = getDesktopAPI().onMessage(
|
||||
const unsubWindowState = desktopBridge.onMessage(
|
||||
'windowState',
|
||||
applyWindowState
|
||||
)
|
||||
const unsubNavigate = getDesktopAPI().onMessage('navigate', (url) => {
|
||||
const unsubNavigate = desktopBridge.onMessage('navigate', (url) => {
|
||||
navigate(url)
|
||||
})
|
||||
const unsubNavigationGesture = getDesktopAPI().onMessage(
|
||||
const unsubNavigationGesture = desktopBridge.onMessage(
|
||||
'navigationGesture',
|
||||
navigateHistory
|
||||
)
|
||||
@ -205,7 +193,8 @@ const ElectronProvider = ({ children }) => {
|
||||
let resetTimer
|
||||
|
||||
const handleWheel = (event) => {
|
||||
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return
|
||||
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)
|
||||
return
|
||||
if (Math.abs(event.deltaX) < Math.abs(event.deltaY)) return
|
||||
if (Math.abs(event.deltaX) < 2) return
|
||||
if (isInHorizontalScrollContainer(event.target)) return
|
||||
@ -230,7 +219,7 @@ const ElectronProvider = ({ children }) => {
|
||||
|
||||
const handleWindowControl = (action) => {
|
||||
if (useElectrobun) {
|
||||
getDesktopAPI().windowControl(action)
|
||||
void desktopBridge.windowControl(action)
|
||||
return
|
||||
}
|
||||
if (electronAvailable && ipcRenderer) {
|
||||
@ -240,7 +229,7 @@ const ElectronProvider = ({ children }) => {
|
||||
|
||||
const getAuthSession = async () => {
|
||||
if (!electronAvailable) return null
|
||||
if (useElectrobun) return await getDesktopAPI().getAuthSession()
|
||||
if (useElectrobun) return await desktopBridge.getAuthSession()
|
||||
if (!ipcRenderer) return null
|
||||
return await ipcRenderer.invoke('auth-session-get')
|
||||
}
|
||||
@ -248,7 +237,7 @@ const ElectronProvider = ({ children }) => {
|
||||
const setAuthSession = async (session) => {
|
||||
if (!electronAvailable) return false
|
||||
if (useElectrobun) {
|
||||
const result = await getDesktopAPI().setAuthSession(session)
|
||||
const result = await desktopBridge.setAuthSession(session)
|
||||
return result?.ok ?? false
|
||||
}
|
||||
if (!ipcRenderer) return false
|
||||
@ -258,7 +247,7 @@ const ElectronProvider = ({ children }) => {
|
||||
const clearAuthSession = async () => {
|
||||
if (!electronAvailable) return false
|
||||
if (useElectrobun) {
|
||||
const result = await getDesktopAPI().clearAuthSession()
|
||||
const result = await desktopBridge.clearAuthSession()
|
||||
return result?.ok ?? false
|
||||
}
|
||||
if (!ipcRenderer) return false
|
||||
@ -267,7 +256,7 @@ const ElectronProvider = ({ children }) => {
|
||||
|
||||
const getAppSettings = useCallback(async () => {
|
||||
if (!electronAvailable) return {}
|
||||
if (useElectrobun) return await getDesktopAPI().getAppSettings()
|
||||
if (useElectrobun) return await desktopBridge.getAppSettings()
|
||||
if (!ipcRenderer) return {}
|
||||
return await ipcRenderer.invoke('app-settings-get')
|
||||
}, [electronAvailable, useElectrobun])
|
||||
@ -276,7 +265,7 @@ const ElectronProvider = ({ children }) => {
|
||||
async (settings) => {
|
||||
if (!electronAvailable) return false
|
||||
if (useElectrobun) {
|
||||
const result = await getDesktopAPI().setAppSettings(settings)
|
||||
const result = await desktopBridge.setAppSettings(settings)
|
||||
return result?.ok ?? false
|
||||
}
|
||||
if (!ipcRenderer) return false
|
||||
@ -289,7 +278,7 @@ const ElectronProvider = ({ children }) => {
|
||||
async (update) => {
|
||||
if (!electronAvailable) return false
|
||||
if (useElectrobun) {
|
||||
const result = await getDesktopAPI().startAppUpdate(update)
|
||||
const result = await desktopBridge.startAppUpdate(update)
|
||||
return result?.ok ?? false
|
||||
}
|
||||
if (!ipcRenderer) return false
|
||||
@ -305,7 +294,7 @@ const ElectronProvider = ({ children }) => {
|
||||
}
|
||||
|
||||
if (useElectrobun) {
|
||||
return getDesktopAPI().onMessage('appUpdateProgress', handler)
|
||||
return desktopBridge.onMessage('appUpdateProgress', handler)
|
||||
}
|
||||
|
||||
if (!ipcRenderer) return () => {}
|
||||
@ -337,7 +326,7 @@ const ElectronProvider = ({ children }) => {
|
||||
if (!electronAvailable) return false
|
||||
try {
|
||||
if (useElectrobun) {
|
||||
const result = await getDesktopAPI().resizeSpotlightWindow(height)
|
||||
const result = await desktopBridge.resizeSpotlightWindow(height)
|
||||
return result?.ok ?? false
|
||||
}
|
||||
if (!ipcRenderer) return false
|
||||
@ -355,7 +344,7 @@ const ElectronProvider = ({ children }) => {
|
||||
async (sections) => {
|
||||
if (!electronAvailable) return false
|
||||
if (useElectrobun) {
|
||||
const result = await getDesktopAPI().setSidebarViewMenu(sections)
|
||||
const result = await desktopBridge.setSidebarViewMenu(sections)
|
||||
return result?.ok ?? false
|
||||
}
|
||||
if (!ipcRenderer) return false
|
||||
@ -366,7 +355,7 @@ const ElectronProvider = ({ children }) => {
|
||||
|
||||
const getElectronVersion = useCallback(async () => {
|
||||
if (!electronAvailable) return null
|
||||
if (useElectrobun) return await getDesktopAPI().getAppVersion()
|
||||
if (useElectrobun) return await desktopBridge.getAppVersion()
|
||||
if (!ipcRenderer) return null
|
||||
return await ipcRenderer.invoke('electron-version')
|
||||
}, [electronAvailable, useElectrobun])
|
||||
|
||||
@ -3,8 +3,6 @@ import { existsSync, statSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const MAC_TRAFFIC_LIGHTS_X = 14
|
||||
const MAC_TRAFFIC_LIGHTS_Y = 12
|
||||
export const MAC_WINDOW_CORNER_RADIUS = 15
|
||||
|
||||
function resolveDylibPath() {
|
||||
@ -57,10 +55,6 @@ export function applyMacOSWindowEffects(mainWindow) {
|
||||
setWindowCornerRadius: {
|
||||
args: [FFIType.ptr, FFIType.f64],
|
||||
returns: FFIType.bool
|
||||
},
|
||||
setWindowTrafficLightsPosition: {
|
||||
args: [FFIType.ptr, FFIType.f64, FFIType.f64],
|
||||
returns: FFIType.bool
|
||||
}
|
||||
})
|
||||
|
||||
@ -69,32 +63,16 @@ export function applyMacOSWindowEffects(mainWindow) {
|
||||
MAC_WINDOW_CORNER_RADIUS
|
||||
)
|
||||
const shadowEnabled = lib.symbols.ensureWindowShadow(mainWindow.ptr)
|
||||
const alignButtons = () =>
|
||||
lib.symbols.setWindowTrafficLightsPosition(
|
||||
mainWindow.ptr,
|
||||
MAC_TRAFFIC_LIGHTS_X,
|
||||
MAC_TRAFFIC_LIGHTS_Y
|
||||
)
|
||||
const refreshWindowChrome = () => {
|
||||
|
||||
mainWindow.on?.('resize', () => {
|
||||
lib.symbols.setWindowCornerRadius(
|
||||
mainWindow.ptr,
|
||||
MAC_WINDOW_CORNER_RADIUS
|
||||
)
|
||||
alignButtons()
|
||||
}
|
||||
|
||||
const buttonsAlignedNow = alignButtons()
|
||||
|
||||
setTimeout(() => {
|
||||
refreshWindowChrome()
|
||||
}, 120)
|
||||
|
||||
mainWindow.on?.('resize', () => {
|
||||
refreshWindowChrome()
|
||||
})
|
||||
|
||||
console.log(
|
||||
`macOS vibrancy applied (vibrancy=${vibrancyEnabled}, shadow=${shadowEnabled}, trafficLights=${buttonsAlignedNow}, cornerRadius=${MAC_WINDOW_CORNER_RADIUS})`
|
||||
`macOS vibrancy applied (vibrancy=${vibrancyEnabled}, shadow=${shadowEnabled}, cornerRadius=${MAC_WINDOW_CORNER_RADIUS})`
|
||||
)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
import { BrowserView } from "electrobun/bun";
|
||||
import { startAppUpdate } from "./appupdate.js";
|
||||
import { setSidebarViewMenu } from "./menu.js";
|
||||
import { sendToRenderer } from "./notify.js";
|
||||
import { resizeSpotlightWindow } from "./spotlight.js";
|
||||
import { BrowserView } from 'electrobun/bun'
|
||||
import { startAppUpdate } from './appupdate.js'
|
||||
import { setSidebarViewMenu } from './menu.js'
|
||||
import { sendToRenderer } from './notify.js'
|
||||
import { resizeSpotlightWindow } from './spotlight.js'
|
||||
import {
|
||||
clearAuthSession,
|
||||
getAppSettings,
|
||||
getAuthSession,
|
||||
setAppSettings,
|
||||
setAuthSession,
|
||||
} from "./store.js";
|
||||
setAuthSession
|
||||
} from './store.js'
|
||||
import {
|
||||
getMainWindow,
|
||||
getWindowState,
|
||||
handleWindowControl,
|
||||
openExternalUrl,
|
||||
openInternalUrl,
|
||||
} from "./window.js";
|
||||
openInternalUrl
|
||||
} from './window.js'
|
||||
|
||||
export function createAppRpc() {
|
||||
return BrowserView.defineRPC({
|
||||
@ -24,52 +24,53 @@ export function createAppRpc() {
|
||||
handlers: {
|
||||
requests: {
|
||||
getOsInfo: async () => ({
|
||||
platform: process.platform,
|
||||
platform: process.platform
|
||||
}),
|
||||
getWindowState: async () => getWindowState(),
|
||||
windowControl: async ({ action }) => {
|
||||
handleWindowControl(action);
|
||||
return { ok: true };
|
||||
handleWindowControl(action)
|
||||
return { ok: true }
|
||||
},
|
||||
openExternalUrl: async ({ url }) => {
|
||||
openExternalUrl(url);
|
||||
return { ok: true };
|
||||
openExternalUrl(url)
|
||||
console.log('openExternalUrl', url)
|
||||
return { ok: true }
|
||||
},
|
||||
openInternalUrl: async ({ url }) => ({
|
||||
ok: openInternalUrl(url),
|
||||
ok: openInternalUrl(url)
|
||||
}),
|
||||
getAuthSession: async () => getAuthSession(),
|
||||
setAuthSession: async ({ session }) => ({
|
||||
ok: await setAuthSession(session),
|
||||
ok: await setAuthSession(session)
|
||||
}),
|
||||
clearAuthSession: async () => ({
|
||||
ok: await clearAuthSession(),
|
||||
ok: await clearAuthSession()
|
||||
}),
|
||||
getAppSettings: async () => getAppSettings(),
|
||||
setAppSettings: async ({ settings }) => ({
|
||||
ok: await setAppSettings(settings),
|
||||
ok: await setAppSettings(settings)
|
||||
}),
|
||||
startAppUpdate: async ({ update }) => {
|
||||
const mainWindow = getMainWindow();
|
||||
const mainWindow = getMainWindow()
|
||||
const sendProgress = (payload) => {
|
||||
sendToRenderer("appUpdateProgress", {
|
||||
sendToRenderer('appUpdateProgress', {
|
||||
timestamp: new Date().toISOString(),
|
||||
...payload,
|
||||
});
|
||||
};
|
||||
...payload
|
||||
})
|
||||
}
|
||||
|
||||
await startAppUpdate(mainWindow, update, sendProgress);
|
||||
return { ok: true };
|
||||
await startAppUpdate(mainWindow, update, sendProgress)
|
||||
return { ok: true }
|
||||
},
|
||||
resizeSpotlightWindow: async ({ height }) => ({
|
||||
ok: resizeSpotlightWindow(height),
|
||||
ok: resizeSpotlightWindow(height)
|
||||
}),
|
||||
setSidebarViewMenu: async ({ sections }) => ({
|
||||
ok: setSidebarViewMenu(sections),
|
||||
ok: setSidebarViewMenu(sections)
|
||||
}),
|
||||
getAppVersion: async () => process.env.ELECTROBUN_VERSION || "desktop",
|
||||
getAppVersion: async () => process.env.ELECTROBUN_VERSION || 'desktop'
|
||||
},
|
||||
messages: {},
|
||||
},
|
||||
});
|
||||
messages: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,33 +1,33 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { BrowserWindow, GlobalShortcut } from "electrobun/bun";
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { BrowserWindow, GlobalShortcut } from 'electrobun/bun'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const DEV_SERVER_PORT = 5780;
|
||||
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
|
||||
const SPOTLIGHT_ROUTE_PATH = "/dashboard/electron/spotlightcontent";
|
||||
const DEV_SERVER_PORT = 5780
|
||||
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
|
||||
const SPOTLIGHT_ROUTE_PATH = '/dashboard/electron/spotlightcontent'
|
||||
|
||||
let spotlightWindow = null;
|
||||
let spotlightWindow = null
|
||||
|
||||
function getSpotlightRouteUrl() {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
return `${DEV_SERVER_URL}${SPOTLIGHT_ROUTE_PATH}`;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `${DEV_SERVER_URL}${SPOTLIGHT_ROUTE_PATH}`
|
||||
}
|
||||
|
||||
return `views://mainview/index.html#${SPOTLIGHT_ROUTE_PATH}`;
|
||||
return `views://mainview/index.html#${SPOTLIGHT_ROUTE_PATH}`
|
||||
}
|
||||
|
||||
export function openSpotlightContentWindow(rpc) {
|
||||
if (spotlightWindow && !spotlightWindow.isDestroyed?.()) {
|
||||
spotlightWindow.show?.();
|
||||
spotlightWindow.focus?.();
|
||||
return spotlightWindow;
|
||||
spotlightWindow.show?.()
|
||||
spotlightWindow.focus?.()
|
||||
return spotlightWindow
|
||||
}
|
||||
|
||||
spotlightWindow = new BrowserWindow({
|
||||
title: "Farm Control Spotlight",
|
||||
title: 'Farm Control Spotlight',
|
||||
url: getSpotlightRouteUrl(),
|
||||
rpc,
|
||||
transparent: true,
|
||||
@ -35,69 +35,72 @@ export function openSpotlightContentWindow(rpc) {
|
||||
width: 700,
|
||||
height: 40,
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
spotlightWindow.on?.("close", (event) => {
|
||||
event?.preventDefault?.();
|
||||
if (spotlightWindow && !spotlightWindow.isDestroyed?.()) {
|
||||
spotlightWindow.hide?.();
|
||||
y: 100
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
spotlightWindow.on?.("blur", () => {
|
||||
spotlightWindow.on?.('close', (event) => {
|
||||
event?.preventDefault?.()
|
||||
if (spotlightWindow && !spotlightWindow.isDestroyed?.()) {
|
||||
spotlightWindow.hide?.();
|
||||
spotlightWindow.hide?.()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return spotlightWindow;
|
||||
spotlightWindow.on?.('blur', () => {
|
||||
if (spotlightWindow && !spotlightWindow.isDestroyed?.()) {
|
||||
spotlightWindow.hide?.()
|
||||
}
|
||||
})
|
||||
|
||||
return spotlightWindow
|
||||
}
|
||||
|
||||
export function getSpotlightWindow() {
|
||||
return spotlightWindow;
|
||||
return spotlightWindow
|
||||
}
|
||||
|
||||
export function registerGlobalShortcuts(rpc) {
|
||||
try {
|
||||
const registered = GlobalShortcut.register("Alt+Shift+Q", () => {
|
||||
openSpotlightContentWindow(rpc);
|
||||
});
|
||||
const registered = GlobalShortcut.register('Alt+Shift+Q', () => {
|
||||
openSpotlightContentWindow(rpc)
|
||||
})
|
||||
|
||||
if (!registered) {
|
||||
console.warn("[globalShortcut] Failed to register Alt+Shift+Q");
|
||||
console.warn('[globalShortcut] Failed to register Alt+Shift+Q')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[globalShortcut] Error registering Alt+Shift+Q",
|
||||
error?.message || error,
|
||||
);
|
||||
'[globalShortcut] Error registering Alt+Shift+Q',
|
||||
error?.message || error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function unregisterGlobalShortcuts() {
|
||||
try {
|
||||
GlobalShortcut.unregisterAll();
|
||||
GlobalShortcut.unregisterAll()
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[globalShortcut] Error unregistering shortcuts",
|
||||
error?.message || error,
|
||||
);
|
||||
'[globalShortcut] Error unregistering shortcuts',
|
||||
error?.message || error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function resizeSpotlightWindow(height) {
|
||||
if (!spotlightWindow || spotlightWindow.isDestroyed?.()) return false;
|
||||
if (!spotlightWindow || spotlightWindow.isDestroyed?.()) return false
|
||||
|
||||
try {
|
||||
const frame = spotlightWindow.getFrame?.() || spotlightWindow.getBounds?.();
|
||||
const width = frame?.width || 700;
|
||||
spotlightWindow.setSize?.(width, height);
|
||||
spotlightWindow.center?.();
|
||||
return true;
|
||||
const frame = spotlightWindow.getFrame?.() || spotlightWindow.getBounds?.()
|
||||
const width = frame?.width || 700
|
||||
spotlightWindow.setSize?.(width, height)
|
||||
spotlightWindow.center?.()
|
||||
return true
|
||||
} catch (error) {
|
||||
console.warn("[spotlight] Failed to resize window.", error?.message || error);
|
||||
return false;
|
||||
console.warn(
|
||||
'[spotlight] Failed to resize window.',
|
||||
error?.message || error
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,258 +1,264 @@
|
||||
import Electrobun, { BrowserWindow, Updater, Utils } from "electrobun/bun";
|
||||
import { applyApplicationMenu, setupApplicationMenuEvents } from "./menu.js";
|
||||
import { applyMacOSWindowEffects } from "./macos-window-effects.js";
|
||||
import { sendToRenderer, setMessageSender } from "./notify.js";
|
||||
import Electrobun, { BrowserWindow, Updater, Utils } from 'electrobun/bun'
|
||||
import { applyApplicationMenu, setupApplicationMenuEvents } from './menu.js'
|
||||
import { applyMacOSWindowEffects } from './macos-window-effects.js'
|
||||
import { sendToRenderer, setMessageSender } from './notify.js'
|
||||
|
||||
const isMacOS = process.platform === "darwin";
|
||||
const isMacOS = process.platform === 'darwin'
|
||||
|
||||
const DEV_SERVER_PORT = 5780;
|
||||
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
|
||||
const PROTOCOL_PREFIX = "farmcontrol://";
|
||||
const DEV_SERVER_PORT = 5780
|
||||
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
|
||||
const PROTOCOL_PREFIX = 'farmcontrol://'
|
||||
|
||||
let mainWindow = null;
|
||||
let webviewDomReady = false;
|
||||
const pendingNavigations = [];
|
||||
let mainWindow = null
|
||||
let webviewDomReady = false
|
||||
const pendingNavigations = []
|
||||
|
||||
export function getMainWindow() {
|
||||
return mainWindow;
|
||||
return mainWindow
|
||||
}
|
||||
|
||||
export async function getMainViewUrl() {
|
||||
const channel = await Updater.localInfo.channel();
|
||||
if (channel === "dev" || process.env.NODE_ENV === "development") {
|
||||
const channel = await Updater.localInfo.channel()
|
||||
if (channel === 'dev' || process.env.NODE_ENV === 'development') {
|
||||
try {
|
||||
await fetch(DEV_SERVER_URL, { method: "HEAD" });
|
||||
console.log(`Using Vite dev server at ${DEV_SERVER_URL}`);
|
||||
return DEV_SERVER_URL;
|
||||
await fetch(DEV_SERVER_URL, { method: 'HEAD' })
|
||||
console.log(`Using Vite dev server at ${DEV_SERVER_URL}`)
|
||||
return DEV_SERVER_URL
|
||||
} catch {
|
||||
console.warn(
|
||||
"Vite dev server not running. Start it with `bun run dev:renderer`.",
|
||||
);
|
||||
'Vite dev server not running. Start it with `bun run dev:renderer`.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return "views://mainview/index.html";
|
||||
return 'views://mainview/index.html'
|
||||
}
|
||||
|
||||
function deliverNavigation(redirectPath) {
|
||||
sendToRenderer("navigate", redirectPath);
|
||||
mainWindow?.show?.();
|
||||
mainWindow?.activate?.();
|
||||
sendToRenderer('navigate', redirectPath)
|
||||
mainWindow?.show?.()
|
||||
mainWindow?.activate?.()
|
||||
}
|
||||
|
||||
function flushPendingNavigations() {
|
||||
if (!mainWindow || !webviewDomReady) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
while (pendingNavigations.length > 0) {
|
||||
const redirectPath = pendingNavigations.shift();
|
||||
setTimeout(() => deliverNavigation(redirectPath), 100);
|
||||
const redirectPath = pendingNavigations.shift()
|
||||
setTimeout(() => deliverNavigation(redirectPath), 100)
|
||||
}
|
||||
}
|
||||
|
||||
function sendNavigateToRenderer(redirectPath) {
|
||||
if (!redirectPath || typeof redirectPath !== "string") {
|
||||
return;
|
||||
if (!redirectPath || typeof redirectPath !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!mainWindow || !webviewDomReady) {
|
||||
pendingNavigations.push(redirectPath);
|
||||
return;
|
||||
pendingNavigations.push(redirectPath)
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => deliverNavigation(redirectPath), 100);
|
||||
setTimeout(() => deliverNavigation(redirectPath), 100)
|
||||
}
|
||||
|
||||
export function handleDeepLink(url) {
|
||||
if (!url?.startsWith(`${PROTOCOL_PREFIX}app`)) return;
|
||||
const redirectPath = url.replace(`${PROTOCOL_PREFIX}app`, "") || "/";
|
||||
sendNavigateToRenderer(redirectPath);
|
||||
if (!url?.startsWith(`${PROTOCOL_PREFIX}app`)) return
|
||||
const redirectPath = url.replace(`${PROTOCOL_PREFIX}app`, '') || '/'
|
||||
sendNavigateToRenderer(redirectPath)
|
||||
}
|
||||
|
||||
function findProtocolUrl(args) {
|
||||
return args.find(
|
||||
(arg) => typeof arg === "string" && arg.startsWith(PROTOCOL_PREFIX),
|
||||
);
|
||||
(arg) => typeof arg === 'string' && arg.startsWith(PROTOCOL_PREFIX)
|
||||
)
|
||||
}
|
||||
|
||||
export function handleDeepLinkFromArgv() {
|
||||
if (process.platform === "darwin") return;
|
||||
const url = findProtocolUrl(process.argv);
|
||||
if (url) handleDeepLink(url);
|
||||
if (process.platform === 'darwin') return
|
||||
const url = findProtocolUrl(process.argv)
|
||||
if (url) handleDeepLink(url)
|
||||
}
|
||||
|
||||
function broadcastWindowState() {
|
||||
sendToRenderer('windowState', getWindowState())
|
||||
}
|
||||
|
||||
function setupWindowEvents(window) {
|
||||
window.on?.("maximize", () => {
|
||||
sendToRenderer("windowState", { isMaximized: true });
|
||||
});
|
||||
window.on?.("unmaximize", () => {
|
||||
sendToRenderer("windowState", { isMaximized: false });
|
||||
});
|
||||
window.on?.("enter-full-screen", () => {
|
||||
sendToRenderer("windowState", { isFullScreen: true });
|
||||
});
|
||||
window.on?.("leave-full-screen", () => {
|
||||
sendToRenderer("windowState", { isFullScreen: false });
|
||||
});
|
||||
// Electrobun emits resize/focus, not Electron's maximize/fullscreen events.
|
||||
window.on?.('resize', broadcastWindowState)
|
||||
window.on?.('focus', broadcastWindowState)
|
||||
}
|
||||
|
||||
export function setupMainWindowMessaging(window = mainWindow) {
|
||||
if (!window) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
setMessageSender((channel, data) => {
|
||||
try {
|
||||
const send = window.webview?.rpc?.send;
|
||||
if (!send) {
|
||||
console.warn(
|
||||
`No RPC sender available for channel: ${channel}. Is the window ready?`,
|
||||
);
|
||||
return false;
|
||||
const webview = window.webview
|
||||
const channelLiteral = JSON.stringify(channel)
|
||||
const payloadLiteral = JSON.stringify(data ?? null)
|
||||
|
||||
// Prefer direct JS dispatch — WebSocket RPC pushes can be deferred by
|
||||
// WKWebView until the next interaction during native window transitions.
|
||||
if (webview?.executeJavascript) {
|
||||
webview.executeJavascript(
|
||||
`window.__farmcontrolDispatchRpcMessage?.(${channelLiteral}, ${payloadLiteral})`
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
send[channel](data);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to send RPC message on channel: ${channel}`, error);
|
||||
return false;
|
||||
const send = webview?.rpc?.send
|
||||
if (!send) {
|
||||
console.warn(
|
||||
`No RPC sender available for channel: ${channel}. Is the window ready?`
|
||||
)
|
||||
return false
|
||||
}
|
||||
});
|
||||
|
||||
send[channel](data)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.warn(`Failed to send RPC message on channel: ${channel}`, error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function createMainWindow(rpc) {
|
||||
const url = await getMainViewUrl();
|
||||
const url = await getMainViewUrl()
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
title: "Farm Control",
|
||||
title: 'Farm Control',
|
||||
url,
|
||||
rpc,
|
||||
titleBarStyle: "hiddenInset",
|
||||
trafficLightOffset: { x: 14, y: 12 },
|
||||
titleBarStyle: 'hiddenInset',
|
||||
...(isMacOS ? { transparent: true } : {}),
|
||||
frame: {
|
||||
width: 1200,
|
||||
height: 800,
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
y: 100
|
||||
}
|
||||
})
|
||||
|
||||
if (isMacOS) {
|
||||
applyMacOSWindowEffects(mainWindow);
|
||||
applyMacOSWindowEffects(mainWindow)
|
||||
}
|
||||
|
||||
setupMainWindowMessaging(mainWindow);
|
||||
applyApplicationMenu();
|
||||
setupMainWindowMessaging(mainWindow)
|
||||
applyApplicationMenu()
|
||||
setupApplicationMenuEvents({
|
||||
onNavigate: sendNavigateToRenderer,
|
||||
onToggleDevTools: () => {
|
||||
mainWindow?.webview?.toggleDevTools?.();
|
||||
},
|
||||
});
|
||||
|
||||
setupWindowEvents(mainWindow);
|
||||
|
||||
Electrobun.events.on("open-url", (event) => {
|
||||
const url = event?.data?.url;
|
||||
if (url) {
|
||||
handleDeepLink(url);
|
||||
mainWindow?.webview?.toggleDevTools?.()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
setupWindowEvents(mainWindow)
|
||||
|
||||
Electrobun.events.on('open-url', (event) => {
|
||||
const url = event?.data?.url
|
||||
if (url) {
|
||||
handleDeepLink(url)
|
||||
}
|
||||
})
|
||||
|
||||
return new Promise((resolve) => {
|
||||
mainWindow.webview.on("dom-ready", () => {
|
||||
webviewDomReady = true;
|
||||
flushPendingNavigations();
|
||||
resolve(mainWindow);
|
||||
});
|
||||
});
|
||||
mainWindow.webview.on('dom-ready', () => {
|
||||
webviewDomReady = true
|
||||
flushPendingNavigations()
|
||||
resolve(mainWindow)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function setupDevAuthServer() {
|
||||
const env = (process.env.NODE_ENV || "development").trim();
|
||||
if (env !== "development") return;
|
||||
const env = (process.env.NODE_ENV || 'development').trim()
|
||||
if (env !== 'development') return
|
||||
|
||||
const express = (await import("express")).default;
|
||||
const app = express();
|
||||
const port = 3500;
|
||||
const express = (await import('express')).default
|
||||
const app = express()
|
||||
const port = 3500
|
||||
|
||||
app.use((req, res) => {
|
||||
const redirectPath = req.originalUrl;
|
||||
res.send(
|
||||
`Open Farmcontrol to continue... (Redirect path: ${redirectPath})`,
|
||||
);
|
||||
sendNavigateToRenderer(redirectPath);
|
||||
});
|
||||
const redirectPath = req.originalUrl
|
||||
res.send(`Open Farmcontrol to continue... (Redirect path: ${redirectPath})`)
|
||||
sendNavigateToRenderer(redirectPath)
|
||||
})
|
||||
|
||||
app.listen(port, () => {});
|
||||
app.listen(port, () => {})
|
||||
}
|
||||
|
||||
export function openInternalUrl(url) {
|
||||
sendNavigateToRenderer(url);
|
||||
return true;
|
||||
sendNavigateToRenderer(url)
|
||||
return true
|
||||
}
|
||||
|
||||
export function getWindowState() {
|
||||
if (!mainWindow) {
|
||||
return { isFullScreen: false, isMaximized: false };
|
||||
return { isFullScreen: false, isMaximized: false }
|
||||
}
|
||||
|
||||
return {
|
||||
isFullScreen: mainWindow.isFullScreen?.() ?? false,
|
||||
isMaximized: mainWindow.isMaximized?.() ?? false,
|
||||
};
|
||||
isMaximized: mainWindow.isMaximized?.() ?? false
|
||||
}
|
||||
}
|
||||
|
||||
export function handleWindowControl(action) {
|
||||
if (!mainWindow) return;
|
||||
if (!mainWindow) return
|
||||
|
||||
switch (action) {
|
||||
case "minimize":
|
||||
mainWindow.minimize?.();
|
||||
break;
|
||||
case "maximize":
|
||||
case 'minimize':
|
||||
mainWindow.minimize?.()
|
||||
break
|
||||
case 'maximize':
|
||||
if (mainWindow.isMaximized?.()) {
|
||||
mainWindow.unmaximize?.();
|
||||
mainWindow.unmaximize?.()
|
||||
} else {
|
||||
mainWindow.maximize?.();
|
||||
mainWindow.maximize?.()
|
||||
}
|
||||
break;
|
||||
case "close":
|
||||
mainWindow.close?.();
|
||||
break;
|
||||
break
|
||||
case 'close':
|
||||
mainWindow.close?.()
|
||||
break
|
||||
default:
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export function sendNavigationGesture(direction) {
|
||||
sendToRenderer("navigationGesture", direction);
|
||||
sendToRenderer('navigationGesture', direction)
|
||||
}
|
||||
|
||||
export function setupNavigationGestures(window) {
|
||||
if (!window) return;
|
||||
if (!window) return
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
window.on?.("swipe", (_event, direction) => {
|
||||
if (direction === "left") {
|
||||
sendNavigationGesture("back");
|
||||
} else if (direction === "right") {
|
||||
sendNavigationGesture("forward");
|
||||
if (process.platform === 'darwin') {
|
||||
window.on?.('swipe', (_event, direction) => {
|
||||
if (direction === 'left') {
|
||||
sendNavigationGesture('back')
|
||||
} else if (direction === 'right') {
|
||||
sendNavigationGesture('forward')
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
window.on?.("app-command", (_event, command) => {
|
||||
if (command === "browser-backward") {
|
||||
sendNavigationGesture("back");
|
||||
} else if (command === "browser-forward") {
|
||||
sendNavigationGesture("forward");
|
||||
window.on?.('app-command', (_event, command) => {
|
||||
if (command === 'browser-backward') {
|
||||
sendNavigationGesture('back')
|
||||
} else if (command === 'browser-forward') {
|
||||
sendNavigationGesture('forward')
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
export function openExternalUrl(url) {
|
||||
Utils.openExternal(url);
|
||||
console.log('openExternalUrl', url)
|
||||
Utils.openExternal(url)
|
||||
}
|
||||
|
||||
@ -1,173 +1,191 @@
|
||||
const listeners = new Map();
|
||||
const pendingMessages = new Map();
|
||||
let rpc = null;
|
||||
let initPromise = null;
|
||||
let initialized = false;
|
||||
const listeners = new Map()
|
||||
const pendingMessages = new Map()
|
||||
let rpc = null
|
||||
let initPromise = null
|
||||
let initialized = false
|
||||
|
||||
export function isElectrobunDesktop() {
|
||||
return Boolean(
|
||||
typeof window !== "undefined" &&
|
||||
typeof window !== 'undefined' &&
|
||||
window.__electrobunWebviewId &&
|
||||
window.__electrobunRpcSocketPort,
|
||||
);
|
||||
window.__electrobunRpcSocketPort
|
||||
)
|
||||
}
|
||||
|
||||
function dispatchMessage(channel, data) {
|
||||
const channelListeners = listeners.get(channel);
|
||||
const channelListeners = listeners.get(channel)
|
||||
if (!channelListeners?.size) {
|
||||
if (!pendingMessages.has(channel)) {
|
||||
pendingMessages.set(channel, []);
|
||||
pendingMessages.set(channel, [])
|
||||
}
|
||||
pendingMessages.get(channel).push(data);
|
||||
return;
|
||||
pendingMessages.get(channel).push(data)
|
||||
return
|
||||
}
|
||||
|
||||
for (const callback of channelListeners) {
|
||||
callback(data);
|
||||
callback(data)
|
||||
}
|
||||
}
|
||||
|
||||
// Bun delivers push messages via executeJavascript for immediate handling.
|
||||
// WebSocket RPC messages can be deferred by WKWebView during native window
|
||||
// transitions (e.g. fullscreen) until the next user interaction.
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__farmcontrolDispatchRpcMessage = dispatchMessage
|
||||
}
|
||||
|
||||
async function setupRpc() {
|
||||
// electrobun/view captures webview globals at import time — import only after
|
||||
// Electrobun preload has set them (Vite dev can evaluate modules earlier).
|
||||
const { default: Electrobun, Electroview } = await import("electrobun/view");
|
||||
const { default: Electrobun, Electroview } = await import('electrobun/view')
|
||||
|
||||
rpc = Electroview.defineRPC({
|
||||
maxRequestTime: 30000,
|
||||
handlers: {
|
||||
requests: {},
|
||||
messages: {
|
||||
"*": (channel, data) => {
|
||||
dispatchMessage(channel, data);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
'*': (channel, data) => {
|
||||
dispatchMessage(channel, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
new Electrobun.Electroview({ rpc });
|
||||
new Electrobun.Electroview({ rpc })
|
||||
}
|
||||
|
||||
function shouldWaitForElectrobun() {
|
||||
if (isElectrobunDesktop()) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
if (typeof window === 'undefined') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (window.__electrobunWebviewId) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
const { protocol, hostname, port } = window.location;
|
||||
const { protocol, hostname, port } = window.location
|
||||
|
||||
if (protocol === "views:") {
|
||||
return true;
|
||||
if (protocol === 'views:') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Vite dev server used by electrobun dev:app
|
||||
if (hostname === "localhost" && port === "5780") {
|
||||
return true;
|
||||
if (hostname === 'localhost' && port === '5780') {
|
||||
return true
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
window.__electrobun ||
|
||||
window.__electrobunEventBridge ||
|
||||
window.__electrobunInternalBridge,
|
||||
);
|
||||
window.__electrobunInternalBridge
|
||||
)
|
||||
}
|
||||
|
||||
export async function initElectrobunBridge() {
|
||||
if (initialized) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!initPromise) {
|
||||
initPromise = (async () => {
|
||||
if (!shouldWaitForElectrobun()) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const deadline = Date.now() + 3000;
|
||||
const deadline = Date.now() + 3000
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
if (isElectrobunDesktop()) {
|
||||
await setupRpc();
|
||||
window.electronAPI = electronAPI;
|
||||
initialized = true;
|
||||
return;
|
||||
await setupRpc()
|
||||
window.electronAPI = electronAPI
|
||||
initialized = true
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"Electrobun bridge: webview globals not found; desktop RPC unavailable.",
|
||||
);
|
||||
})();
|
||||
'Electrobun bridge: webview globals not found; desktop RPC unavailable.'
|
||||
)
|
||||
})()
|
||||
}
|
||||
|
||||
await initPromise;
|
||||
await initPromise
|
||||
}
|
||||
|
||||
function onMessage(channel, callback) {
|
||||
if (!listeners.has(channel)) {
|
||||
listeners.set(channel, new Set());
|
||||
}
|
||||
listeners.get(channel).add(callback);
|
||||
void initElectrobunBridge()
|
||||
|
||||
const queued = pendingMessages.get(channel);
|
||||
if (!listeners.has(channel)) {
|
||||
listeners.set(channel, new Set())
|
||||
}
|
||||
listeners.get(channel).add(callback)
|
||||
|
||||
const queued = pendingMessages.get(channel)
|
||||
if (queued?.length) {
|
||||
pendingMessages.delete(channel);
|
||||
pendingMessages.delete(channel)
|
||||
for (const payload of queued) {
|
||||
callback(payload);
|
||||
callback(payload)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
listeners.get(channel)?.delete(callback);
|
||||
};
|
||||
listeners.get(channel)?.delete(callback)
|
||||
}
|
||||
}
|
||||
|
||||
function removeAllListeners(channel) {
|
||||
listeners.delete(channel);
|
||||
pendingMessages.delete(channel);
|
||||
listeners.delete(channel)
|
||||
pendingMessages.delete(channel)
|
||||
}
|
||||
|
||||
async function invokeRequest(method, params = {}) {
|
||||
await initElectrobunBridge();
|
||||
await initElectrobunBridge()
|
||||
|
||||
if (!rpc?.request?.[method]) {
|
||||
console.warn(`Unhandled RPC request: ${method}`);
|
||||
return null;
|
||||
if (!rpc?.request) {
|
||||
console.warn(`Electrobun RPC unavailable for request: ${method}`)
|
||||
return null
|
||||
}
|
||||
|
||||
return await rpc.request[method](params);
|
||||
try {
|
||||
return await rpc.request[method](params)
|
||||
} catch (error) {
|
||||
console.warn(`Electrobun RPC request failed: ${method}`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const electronAPI = {
|
||||
get isDesktop() {
|
||||
return isElectrobunDesktop();
|
||||
return isElectrobunDesktop()
|
||||
},
|
||||
onMessage,
|
||||
removeAllListeners,
|
||||
getOsInfo: () => invokeRequest("getOsInfo"),
|
||||
getWindowState: () => invokeRequest("getWindowState"),
|
||||
windowControl: (action) => invokeRequest("windowControl", { action }),
|
||||
openExternalUrl: (url) => invokeRequest("openExternalUrl", { url }),
|
||||
openInternalUrl: (url) => invokeRequest("openInternalUrl", { url }),
|
||||
getAuthSession: () => invokeRequest("getAuthSession"),
|
||||
setAuthSession: (session) => invokeRequest("setAuthSession", { session }),
|
||||
clearAuthSession: () => invokeRequest("clearAuthSession"),
|
||||
getAppSettings: () => invokeRequest("getAppSettings"),
|
||||
setAppSettings: (settings) => invokeRequest("setAppSettings", { settings }),
|
||||
startAppUpdate: (update) => invokeRequest("startAppUpdate", { update }),
|
||||
getOsInfo: () => invokeRequest('getOsInfo'),
|
||||
getWindowState: () => invokeRequest('getWindowState'),
|
||||
windowControl: (action) => invokeRequest('windowControl', { action }),
|
||||
openExternalUrl: (url) => invokeRequest('openExternalUrl', { url }),
|
||||
openInternalUrl: (url) => invokeRequest('openInternalUrl', { url }),
|
||||
getAuthSession: () => invokeRequest('getAuthSession'),
|
||||
setAuthSession: (session) => invokeRequest('setAuthSession', { session }),
|
||||
clearAuthSession: () => invokeRequest('clearAuthSession'),
|
||||
getAppSettings: () => invokeRequest('getAppSettings'),
|
||||
setAppSettings: (settings) => invokeRequest('setAppSettings', { settings }),
|
||||
startAppUpdate: (update) => invokeRequest('startAppUpdate', { update }),
|
||||
resizeSpotlightWindow: (height) =>
|
||||
invokeRequest("resizeSpotlightWindow", { height }),
|
||||
invokeRequest('resizeSpotlightWindow', { height }),
|
||||
setSidebarViewMenu: (sections) =>
|
||||
invokeRequest("setSidebarViewMenu", { sections }),
|
||||
getAppVersion: () => invokeRequest("getAppVersion"),
|
||||
};
|
||||
invokeRequest('setSidebarViewMenu', { sections }),
|
||||
getAppVersion: () => invokeRequest('getAppVersion')
|
||||
}
|
||||
|
||||
export default electronAPI;
|
||||
if (typeof window !== 'undefined') {
|
||||
window.electronAPI = electronAPI
|
||||
}
|
||||
|
||||
export default electronAPI
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user