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 {
|
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 {
|
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 {
|
html.macos-vibrancy .light-mode .ant-layout-content {
|
||||||
@ -145,6 +145,15 @@ html.macos-vibrancy .electron-sider .ant-menu-light {
|
|||||||
background: transparent !important;
|
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 {
|
.electron-navigation {
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
@ -153,6 +162,10 @@ html.macos-vibrancy .electron-sider .ant-menu-light {
|
|||||||
padding-inline: 10px;
|
padding-inline: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.electron-navigation.ant-menu-horizontal .ant-menu-item {
|
||||||
|
padding-inline: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.electron-sidebar .ant-menu-item,
|
.electron-sidebar .ant-menu-item,
|
||||||
.electron-sidebar .ant-menu-submenu-title {
|
.electron-sidebar .ant-menu-submenu-title {
|
||||||
height: 32.5px !important;
|
height: 32.5px !important;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#import <objc/runtime.h>
|
||||||
|
|
||||||
static NSString *const kElectrobunVibrancyViewIdentifier =
|
static NSString *const kElectrobunVibrancyViewIdentifier =
|
||||||
@"ElectrobunVibrancyView";
|
@"ElectrobunVibrancyView";
|
||||||
@ -8,6 +9,10 @@ static NSString *const kElectrobunWindowBorderViewIdentifier =
|
|||||||
@"ElectrobunWindowBorderView";
|
@"ElectrobunWindowBorderView";
|
||||||
|
|
||||||
static CGFloat gWindowCornerRadius = 15.0;
|
static CGFloat gWindowCornerRadius = 15.0;
|
||||||
|
static CGFloat gTrafficLightsX = 16.0;
|
||||||
|
static CGFloat gTrafficLightsY = 12.0;
|
||||||
|
|
||||||
|
static const void *kElectrobunChromeObserverKey = &kElectrobunChromeObserverKey;
|
||||||
|
|
||||||
@interface ElectrobunWindowBorderView : NSView
|
@interface ElectrobunWindowBorderView : NSView
|
||||||
@property(nonatomic) CGFloat cornerRadius;
|
@property(nonatomic) CGFloat cornerRadius;
|
||||||
@ -94,6 +99,58 @@ static CGFloat gWindowCornerRadius = 15.0;
|
|||||||
}
|
}
|
||||||
@end
|
@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) {
|
static NSVisualEffectView *findVibrancyView(NSView *contentView) {
|
||||||
for (NSView *subview in [contentView subviews]) {
|
for (NSView *subview in [contentView subviews]) {
|
||||||
if ([subview isKindOfClass:[NSVisualEffectView class]] &&
|
if ([subview isKindOfClass:[NSVisualEffectView class]] &&
|
||||||
@ -130,8 +187,13 @@ static ElectrobunWindowBorderView *findWindowBorderView(NSView *contentView) {
|
|||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void applyWindowCornerRadius(NSWindow *window, CGFloat radius) {
|
static BOOL isWindowFullScreen(NSWindow *window) {
|
||||||
gWindowCornerRadius = MAX(0.0, radius);
|
return (window.styleMask & NSWindowStyleMaskFullScreen) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void refreshWindowChrome(NSWindow *window) {
|
||||||
|
BOOL fullScreen = isWindowFullScreen(window);
|
||||||
|
CGFloat radius = fullScreen ? 0.0 : gWindowCornerRadius;
|
||||||
|
|
||||||
NSView *contentView = [window contentView];
|
NSView *contentView = [window contentView];
|
||||||
if (contentView == nil) {
|
if (contentView == nil) {
|
||||||
@ -139,23 +201,31 @@ static void applyWindowCornerRadius(NSWindow *window, CGFloat radius) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
contentView.wantsLayer = YES;
|
contentView.wantsLayer = YES;
|
||||||
contentView.layer.cornerRadius = gWindowCornerRadius;
|
contentView.layer.cornerRadius = radius;
|
||||||
contentView.layer.masksToBounds = YES;
|
contentView.layer.masksToBounds = YES;
|
||||||
|
|
||||||
NSVisualEffectView *effectView = findVibrancyView(contentView);
|
NSVisualEffectView *effectView = findVibrancyView(contentView);
|
||||||
if (effectView != nil) {
|
if (effectView != nil) {
|
||||||
effectView.wantsLayer = YES;
|
effectView.wantsLayer = YES;
|
||||||
effectView.layer.cornerRadius = gWindowCornerRadius;
|
effectView.layer.cornerRadius = radius;
|
||||||
effectView.layer.masksToBounds = YES;
|
effectView.layer.masksToBounds = YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
ElectrobunWindowBorderView *borderView = findWindowBorderView(contentView);
|
ElectrobunWindowBorderView *borderView = findWindowBorderView(contentView);
|
||||||
if (borderView != nil) {
|
if (borderView != nil) {
|
||||||
borderView.cornerRadius = gWindowCornerRadius;
|
borderView.hidden = fullScreen;
|
||||||
[borderView setNeedsDisplay:YES];
|
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) {
|
static void ensureWindowBorder(NSWindow *window) {
|
||||||
NSView *contentView = [window contentView];
|
NSView *contentView = [window contentView];
|
||||||
if (contentView == nil) {
|
if (contentView == nil) {
|
||||||
@ -171,7 +241,6 @@ static void ensureWindowBorder(NSWindow *window) {
|
|||||||
setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
|
setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
|
||||||
}
|
}
|
||||||
|
|
||||||
borderView.cornerRadius = gWindowCornerRadius;
|
|
||||||
[borderView setFrame:[contentView bounds]];
|
[borderView setFrame:[contentView bounds]];
|
||||||
|
|
||||||
if ([borderView superview] == nil) {
|
if ([borderView superview] == nil) {
|
||||||
@ -185,7 +254,76 @@ static void ensureWindowBorder(NSWindow *window) {
|
|||||||
relativeTo:nil];
|
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) {
|
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, *)) {
|
if (@available(macOS 10.14, *)) {
|
||||||
[effectView setMaterial:NSVisualEffectMaterialUnderWindowBackground];
|
[effectView setMaterial:NSVisualEffectMaterialHUDWindow];
|
||||||
} else {
|
} else {
|
||||||
[effectView setMaterial:NSVisualEffectMaterialSidebar];
|
[effectView setMaterial:NSVisualEffectMaterialSidebar];
|
||||||
}
|
}
|
||||||
@ -243,6 +381,8 @@ extern "C" bool enableWindowVibrancy(void *windowPtr, double cornerRadius) {
|
|||||||
|
|
||||||
ensureWindowBorder(window);
|
ensureWindowBorder(window);
|
||||||
applyWindowCornerRadius(window, gWindowCornerRadius);
|
applyWindowCornerRadius(window, gWindowCornerRadius);
|
||||||
|
ensureTrafficLightsObserver(window);
|
||||||
|
scheduleTrafficLightsPosition(window, 0);
|
||||||
|
|
||||||
[window invalidateShadow];
|
[window invalidateShadow];
|
||||||
success = YES;
|
success = YES;
|
||||||
@ -307,45 +447,13 @@ extern "C" bool setWindowTrafficLightsPosition(void *windowPtr, double x,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSButton *closeButton =
|
gTrafficLightsX = (CGFloat)x;
|
||||||
[window standardWindowButton:NSWindowCloseButton];
|
gTrafficLightsY = (CGFloat)yFromTop;
|
||||||
NSButton *minimizeButton =
|
ensureTrafficLightsObserver(window);
|
||||||
[window standardWindowButton:NSWindowMiniaturizeButton];
|
success = applyTrafficLightsPosition(window, gTrafficLightsX, gTrafficLightsY);
|
||||||
NSButton *zoomButton = [window standardWindowButton:NSWindowZoomButton];
|
if (success) {
|
||||||
|
[window invalidateShadow];
|
||||||
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];
|
|
||||||
[window invalidateShadow];
|
|
||||||
success = YES;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
|
|||||||
@ -1,20 +1,23 @@
|
|||||||
import { createAppRpc } from "../desktop/rpc.js";
|
import { createAppRpc } from '../desktop/rpc.js'
|
||||||
import { registerGlobalShortcuts, unregisterGlobalShortcuts } from "../desktop/spotlight.js";
|
import {
|
||||||
|
registerGlobalShortcuts,
|
||||||
|
unregisterGlobalShortcuts
|
||||||
|
} from '../desktop/spotlight.js'
|
||||||
import {
|
import {
|
||||||
createMainWindow,
|
createMainWindow,
|
||||||
handleDeepLinkFromArgv,
|
handleDeepLinkFromArgv,
|
||||||
setupDevAuthServer,
|
setupDevAuthServer,
|
||||||
setupNavigationGestures,
|
setupNavigationGestures
|
||||||
} from "../desktop/window.js";
|
} from '../desktop/window.js'
|
||||||
|
|
||||||
const rpc = createAppRpc();
|
const rpc = createAppRpc()
|
||||||
const mainWindow = await createMainWindow(rpc);
|
const mainWindow = await createMainWindow(rpc)
|
||||||
|
|
||||||
setupNavigationGestures(mainWindow);
|
setupNavigationGestures(mainWindow)
|
||||||
registerGlobalShortcuts(rpc);
|
registerGlobalShortcuts(rpc)
|
||||||
setupDevAuthServer();
|
setupDevAuthServer()
|
||||||
handleDeepLinkFromArgv();
|
handleDeepLinkFromArgv()
|
||||||
|
|
||||||
process.on("exit", () => {
|
process.on('exit', () => {
|
||||||
unregisterGlobalShortcuts();
|
unregisterGlobalShortcuts()
|
||||||
});
|
})
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useContext } from 'react'
|
import { useContext } from 'react'
|
||||||
import { Flex, Button } from 'antd'
|
import { Flex, Button, Divider } from 'antd'
|
||||||
import { ElectronContext } from '../context/ElectronContext'
|
import { ElectronContext } from '../context/ElectronContext'
|
||||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
import XMarkIcon from '../../Icons/XMarkIcon'
|
||||||
import MinusIcon from '../../Icons/MinusIcon'
|
import MinusIcon from '../../Icons/MinusIcon'
|
||||||
@ -36,7 +36,13 @@ const DashboardWindowButtons = () => {
|
|||||||
<Flex align='center'>
|
<Flex align='center'>
|
||||||
{platform == 'darwin' ? (
|
{platform == 'darwin' ? (
|
||||||
isFullScreen == false ? (
|
isFullScreen == false ? (
|
||||||
<div style={{ width: '80px' }} />
|
<>
|
||||||
|
<div style={{ width: '82px' }} />
|
||||||
|
<Divider
|
||||||
|
type='vertical'
|
||||||
|
style={{ height: '14px', margin: '3px 6px 0 0' }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
) : null
|
) : null
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,27 +1,16 @@
|
|||||||
import { createContext, useCallback, useEffect, useRef, useState } from 'react'
|
import { createContext, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import desktopBridge, {
|
||||||
|
isElectrobunDesktop
|
||||||
|
} from '../../../electrobun-bridge.js'
|
||||||
|
|
||||||
const electron = window.require ? window.require('electron') : null
|
const electron = window.require ? window.require('electron') : null
|
||||||
const ipcRenderer = electron ? electron.ipcRenderer : null
|
const ipcRenderer = electron ? electron.ipcRenderer : null
|
||||||
|
|
||||||
function getDesktopAPI() {
|
|
||||||
return window.electronAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export function isElectron() {
|
export function isElectron() {
|
||||||
const desktopAPI = getDesktopAPI()
|
if (isElectrobunDesktop()) {
|
||||||
|
|
||||||
if (desktopAPI?.isDesktop) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof window !== 'undefined' &&
|
|
||||||
window.__electrobunWebviewId &&
|
|
||||||
window.__electrobunRpcSocketPort
|
|
||||||
) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,12 +55,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
const [isMaximized, setIsMaximized] = useState(false)
|
const [isMaximized, setIsMaximized] = useState(false)
|
||||||
const [isFullScreen, setIsFullScreen] = useState(false)
|
const [isFullScreen, setIsFullScreen] = useState(false)
|
||||||
const [electronAvailable] = useState(isElectron())
|
const [electronAvailable] = useState(isElectron())
|
||||||
const useElectrobun = Boolean(
|
const useElectrobun = isElectrobunDesktop()
|
||||||
getDesktopAPI()?.isDesktop ||
|
|
||||||
(typeof window !== 'undefined' &&
|
|
||||||
window.__electrobunWebviewId &&
|
|
||||||
window.__electrobunRpcSocketPort)
|
|
||||||
)
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const lastNavigationAtRef = useRef(0)
|
const lastNavigationAtRef = useRef(0)
|
||||||
|
|
||||||
@ -101,7 +85,9 @@ const ElectronProvider = ({ children }) => {
|
|||||||
|
|
||||||
const openExternalUrl = (url) => {
|
const openExternalUrl = (url) => {
|
||||||
if (useElectrobun) {
|
if (useElectrobun) {
|
||||||
getDesktopAPI().openExternalUrl(url)
|
void desktopBridge.openExternalUrl(url).catch((error) => {
|
||||||
|
console.warn('[ElectronContext] Failed to open external url:', error)
|
||||||
|
})
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (electronAvailable && ipcRenderer) {
|
if (electronAvailable && ipcRenderer) {
|
||||||
@ -113,7 +99,9 @@ const ElectronProvider = ({ children }) => {
|
|||||||
|
|
||||||
const openInternalUrl = (url) => {
|
const openInternalUrl = (url) => {
|
||||||
if (useElectrobun) {
|
if (useElectrobun) {
|
||||||
getDesktopAPI().openInternalUrl(url)
|
void desktopBridge.openInternalUrl(url).catch((error) => {
|
||||||
|
console.warn('[ElectronContext] Failed to open internal url:', error)
|
||||||
|
})
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (electronAvailable && ipcRenderer) {
|
if (electronAvailable && ipcRenderer) {
|
||||||
@ -127,7 +115,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
if (!electronAvailable) return
|
if (!electronAvailable) return
|
||||||
|
|
||||||
if (useElectrobun) {
|
if (useElectrobun) {
|
||||||
getDesktopAPI().getOsInfo().then((info) => {
|
desktopBridge.getOsInfo().then((info) => {
|
||||||
if (info?.platform) {
|
if (info?.platform) {
|
||||||
setPlatform(info.platform)
|
setPlatform(info.platform)
|
||||||
if (info.platform === 'darwin') {
|
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',
|
'windowState',
|
||||||
applyWindowState
|
applyWindowState
|
||||||
)
|
)
|
||||||
const unsubNavigate = getDesktopAPI().onMessage('navigate', (url) => {
|
const unsubNavigate = desktopBridge.onMessage('navigate', (url) => {
|
||||||
navigate(url)
|
navigate(url)
|
||||||
})
|
})
|
||||||
const unsubNavigationGesture = getDesktopAPI().onMessage(
|
const unsubNavigationGesture = desktopBridge.onMessage(
|
||||||
'navigationGesture',
|
'navigationGesture',
|
||||||
navigateHistory
|
navigateHistory
|
||||||
)
|
)
|
||||||
@ -205,7 +193,8 @@ const ElectronProvider = ({ children }) => {
|
|||||||
let resetTimer
|
let resetTimer
|
||||||
|
|
||||||
const handleWheel = (event) => {
|
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) < Math.abs(event.deltaY)) return
|
||||||
if (Math.abs(event.deltaX) < 2) return
|
if (Math.abs(event.deltaX) < 2) return
|
||||||
if (isInHorizontalScrollContainer(event.target)) return
|
if (isInHorizontalScrollContainer(event.target)) return
|
||||||
@ -230,7 +219,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
|
|
||||||
const handleWindowControl = (action) => {
|
const handleWindowControl = (action) => {
|
||||||
if (useElectrobun) {
|
if (useElectrobun) {
|
||||||
getDesktopAPI().windowControl(action)
|
void desktopBridge.windowControl(action)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (electronAvailable && ipcRenderer) {
|
if (electronAvailable && ipcRenderer) {
|
||||||
@ -240,7 +229,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
|
|
||||||
const getAuthSession = async () => {
|
const getAuthSession = async () => {
|
||||||
if (!electronAvailable) return null
|
if (!electronAvailable) return null
|
||||||
if (useElectrobun) return await getDesktopAPI().getAuthSession()
|
if (useElectrobun) return await desktopBridge.getAuthSession()
|
||||||
if (!ipcRenderer) return null
|
if (!ipcRenderer) return null
|
||||||
return await ipcRenderer.invoke('auth-session-get')
|
return await ipcRenderer.invoke('auth-session-get')
|
||||||
}
|
}
|
||||||
@ -248,7 +237,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
const setAuthSession = async (session) => {
|
const setAuthSession = async (session) => {
|
||||||
if (!electronAvailable) return false
|
if (!electronAvailable) return false
|
||||||
if (useElectrobun) {
|
if (useElectrobun) {
|
||||||
const result = await getDesktopAPI().setAuthSession(session)
|
const result = await desktopBridge.setAuthSession(session)
|
||||||
return result?.ok ?? false
|
return result?.ok ?? false
|
||||||
}
|
}
|
||||||
if (!ipcRenderer) return false
|
if (!ipcRenderer) return false
|
||||||
@ -258,7 +247,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
const clearAuthSession = async () => {
|
const clearAuthSession = async () => {
|
||||||
if (!electronAvailable) return false
|
if (!electronAvailable) return false
|
||||||
if (useElectrobun) {
|
if (useElectrobun) {
|
||||||
const result = await getDesktopAPI().clearAuthSession()
|
const result = await desktopBridge.clearAuthSession()
|
||||||
return result?.ok ?? false
|
return result?.ok ?? false
|
||||||
}
|
}
|
||||||
if (!ipcRenderer) return false
|
if (!ipcRenderer) return false
|
||||||
@ -267,7 +256,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
|
|
||||||
const getAppSettings = useCallback(async () => {
|
const getAppSettings = useCallback(async () => {
|
||||||
if (!electronAvailable) return {}
|
if (!electronAvailable) return {}
|
||||||
if (useElectrobun) return await getDesktopAPI().getAppSettings()
|
if (useElectrobun) return await desktopBridge.getAppSettings()
|
||||||
if (!ipcRenderer) return {}
|
if (!ipcRenderer) return {}
|
||||||
return await ipcRenderer.invoke('app-settings-get')
|
return await ipcRenderer.invoke('app-settings-get')
|
||||||
}, [electronAvailable, useElectrobun])
|
}, [electronAvailable, useElectrobun])
|
||||||
@ -276,7 +265,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
async (settings) => {
|
async (settings) => {
|
||||||
if (!electronAvailable) return false
|
if (!electronAvailable) return false
|
||||||
if (useElectrobun) {
|
if (useElectrobun) {
|
||||||
const result = await getDesktopAPI().setAppSettings(settings)
|
const result = await desktopBridge.setAppSettings(settings)
|
||||||
return result?.ok ?? false
|
return result?.ok ?? false
|
||||||
}
|
}
|
||||||
if (!ipcRenderer) return false
|
if (!ipcRenderer) return false
|
||||||
@ -289,7 +278,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
async (update) => {
|
async (update) => {
|
||||||
if (!electronAvailable) return false
|
if (!electronAvailable) return false
|
||||||
if (useElectrobun) {
|
if (useElectrobun) {
|
||||||
const result = await getDesktopAPI().startAppUpdate(update)
|
const result = await desktopBridge.startAppUpdate(update)
|
||||||
return result?.ok ?? false
|
return result?.ok ?? false
|
||||||
}
|
}
|
||||||
if (!ipcRenderer) return false
|
if (!ipcRenderer) return false
|
||||||
@ -305,7 +294,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (useElectrobun) {
|
if (useElectrobun) {
|
||||||
return getDesktopAPI().onMessage('appUpdateProgress', handler)
|
return desktopBridge.onMessage('appUpdateProgress', handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ipcRenderer) return () => {}
|
if (!ipcRenderer) return () => {}
|
||||||
@ -337,7 +326,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
if (!electronAvailable) return false
|
if (!electronAvailable) return false
|
||||||
try {
|
try {
|
||||||
if (useElectrobun) {
|
if (useElectrobun) {
|
||||||
const result = await getDesktopAPI().resizeSpotlightWindow(height)
|
const result = await desktopBridge.resizeSpotlightWindow(height)
|
||||||
return result?.ok ?? false
|
return result?.ok ?? false
|
||||||
}
|
}
|
||||||
if (!ipcRenderer) return false
|
if (!ipcRenderer) return false
|
||||||
@ -355,7 +344,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
async (sections) => {
|
async (sections) => {
|
||||||
if (!electronAvailable) return false
|
if (!electronAvailable) return false
|
||||||
if (useElectrobun) {
|
if (useElectrobun) {
|
||||||
const result = await getDesktopAPI().setSidebarViewMenu(sections)
|
const result = await desktopBridge.setSidebarViewMenu(sections)
|
||||||
return result?.ok ?? false
|
return result?.ok ?? false
|
||||||
}
|
}
|
||||||
if (!ipcRenderer) return false
|
if (!ipcRenderer) return false
|
||||||
@ -366,7 +355,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
|
|
||||||
const getElectronVersion = useCallback(async () => {
|
const getElectronVersion = useCallback(async () => {
|
||||||
if (!electronAvailable) return null
|
if (!electronAvailable) return null
|
||||||
if (useElectrobun) return await getDesktopAPI().getAppVersion()
|
if (useElectrobun) return await desktopBridge.getAppVersion()
|
||||||
if (!ipcRenderer) return null
|
if (!ipcRenderer) return null
|
||||||
return await ipcRenderer.invoke('electron-version')
|
return await ipcRenderer.invoke('electron-version')
|
||||||
}, [electronAvailable, useElectrobun])
|
}, [electronAvailable, useElectrobun])
|
||||||
|
|||||||
@ -3,8 +3,6 @@ import { existsSync, statSync } from 'node:fs'
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
const MAC_TRAFFIC_LIGHTS_X = 14
|
|
||||||
const MAC_TRAFFIC_LIGHTS_Y = 12
|
|
||||||
export const MAC_WINDOW_CORNER_RADIUS = 15
|
export const MAC_WINDOW_CORNER_RADIUS = 15
|
||||||
|
|
||||||
function resolveDylibPath() {
|
function resolveDylibPath() {
|
||||||
@ -57,10 +55,6 @@ export function applyMacOSWindowEffects(mainWindow) {
|
|||||||
setWindowCornerRadius: {
|
setWindowCornerRadius: {
|
||||||
args: [FFIType.ptr, FFIType.f64],
|
args: [FFIType.ptr, FFIType.f64],
|
||||||
returns: FFIType.bool
|
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
|
MAC_WINDOW_CORNER_RADIUS
|
||||||
)
|
)
|
||||||
const shadowEnabled = lib.symbols.ensureWindowShadow(mainWindow.ptr)
|
const shadowEnabled = lib.symbols.ensureWindowShadow(mainWindow.ptr)
|
||||||
const alignButtons = () =>
|
|
||||||
lib.symbols.setWindowTrafficLightsPosition(
|
mainWindow.on?.('resize', () => {
|
||||||
mainWindow.ptr,
|
|
||||||
MAC_TRAFFIC_LIGHTS_X,
|
|
||||||
MAC_TRAFFIC_LIGHTS_Y
|
|
||||||
)
|
|
||||||
const refreshWindowChrome = () => {
|
|
||||||
lib.symbols.setWindowCornerRadius(
|
lib.symbols.setWindowCornerRadius(
|
||||||
mainWindow.ptr,
|
mainWindow.ptr,
|
||||||
MAC_WINDOW_CORNER_RADIUS
|
MAC_WINDOW_CORNER_RADIUS
|
||||||
)
|
)
|
||||||
alignButtons()
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonsAlignedNow = alignButtons()
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
refreshWindowChrome()
|
|
||||||
}, 120)
|
|
||||||
|
|
||||||
mainWindow.on?.('resize', () => {
|
|
||||||
refreshWindowChrome()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(
|
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) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
import { BrowserView } from "electrobun/bun";
|
import { BrowserView } from 'electrobun/bun'
|
||||||
import { startAppUpdate } from "./appupdate.js";
|
import { startAppUpdate } from './appupdate.js'
|
||||||
import { setSidebarViewMenu } from "./menu.js";
|
import { setSidebarViewMenu } from './menu.js'
|
||||||
import { sendToRenderer } from "./notify.js";
|
import { sendToRenderer } from './notify.js'
|
||||||
import { resizeSpotlightWindow } from "./spotlight.js";
|
import { resizeSpotlightWindow } from './spotlight.js'
|
||||||
import {
|
import {
|
||||||
clearAuthSession,
|
clearAuthSession,
|
||||||
getAppSettings,
|
getAppSettings,
|
||||||
getAuthSession,
|
getAuthSession,
|
||||||
setAppSettings,
|
setAppSettings,
|
||||||
setAuthSession,
|
setAuthSession
|
||||||
} from "./store.js";
|
} from './store.js'
|
||||||
import {
|
import {
|
||||||
getMainWindow,
|
getMainWindow,
|
||||||
getWindowState,
|
getWindowState,
|
||||||
handleWindowControl,
|
handleWindowControl,
|
||||||
openExternalUrl,
|
openExternalUrl,
|
||||||
openInternalUrl,
|
openInternalUrl
|
||||||
} from "./window.js";
|
} from './window.js'
|
||||||
|
|
||||||
export function createAppRpc() {
|
export function createAppRpc() {
|
||||||
return BrowserView.defineRPC({
|
return BrowserView.defineRPC({
|
||||||
@ -24,52 +24,53 @@ export function createAppRpc() {
|
|||||||
handlers: {
|
handlers: {
|
||||||
requests: {
|
requests: {
|
||||||
getOsInfo: async () => ({
|
getOsInfo: async () => ({
|
||||||
platform: process.platform,
|
platform: process.platform
|
||||||
}),
|
}),
|
||||||
getWindowState: async () => getWindowState(),
|
getWindowState: async () => getWindowState(),
|
||||||
windowControl: async ({ action }) => {
|
windowControl: async ({ action }) => {
|
||||||
handleWindowControl(action);
|
handleWindowControl(action)
|
||||||
return { ok: true };
|
return { ok: true }
|
||||||
},
|
},
|
||||||
openExternalUrl: async ({ url }) => {
|
openExternalUrl: async ({ url }) => {
|
||||||
openExternalUrl(url);
|
openExternalUrl(url)
|
||||||
return { ok: true };
|
console.log('openExternalUrl', url)
|
||||||
|
return { ok: true }
|
||||||
},
|
},
|
||||||
openInternalUrl: async ({ url }) => ({
|
openInternalUrl: async ({ url }) => ({
|
||||||
ok: openInternalUrl(url),
|
ok: openInternalUrl(url)
|
||||||
}),
|
}),
|
||||||
getAuthSession: async () => getAuthSession(),
|
getAuthSession: async () => getAuthSession(),
|
||||||
setAuthSession: async ({ session }) => ({
|
setAuthSession: async ({ session }) => ({
|
||||||
ok: await setAuthSession(session),
|
ok: await setAuthSession(session)
|
||||||
}),
|
}),
|
||||||
clearAuthSession: async () => ({
|
clearAuthSession: async () => ({
|
||||||
ok: await clearAuthSession(),
|
ok: await clearAuthSession()
|
||||||
}),
|
}),
|
||||||
getAppSettings: async () => getAppSettings(),
|
getAppSettings: async () => getAppSettings(),
|
||||||
setAppSettings: async ({ settings }) => ({
|
setAppSettings: async ({ settings }) => ({
|
||||||
ok: await setAppSettings(settings),
|
ok: await setAppSettings(settings)
|
||||||
}),
|
}),
|
||||||
startAppUpdate: async ({ update }) => {
|
startAppUpdate: async ({ update }) => {
|
||||||
const mainWindow = getMainWindow();
|
const mainWindow = getMainWindow()
|
||||||
const sendProgress = (payload) => {
|
const sendProgress = (payload) => {
|
||||||
sendToRenderer("appUpdateProgress", {
|
sendToRenderer('appUpdateProgress', {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
...payload,
|
...payload
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
await startAppUpdate(mainWindow, update, sendProgress);
|
await startAppUpdate(mainWindow, update, sendProgress)
|
||||||
return { ok: true };
|
return { ok: true }
|
||||||
},
|
},
|
||||||
resizeSpotlightWindow: async ({ height }) => ({
|
resizeSpotlightWindow: async ({ height }) => ({
|
||||||
ok: resizeSpotlightWindow(height),
|
ok: resizeSpotlightWindow(height)
|
||||||
}),
|
}),
|
||||||
setSidebarViewMenu: async ({ sections }) => ({
|
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 path from 'node:path'
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from 'node:url'
|
||||||
import { BrowserWindow, GlobalShortcut } from "electrobun/bun";
|
import { BrowserWindow, GlobalShortcut } from 'electrobun/bun'
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
const DEV_SERVER_PORT = 5780;
|
const DEV_SERVER_PORT = 5780
|
||||||
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
|
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
|
||||||
const SPOTLIGHT_ROUTE_PATH = "/dashboard/electron/spotlightcontent";
|
const SPOTLIGHT_ROUTE_PATH = '/dashboard/electron/spotlightcontent'
|
||||||
|
|
||||||
let spotlightWindow = null;
|
let spotlightWindow = null
|
||||||
|
|
||||||
function getSpotlightRouteUrl() {
|
function getSpotlightRouteUrl() {
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
return `${DEV_SERVER_URL}${SPOTLIGHT_ROUTE_PATH}`;
|
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) {
|
export function openSpotlightContentWindow(rpc) {
|
||||||
if (spotlightWindow && !spotlightWindow.isDestroyed?.()) {
|
if (spotlightWindow && !spotlightWindow.isDestroyed?.()) {
|
||||||
spotlightWindow.show?.();
|
spotlightWindow.show?.()
|
||||||
spotlightWindow.focus?.();
|
spotlightWindow.focus?.()
|
||||||
return spotlightWindow;
|
return spotlightWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
spotlightWindow = new BrowserWindow({
|
spotlightWindow = new BrowserWindow({
|
||||||
title: "Farm Control Spotlight",
|
title: 'Farm Control Spotlight',
|
||||||
url: getSpotlightRouteUrl(),
|
url: getSpotlightRouteUrl(),
|
||||||
rpc,
|
rpc,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
@ -35,69 +35,72 @@ export function openSpotlightContentWindow(rpc) {
|
|||||||
width: 700,
|
width: 700,
|
||||||
height: 40,
|
height: 40,
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 100,
|
y: 100
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
spotlightWindow.on?.("close", (event) => {
|
|
||||||
event?.preventDefault?.();
|
|
||||||
if (spotlightWindow && !spotlightWindow.isDestroyed?.()) {
|
|
||||||
spotlightWindow.hide?.();
|
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
spotlightWindow.on?.("blur", () => {
|
spotlightWindow.on?.('close', (event) => {
|
||||||
|
event?.preventDefault?.()
|
||||||
if (spotlightWindow && !spotlightWindow.isDestroyed?.()) {
|
if (spotlightWindow && !spotlightWindow.isDestroyed?.()) {
|
||||||
spotlightWindow.hide?.();
|
spotlightWindow.hide?.()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return spotlightWindow;
|
spotlightWindow.on?.('blur', () => {
|
||||||
|
if (spotlightWindow && !spotlightWindow.isDestroyed?.()) {
|
||||||
|
spotlightWindow.hide?.()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return spotlightWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSpotlightWindow() {
|
export function getSpotlightWindow() {
|
||||||
return spotlightWindow;
|
return spotlightWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerGlobalShortcuts(rpc) {
|
export function registerGlobalShortcuts(rpc) {
|
||||||
try {
|
try {
|
||||||
const registered = GlobalShortcut.register("Alt+Shift+Q", () => {
|
const registered = GlobalShortcut.register('Alt+Shift+Q', () => {
|
||||||
openSpotlightContentWindow(rpc);
|
openSpotlightContentWindow(rpc)
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!registered) {
|
if (!registered) {
|
||||||
console.warn("[globalShortcut] Failed to register Alt+Shift+Q");
|
console.warn('[globalShortcut] Failed to register Alt+Shift+Q')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[globalShortcut] Error registering Alt+Shift+Q",
|
'[globalShortcut] Error registering Alt+Shift+Q',
|
||||||
error?.message || error,
|
error?.message || error
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unregisterGlobalShortcuts() {
|
export function unregisterGlobalShortcuts() {
|
||||||
try {
|
try {
|
||||||
GlobalShortcut.unregisterAll();
|
GlobalShortcut.unregisterAll()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[globalShortcut] Error unregistering shortcuts",
|
'[globalShortcut] Error unregistering shortcuts',
|
||||||
error?.message || error,
|
error?.message || error
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resizeSpotlightWindow(height) {
|
export function resizeSpotlightWindow(height) {
|
||||||
if (!spotlightWindow || spotlightWindow.isDestroyed?.()) return false;
|
if (!spotlightWindow || spotlightWindow.isDestroyed?.()) return false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const frame = spotlightWindow.getFrame?.() || spotlightWindow.getBounds?.();
|
const frame = spotlightWindow.getFrame?.() || spotlightWindow.getBounds?.()
|
||||||
const width = frame?.width || 700;
|
const width = frame?.width || 700
|
||||||
spotlightWindow.setSize?.(width, height);
|
spotlightWindow.setSize?.(width, height)
|
||||||
spotlightWindow.center?.();
|
spotlightWindow.center?.()
|
||||||
return true;
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[spotlight] Failed to resize window.", error?.message || error);
|
console.warn(
|
||||||
return false;
|
'[spotlight] Failed to resize window.',
|
||||||
|
error?.message || error
|
||||||
|
)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,258 +1,264 @@
|
|||||||
import Electrobun, { BrowserWindow, Updater, Utils } from "electrobun/bun";
|
import Electrobun, { BrowserWindow, Updater, Utils } from 'electrobun/bun'
|
||||||
import { applyApplicationMenu, setupApplicationMenuEvents } from "./menu.js";
|
import { applyApplicationMenu, setupApplicationMenuEvents } from './menu.js'
|
||||||
import { applyMacOSWindowEffects } from "./macos-window-effects.js";
|
import { applyMacOSWindowEffects } from './macos-window-effects.js'
|
||||||
import { sendToRenderer, setMessageSender } from "./notify.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_PORT = 5780
|
||||||
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
|
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
|
||||||
const PROTOCOL_PREFIX = "farmcontrol://";
|
const PROTOCOL_PREFIX = 'farmcontrol://'
|
||||||
|
|
||||||
let mainWindow = null;
|
let mainWindow = null
|
||||||
let webviewDomReady = false;
|
let webviewDomReady = false
|
||||||
const pendingNavigations = [];
|
const pendingNavigations = []
|
||||||
|
|
||||||
export function getMainWindow() {
|
export function getMainWindow() {
|
||||||
return mainWindow;
|
return mainWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMainViewUrl() {
|
export async function getMainViewUrl() {
|
||||||
const channel = await Updater.localInfo.channel();
|
const channel = await Updater.localInfo.channel()
|
||||||
if (channel === "dev" || process.env.NODE_ENV === "development") {
|
if (channel === 'dev' || process.env.NODE_ENV === 'development') {
|
||||||
try {
|
try {
|
||||||
await fetch(DEV_SERVER_URL, { method: "HEAD" });
|
await fetch(DEV_SERVER_URL, { method: 'HEAD' })
|
||||||
console.log(`Using Vite dev server at ${DEV_SERVER_URL}`);
|
console.log(`Using Vite dev server at ${DEV_SERVER_URL}`)
|
||||||
return DEV_SERVER_URL;
|
return DEV_SERVER_URL
|
||||||
} catch {
|
} catch {
|
||||||
console.warn(
|
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) {
|
function deliverNavigation(redirectPath) {
|
||||||
sendToRenderer("navigate", redirectPath);
|
sendToRenderer('navigate', redirectPath)
|
||||||
mainWindow?.show?.();
|
mainWindow?.show?.()
|
||||||
mainWindow?.activate?.();
|
mainWindow?.activate?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
function flushPendingNavigations() {
|
function flushPendingNavigations() {
|
||||||
if (!mainWindow || !webviewDomReady) {
|
if (!mainWindow || !webviewDomReady) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
while (pendingNavigations.length > 0) {
|
while (pendingNavigations.length > 0) {
|
||||||
const redirectPath = pendingNavigations.shift();
|
const redirectPath = pendingNavigations.shift()
|
||||||
setTimeout(() => deliverNavigation(redirectPath), 100);
|
setTimeout(() => deliverNavigation(redirectPath), 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendNavigateToRenderer(redirectPath) {
|
function sendNavigateToRenderer(redirectPath) {
|
||||||
if (!redirectPath || typeof redirectPath !== "string") {
|
if (!redirectPath || typeof redirectPath !== 'string') {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mainWindow || !webviewDomReady) {
|
if (!mainWindow || !webviewDomReady) {
|
||||||
pendingNavigations.push(redirectPath);
|
pendingNavigations.push(redirectPath)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => deliverNavigation(redirectPath), 100);
|
setTimeout(() => deliverNavigation(redirectPath), 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleDeepLink(url) {
|
export function handleDeepLink(url) {
|
||||||
if (!url?.startsWith(`${PROTOCOL_PREFIX}app`)) return;
|
if (!url?.startsWith(`${PROTOCOL_PREFIX}app`)) return
|
||||||
const redirectPath = url.replace(`${PROTOCOL_PREFIX}app`, "") || "/";
|
const redirectPath = url.replace(`${PROTOCOL_PREFIX}app`, '') || '/'
|
||||||
sendNavigateToRenderer(redirectPath);
|
sendNavigateToRenderer(redirectPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
function findProtocolUrl(args) {
|
function findProtocolUrl(args) {
|
||||||
return args.find(
|
return args.find(
|
||||||
(arg) => typeof arg === "string" && arg.startsWith(PROTOCOL_PREFIX),
|
(arg) => typeof arg === 'string' && arg.startsWith(PROTOCOL_PREFIX)
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleDeepLinkFromArgv() {
|
export function handleDeepLinkFromArgv() {
|
||||||
if (process.platform === "darwin") return;
|
if (process.platform === 'darwin') return
|
||||||
const url = findProtocolUrl(process.argv);
|
const url = findProtocolUrl(process.argv)
|
||||||
if (url) handleDeepLink(url);
|
if (url) handleDeepLink(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastWindowState() {
|
||||||
|
sendToRenderer('windowState', getWindowState())
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupWindowEvents(window) {
|
function setupWindowEvents(window) {
|
||||||
window.on?.("maximize", () => {
|
// Electrobun emits resize/focus, not Electron's maximize/fullscreen events.
|
||||||
sendToRenderer("windowState", { isMaximized: true });
|
window.on?.('resize', broadcastWindowState)
|
||||||
});
|
window.on?.('focus', broadcastWindowState)
|
||||||
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 });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupMainWindowMessaging(window = mainWindow) {
|
export function setupMainWindowMessaging(window = mainWindow) {
|
||||||
if (!window) {
|
if (!window) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessageSender((channel, data) => {
|
setMessageSender((channel, data) => {
|
||||||
try {
|
try {
|
||||||
const send = window.webview?.rpc?.send;
|
const webview = window.webview
|
||||||
if (!send) {
|
const channelLiteral = JSON.stringify(channel)
|
||||||
console.warn(
|
const payloadLiteral = JSON.stringify(data ?? null)
|
||||||
`No RPC sender available for channel: ${channel}. Is the window ready?`,
|
|
||||||
);
|
// Prefer direct JS dispatch — WebSocket RPC pushes can be deferred by
|
||||||
return false;
|
// WKWebView until the next interaction during native window transitions.
|
||||||
|
if (webview?.executeJavascript) {
|
||||||
|
webview.executeJavascript(
|
||||||
|
`window.__farmcontrolDispatchRpcMessage?.(${channelLiteral}, ${payloadLiteral})`
|
||||||
|
)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
send[channel](data);
|
const send = webview?.rpc?.send
|
||||||
return true;
|
if (!send) {
|
||||||
|
console.warn(
|
||||||
|
`No RPC sender available for channel: ${channel}. Is the window ready?`
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
send[channel](data)
|
||||||
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to send RPC message on channel: ${channel}`, error);
|
console.warn(`Failed to send RPC message on channel: ${channel}`, error)
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createMainWindow(rpc) {
|
export async function createMainWindow(rpc) {
|
||||||
const url = await getMainViewUrl();
|
const url = await getMainViewUrl()
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
title: "Farm Control",
|
title: 'Farm Control',
|
||||||
url,
|
url,
|
||||||
rpc,
|
rpc,
|
||||||
titleBarStyle: "hiddenInset",
|
titleBarStyle: 'hiddenInset',
|
||||||
trafficLightOffset: { x: 14, y: 12 },
|
|
||||||
...(isMacOS ? { transparent: true } : {}),
|
...(isMacOS ? { transparent: true } : {}),
|
||||||
frame: {
|
frame: {
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 100,
|
y: 100
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
applyMacOSWindowEffects(mainWindow);
|
applyMacOSWindowEffects(mainWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
setupMainWindowMessaging(mainWindow);
|
setupMainWindowMessaging(mainWindow)
|
||||||
applyApplicationMenu();
|
applyApplicationMenu()
|
||||||
setupApplicationMenuEvents({
|
setupApplicationMenuEvents({
|
||||||
onNavigate: sendNavigateToRenderer,
|
onNavigate: sendNavigateToRenderer,
|
||||||
onToggleDevTools: () => {
|
onToggleDevTools: () => {
|
||||||
mainWindow?.webview?.toggleDevTools?.();
|
mainWindow?.webview?.toggleDevTools?.()
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setupWindowEvents(mainWindow);
|
|
||||||
|
|
||||||
Electrobun.events.on("open-url", (event) => {
|
|
||||||
const url = event?.data?.url;
|
|
||||||
if (url) {
|
|
||||||
handleDeepLink(url);
|
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
|
setupWindowEvents(mainWindow)
|
||||||
|
|
||||||
|
Electrobun.events.on('open-url', (event) => {
|
||||||
|
const url = event?.data?.url
|
||||||
|
if (url) {
|
||||||
|
handleDeepLink(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
mainWindow.webview.on("dom-ready", () => {
|
mainWindow.webview.on('dom-ready', () => {
|
||||||
webviewDomReady = true;
|
webviewDomReady = true
|
||||||
flushPendingNavigations();
|
flushPendingNavigations()
|
||||||
resolve(mainWindow);
|
resolve(mainWindow)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupDevAuthServer() {
|
export async function setupDevAuthServer() {
|
||||||
const env = (process.env.NODE_ENV || "development").trim();
|
const env = (process.env.NODE_ENV || 'development').trim()
|
||||||
if (env !== "development") return;
|
if (env !== 'development') return
|
||||||
|
|
||||||
const express = (await import("express")).default;
|
const express = (await import('express')).default
|
||||||
const app = express();
|
const app = express()
|
||||||
const port = 3500;
|
const port = 3500
|
||||||
|
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
const redirectPath = req.originalUrl;
|
const redirectPath = req.originalUrl
|
||||||
res.send(
|
res.send(`Open Farmcontrol to continue... (Redirect path: ${redirectPath})`)
|
||||||
`Open Farmcontrol to continue... (Redirect path: ${redirectPath})`,
|
sendNavigateToRenderer(redirectPath)
|
||||||
);
|
})
|
||||||
sendNavigateToRenderer(redirectPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(port, () => {});
|
app.listen(port, () => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openInternalUrl(url) {
|
export function openInternalUrl(url) {
|
||||||
sendNavigateToRenderer(url);
|
sendNavigateToRenderer(url)
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWindowState() {
|
export function getWindowState() {
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return { isFullScreen: false, isMaximized: false };
|
return { isFullScreen: false, isMaximized: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isFullScreen: mainWindow.isFullScreen?.() ?? false,
|
isFullScreen: mainWindow.isFullScreen?.() ?? false,
|
||||||
isMaximized: mainWindow.isMaximized?.() ?? false,
|
isMaximized: mainWindow.isMaximized?.() ?? false
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleWindowControl(action) {
|
export function handleWindowControl(action) {
|
||||||
if (!mainWindow) return;
|
if (!mainWindow) return
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "minimize":
|
case 'minimize':
|
||||||
mainWindow.minimize?.();
|
mainWindow.minimize?.()
|
||||||
break;
|
break
|
||||||
case "maximize":
|
case 'maximize':
|
||||||
if (mainWindow.isMaximized?.()) {
|
if (mainWindow.isMaximized?.()) {
|
||||||
mainWindow.unmaximize?.();
|
mainWindow.unmaximize?.()
|
||||||
} else {
|
} else {
|
||||||
mainWindow.maximize?.();
|
mainWindow.maximize?.()
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
case "close":
|
case 'close':
|
||||||
mainWindow.close?.();
|
mainWindow.close?.()
|
||||||
break;
|
break
|
||||||
default:
|
default:
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendNavigationGesture(direction) {
|
export function sendNavigationGesture(direction) {
|
||||||
sendToRenderer("navigationGesture", direction);
|
sendToRenderer('navigationGesture', direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupNavigationGestures(window) {
|
export function setupNavigationGestures(window) {
|
||||||
if (!window) return;
|
if (!window) return
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === 'darwin') {
|
||||||
window.on?.("swipe", (_event, direction) => {
|
window.on?.('swipe', (_event, direction) => {
|
||||||
if (direction === "left") {
|
if (direction === 'left') {
|
||||||
sendNavigationGesture("back");
|
sendNavigationGesture('back')
|
||||||
} else if (direction === "right") {
|
} else if (direction === 'right') {
|
||||||
sendNavigationGesture("forward");
|
sendNavigationGesture('forward')
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
window.on?.("app-command", (_event, command) => {
|
window.on?.('app-command', (_event, command) => {
|
||||||
if (command === "browser-backward") {
|
if (command === 'browser-backward') {
|
||||||
sendNavigationGesture("back");
|
sendNavigationGesture('back')
|
||||||
} else if (command === "browser-forward") {
|
} else if (command === 'browser-forward') {
|
||||||
sendNavigationGesture("forward");
|
sendNavigationGesture('forward')
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openExternalUrl(url) {
|
export function openExternalUrl(url) {
|
||||||
Utils.openExternal(url);
|
console.log('openExternalUrl', url)
|
||||||
|
Utils.openExternal(url)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,173 +1,191 @@
|
|||||||
const listeners = new Map();
|
const listeners = new Map()
|
||||||
const pendingMessages = new Map();
|
const pendingMessages = new Map()
|
||||||
let rpc = null;
|
let rpc = null
|
||||||
let initPromise = null;
|
let initPromise = null
|
||||||
let initialized = false;
|
let initialized = false
|
||||||
|
|
||||||
export function isElectrobunDesktop() {
|
export function isElectrobunDesktop() {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
typeof window !== "undefined" &&
|
typeof window !== 'undefined' &&
|
||||||
window.__electrobunWebviewId &&
|
window.__electrobunWebviewId &&
|
||||||
window.__electrobunRpcSocketPort,
|
window.__electrobunRpcSocketPort
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatchMessage(channel, data) {
|
function dispatchMessage(channel, data) {
|
||||||
const channelListeners = listeners.get(channel);
|
const channelListeners = listeners.get(channel)
|
||||||
if (!channelListeners?.size) {
|
if (!channelListeners?.size) {
|
||||||
if (!pendingMessages.has(channel)) {
|
if (!pendingMessages.has(channel)) {
|
||||||
pendingMessages.set(channel, []);
|
pendingMessages.set(channel, [])
|
||||||
}
|
}
|
||||||
pendingMessages.get(channel).push(data);
|
pendingMessages.get(channel).push(data)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const callback of channelListeners) {
|
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() {
|
async function setupRpc() {
|
||||||
// electrobun/view captures webview globals at import time — import only after
|
// electrobun/view captures webview globals at import time — import only after
|
||||||
// Electrobun preload has set them (Vite dev can evaluate modules earlier).
|
// 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({
|
rpc = Electroview.defineRPC({
|
||||||
maxRequestTime: 30000,
|
maxRequestTime: 30000,
|
||||||
handlers: {
|
handlers: {
|
||||||
requests: {},
|
requests: {},
|
||||||
messages: {
|
messages: {
|
||||||
"*": (channel, data) => {
|
'*': (channel, data) => {
|
||||||
dispatchMessage(channel, data);
|
dispatchMessage(channel, data)
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
new Electrobun.Electroview({ rpc });
|
new Electrobun.Electroview({ rpc })
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldWaitForElectrobun() {
|
function shouldWaitForElectrobun() {
|
||||||
if (isElectrobunDesktop()) {
|
if (isElectrobunDesktop()) {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === 'undefined') {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.__electrobunWebviewId) {
|
if (window.__electrobunWebviewId) {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const { protocol, hostname, port } = window.location;
|
const { protocol, hostname, port } = window.location
|
||||||
|
|
||||||
if (protocol === "views:") {
|
if (protocol === 'views:') {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vite dev server used by electrobun dev:app
|
// Vite dev server used by electrobun dev:app
|
||||||
if (hostname === "localhost" && port === "5780") {
|
if (hostname === 'localhost' && port === '5780') {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return Boolean(
|
return Boolean(
|
||||||
window.__electrobun ||
|
window.__electrobun ||
|
||||||
window.__electrobunEventBridge ||
|
window.__electrobunEventBridge ||
|
||||||
window.__electrobunInternalBridge,
|
window.__electrobunInternalBridge
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initElectrobunBridge() {
|
export async function initElectrobunBridge() {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!initPromise) {
|
if (!initPromise) {
|
||||||
initPromise = (async () => {
|
initPromise = (async () => {
|
||||||
if (!shouldWaitForElectrobun()) {
|
if (!shouldWaitForElectrobun()) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const deadline = Date.now() + 3000;
|
const deadline = Date.now() + 3000
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
if (isElectrobunDesktop()) {
|
if (isElectrobunDesktop()) {
|
||||||
await setupRpc();
|
await setupRpc()
|
||||||
window.electronAPI = electronAPI;
|
window.electronAPI = electronAPI
|
||||||
initialized = true;
|
initialized = true
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn(
|
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) {
|
function onMessage(channel, callback) {
|
||||||
if (!listeners.has(channel)) {
|
void initElectrobunBridge()
|
||||||
listeners.set(channel, new Set());
|
|
||||||
}
|
|
||||||
listeners.get(channel).add(callback);
|
|
||||||
|
|
||||||
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) {
|
if (queued?.length) {
|
||||||
pendingMessages.delete(channel);
|
pendingMessages.delete(channel)
|
||||||
for (const payload of queued) {
|
for (const payload of queued) {
|
||||||
callback(payload);
|
callback(payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
listeners.get(channel)?.delete(callback);
|
listeners.get(channel)?.delete(callback)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAllListeners(channel) {
|
function removeAllListeners(channel) {
|
||||||
listeners.delete(channel);
|
listeners.delete(channel)
|
||||||
pendingMessages.delete(channel);
|
pendingMessages.delete(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function invokeRequest(method, params = {}) {
|
async function invokeRequest(method, params = {}) {
|
||||||
await initElectrobunBridge();
|
await initElectrobunBridge()
|
||||||
|
|
||||||
if (!rpc?.request?.[method]) {
|
if (!rpc?.request) {
|
||||||
console.warn(`Unhandled RPC request: ${method}`);
|
console.warn(`Electrobun RPC unavailable for request: ${method}`)
|
||||||
return null;
|
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 = {
|
const electronAPI = {
|
||||||
get isDesktop() {
|
get isDesktop() {
|
||||||
return isElectrobunDesktop();
|
return isElectrobunDesktop()
|
||||||
},
|
},
|
||||||
onMessage,
|
onMessage,
|
||||||
removeAllListeners,
|
removeAllListeners,
|
||||||
getOsInfo: () => invokeRequest("getOsInfo"),
|
getOsInfo: () => invokeRequest('getOsInfo'),
|
||||||
getWindowState: () => invokeRequest("getWindowState"),
|
getWindowState: () => invokeRequest('getWindowState'),
|
||||||
windowControl: (action) => invokeRequest("windowControl", { action }),
|
windowControl: (action) => invokeRequest('windowControl', { action }),
|
||||||
openExternalUrl: (url) => invokeRequest("openExternalUrl", { url }),
|
openExternalUrl: (url) => invokeRequest('openExternalUrl', { url }),
|
||||||
openInternalUrl: (url) => invokeRequest("openInternalUrl", { url }),
|
openInternalUrl: (url) => invokeRequest('openInternalUrl', { url }),
|
||||||
getAuthSession: () => invokeRequest("getAuthSession"),
|
getAuthSession: () => invokeRequest('getAuthSession'),
|
||||||
setAuthSession: (session) => invokeRequest("setAuthSession", { session }),
|
setAuthSession: (session) => invokeRequest('setAuthSession', { session }),
|
||||||
clearAuthSession: () => invokeRequest("clearAuthSession"),
|
clearAuthSession: () => invokeRequest('clearAuthSession'),
|
||||||
getAppSettings: () => invokeRequest("getAppSettings"),
|
getAppSettings: () => invokeRequest('getAppSettings'),
|
||||||
setAppSettings: (settings) => invokeRequest("setAppSettings", { settings }),
|
setAppSettings: (settings) => invokeRequest('setAppSettings', { settings }),
|
||||||
startAppUpdate: (update) => invokeRequest("startAppUpdate", { update }),
|
startAppUpdate: (update) => invokeRequest('startAppUpdate', { update }),
|
||||||
resizeSpotlightWindow: (height) =>
|
resizeSpotlightWindow: (height) =>
|
||||||
invokeRequest("resizeSpotlightWindow", { height }),
|
invokeRequest('resizeSpotlightWindow', { height }),
|
||||||
setSidebarViewMenu: (sections) =>
|
setSidebarViewMenu: (sections) =>
|
||||||
invokeRequest("setSidebarViewMenu", { sections }),
|
invokeRequest('setSidebarViewMenu', { sections }),
|
||||||
getAppVersion: () => invokeRequest("getAppVersion"),
|
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