Files
VRCX/src-electron/main.js
玺朽 bed76e0ad8 Add custom macOS-style title bar (#1404)
Introduces a custom MacOSTitleBar component for macOS, updates the Electron window to use 'hiddenInset' titleBarStyle, and adjusts App.vue to conditionally render the new title bar and add appropriate padding. This improves the native look and feel on macOS platforms.
2025-10-15 04:22:23 +11:00

953 lines
28 KiB
JavaScript

require('hazardous');
const path = require('path');
const {
BrowserWindow,
ipcMain,
app,
Tray,
Menu,
dialog,
Notification,
nativeImage
} = require('electron');
const { spawn, spawnSync } = require('child_process');
const fs = require('fs');
const https = require('https');
//app.disableHardwareAcceleration();
const bundledDotNetPath = path.join(process.resourcesPath, 'dotnet-runtime');
if (fs.existsSync(bundledDotNetPath)) {
// Include bundled .NET runtime
process.env.DOTNET_ROOT = bundledDotNetPath;
process.env.PATH = `${bundledDotNetPath}:${process.env.PATH}`;
} else if (process.platform === 'darwin') {
const dotnetPath = path.join('/usr/local/share/dotnet');
const dotnetPathArm = path.join('/usr/local/share/dotnet/x64');
if (fs.existsSync(dotnetPathArm)) {
process.env.DOTNET_ROOT = dotnetPathArm;
process.env.PATH = `${dotnetPathArm}:${process.env.PATH}`;
} else if (fs.existsSync(dotnetPath)) {
process.env.DOTNET_ROOT = dotnetPath;
process.env.PATH = `${dotnetPath}:${process.env.PATH}`;
}
}
if (!isDotNetInstalled()) {
app.whenReady().then(() => {
dialog.showErrorBox(
'VRCX',
'Please install .NET 9.0 Runtime "dotnet-runtime-9.0" to run VRCX.'
);
app.quit();
});
}
const VRCX_URI_PREFIX = 'vrcx';
let isOverlayActive = false;
let appIsQuitting = false;
// Get launch arguments
let appImagePath = process.env.APPIMAGE;
const args = process.argv.slice(1);
const noInstall = args.includes('--no-install');
const x11 = args.includes('--x11');
const noDesktop = args.includes('--no-desktop');
const startup = args.includes('--startup');
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(VRCX_URI_PREFIX, process.execPath, [
path.resolve(process.argv[1])
]);
} else {
app.setAsDefaultProtocolClient(VRCX_URI_PREFIX);
}
}
const homePath = getHomePath();
tryRelaunchWithArgs(args);
tryCopyFromWinePrefix();
const rootDir = app.getAppPath();
const armPath = path.join(rootDir, 'build/Electron/VRCX-Electron-arm64.cjs');
if (fs.existsSync(armPath)) {
require(armPath);
} else {
require(path.join(rootDir, 'build/Electron/VRCX-Electron.cjs'));
}
const InteropApi = require('./InteropApi');
const interopApi = new InteropApi();
const WRIST_FRAME_WIDTH = 512;
const WRIST_FRAME_HEIGHT = 512;
const WRIST_FRAME_SIZE = WRIST_FRAME_WIDTH * WRIST_FRAME_HEIGHT * 4;
const WRIST_SHM_PATH = '/dev/shm/vrcx_wrist_overlay';
function createWristOverlayWindowShm() {
fs.writeFileSync(WRIST_SHM_PATH, Buffer.alloc(WRIST_FRAME_SIZE + 1));
}
const HMD_FRAME_WIDTH = 1024;
const HMD_FRAME_HEIGHT = 1024;
const HMD_FRAME_SIZE = HMD_FRAME_WIDTH * HMD_FRAME_HEIGHT * 4;
const HMD_SHM_PATH = '/dev/shm/vrcx_hmd_overlay';
function createHmdOverlayWindowShm() {
fs.writeFileSync(HMD_SHM_PATH, Buffer.alloc(HMD_FRAME_SIZE + 1));
}
const version = getVersion();
interopApi.getDotNetObject('ProgramElectron').PreInit(version, args);
interopApi.getDotNetObject('VRCXStorage').Load();
interopApi.getDotNetObject('ProgramElectron').Init();
interopApi.getDotNetObject('SQLite').Init();
interopApi.getDotNetObject('AppApiElectron').Init();
interopApi.getDotNetObject('Discord').Init();
interopApi.getDotNetObject('WebApi').Init();
interopApi.getDotNetObject('LogWatcher').Init();
interopApi.getDotNetObject('SystemMonitorElectron').Init();
interopApi.getDotNetObject('AppApiVrElectron').Init();
ipcMain.handle('callDotNetMethod', (event, className, methodName, args) => {
return interopApi.callMethod(className, methodName, args);
});
/** @type {BrowserWindow} */
let mainWindow = undefined;
const VRCXStorage = interopApi.getDotNetObject('VRCXStorage');
const hasAskedToMoveAppImage =
VRCXStorage.Get('VRCX_HasAskedToMoveAppImage') === 'true';
let isCloseToTray = VRCXStorage.Get('VRCX_CloseToTray') === 'true';
const gotTheLock = app.requestSingleInstanceLock();
const strip_vrcx_prefix_regex = new RegExp('^' + VRCX_URI_PREFIX + '://');
if (!gotTheLock) {
console.log('Another instance is already running. Exiting.');
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
if (mainWindow && commandLine.length >= 2) {
try {
mainWindow.webContents.send(
'launch-command',
commandLine
.pop()
.trim()
.replace(strip_vrcx_prefix_regex, '')
);
} catch (err) {
console.error('Error processing second-instance command:', err);
}
}
});
app.on('open-url', (event, url) => {
if (mainWindow && url) {
mainWindow.webContents.send(
'launch-command',
url.replace(strip_vrcx_prefix_regex, '')
);
}
});
}
ipcMain.handle('getArch', () => {
return process.arch.toString();
});
ipcMain.handle('applyWindowSettings', (event, position, size, state) => {
if (position) {
mainWindow.setPosition(parseInt(position.x), parseInt(position.y));
}
if (size) {
mainWindow.setSize(parseInt(size.width), parseInt(size.height));
}
if (state) {
if (state === '0') {
mainWindow.restore();
} else if (state === '1') {
mainWindow.restore();
} else if (state === '2') {
mainWindow.maximize();
}
}
});
ipcMain.handle('dialog:openFile', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [{ name: 'Images', extensions: ['png'] }]
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
ipcMain.handle('dialog:openDirectory', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory']
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
ipcMain.handle('notification:showNotification', (event, title, body, icon) => {
const notification = {
title,
body,
icon
};
new Notification(notification).show();
});
ipcMain.handle('app:restart', () => {
if (process.platform === 'linux') {
const options = {
execPath: process.execPath,
args: process.argv.slice(1)
};
if (appImagePath) {
options.execPath = appImagePath;
if (!x11 && !options.args.includes('--appimage-extract-and-run')) {
options.args.unshift('--appimage-extract-and-run');
}
}
app.relaunch(options);
app.exit(0);
} else {
app.relaunch();
app.quit();
}
});
ipcMain.handle('app:getWristOverlayWindow', () => {
if (wristOverlayWindow && wristOverlayWindow.webContents) {
return (
!wristOverlayWindow.webContents.isLoading() &&
wristOverlayWindow.webContents.isPainting()
);
}
return false;
});
ipcMain.handle('app:getHmdOverlayWindow', () => {
if (hmdOverlayWindow && hmdOverlayWindow.webContents) {
return (
!hmdOverlayWindow.webContents.isLoading() &&
hmdOverlayWindow.webContents.isPainting()
);
}
return false;
});
ipcMain.handle(
'app:updateVr',
(event, active, hmdOverlay, wristOverlay, menuButton, overlayHand) => {
if (!active) {
disposeOverlay();
return;
}
isOverlayActive = true;
if (!hmdOverlay) {
destroyHmdOverlayWindow();
} else if (active && !hmdOverlayWindow) {
createHmdOverlayWindowOffscreen();
}
if (!wristOverlay) {
destroyWristOverlayWindow();
} else if (active && !wristOverlayWindow) {
createWristOverlayWindowOffscreen();
}
}
);
function tryRelaunchWithArgs(args) {
if (
process.platform !== 'linux' ||
x11 ||
args.includes('--ozone-platform-hint=auto')
) {
return;
}
const fullArgs = ['--ozone-platform-hint=auto', ...args];
let execPath = process.execPath;
if (appImagePath) {
execPath = appImagePath;
fullArgs.unshift('--appimage-extract-and-run');
}
console.log('Relaunching with args:', fullArgs);
const child = spawn(execPath, fullArgs, {
detached: true,
stdio: 'inherit'
});
child.unref();
app.exit(0);
}
function createWindow() {
app.commandLine.appendSwitch('enable-speech-dispatcher');
const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0;
const y = parseInt(VRCXStorage.Get('VRCX_LocationY')) || 0;
const width = parseInt(VRCXStorage.Get('VRCX_SizeWidth')) || 1920;
const height = parseInt(VRCXStorage.Get('VRCX_SizeHeight')) || 1080;
mainWindow = new BrowserWindow({
x,
y,
width,
height,
icon: path.join(rootDir, 'VRCX.png'),
autoHideMenuBar: true,
titleBarStyle: 'hiddenInset',
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
applyWindowState();
const indexPath = path.join(rootDir, 'build/html/index.html');
mainWindow.loadFile(indexPath);
// add proxy config, doesn't work, thanks electron
// const proxy = VRCXStorage.Get('VRCX_Proxy');
// if (proxy) {
// session.setProxy(
// { proxyRules: proxy.replaceAll('://', '=') },
// function () {
// mainWindow.loadFile(indexPath);
// }
// );
// session.setProxy({
// proxyRules: proxy.replaceAll('://', '=')
// });
// }
// Open the DevTools.
// mainWindow.webContents.openDevTools()
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.control && input.key === '=') {
mainWindow.webContents.setZoomLevel(
mainWindow.webContents.getZoomLevel() + 1
);
}
});
mainWindow.webContents.on('zoom-changed', (event, zoomDirection) => {
const currentZoom = mainWindow.webContents.getZoomLevel();
if (zoomDirection === 'in') {
mainWindow.webContents.setZoomLevel(currentZoom + 1);
} else {
mainWindow.webContents.setZoomLevel(currentZoom - 1);
}
});
mainWindow.webContents.setVisualZoomLevelLimits(1, 5);
mainWindow.on('close', (event) => {
isCloseToTray = VRCXStorage.Get('VRCX_CloseToTray') === 'true';
if (isCloseToTray && !appIsQuitting) {
event.preventDefault();
mainWindow.hide();
} else {
app.quit();
}
});
mainWindow.on('resize', () => {
const [width, height] = mainWindow
.getSize()
.map((size) => size.toString());
mainWindow.webContents.send('setWindowSize', { width, height });
});
mainWindow.on('move', () => {
const [x, y] = mainWindow
.getPosition()
.map((coord) => coord.toString());
mainWindow.webContents.send('setWindowPosition', { x, y });
});
mainWindow.on('maximize', () => {
mainWindow.webContents.send('setWindowState', '2');
});
mainWindow.on('minimize', () => {
mainWindow.webContents.send('setWindowState', '1');
});
mainWindow.on('unmaximize', () => {
mainWindow.webContents.send('setWindowState', '0');
});
mainWindow.on('restore', () => {
mainWindow.webContents.send('setWindowState', '0');
});
mainWindow.on('focus', () => {
mainWindow.webContents.send('onBrowserFocus');
});
}
let wristOverlayWindow = undefined;
function createWristOverlayWindowOffscreen() {
if (!fs.existsSync(WRIST_SHM_PATH)) {
createWristOverlayWindowShm();
}
const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0;
const y = parseInt(VRCXStorage.Get('VRCX_LocationY')) || 0;
const width = WRIST_FRAME_WIDTH;
const height = WRIST_FRAME_HEIGHT;
wristOverlayWindow = new BrowserWindow({
x,
y,
width,
height,
icon: path.join(rootDir, 'VRCX.png'),
autoHideMenuBar: true,
transparent: true,
frame: false,
show: false,
webPreferences: {
offscreen: true,
preload: path.join(__dirname, 'preload.js')
}
});
wristOverlayWindow.webContents.setFrameRate(2);
const indexPath = path.join(rootDir, 'build/html/vr.html');
const fileUrl = `file://${indexPath}?wrist`;
wristOverlayWindow.loadURL(fileUrl, { userAgent: version });
// Use paint event for offscreen rendering
wristOverlayWindow.webContents.on('paint', (event, dirty, image) => {
const buffer = image.toBitmap();
//console.log('Captured wrist frame via paint event, size:', buffer.length);
writeWristFrame(buffer);
});
}
function writeWristFrame(imageBuffer) {
try {
const fd = fs.openSync(WRIST_SHM_PATH, 'r+');
const buffer = Buffer.alloc(WRIST_FRAME_SIZE + 1);
buffer[0] = 0; // not ready
imageBuffer.copy(buffer, 1, 0, WRIST_FRAME_SIZE);
buffer[0] = 1; // ready
fs.writeSync(fd, buffer);
fs.closeSync(fd);
//console.log('Wrote wrist frame to shared memory');
} catch (err) {
console.error('Error writing wrist frame to shared memory:', err);
}
}
function destroyWristOverlayWindow() {
if (wristOverlayWindow && !wristOverlayWindow.isDestroyed()) {
wristOverlayWindow.close();
}
wristOverlayWindow = undefined;
}
let hmdOverlayWindow = undefined;
function createHmdOverlayWindowOffscreen() {
if (!fs.existsSync(HMD_SHM_PATH)) {
createHmdOverlayWindowShm();
}
const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0;
const y = parseInt(VRCXStorage.Get('VRCX_LocationY')) || 0;
const width = HMD_FRAME_WIDTH;
const height = HMD_FRAME_HEIGHT;
hmdOverlayWindow = new BrowserWindow({
x,
y,
width,
height,
icon: path.join(rootDir, 'VRCX.png'),
autoHideMenuBar: true,
transparent: true,
frame: false,
show: false,
webPreferences: {
offscreen: true,
preload: path.join(__dirname, 'preload.js')
}
});
hmdOverlayWindow.webContents.setFrameRate(48);
const indexPath = path.join(rootDir, 'build/html/vr.html');
const fileUrl = `file://${indexPath}?hmd`;
hmdOverlayWindow.loadURL(fileUrl, { userAgent: version });
// Use paint event for offscreen rendering
hmdOverlayWindow.webContents.on('paint', (event, dirty, image) => {
const buffer = image.toBitmap();
//console.log('Captured HMD frame via paint event, size:', buffer.length);
writeHmdFrame(buffer);
});
}
function writeHmdFrame(imageBuffer) {
try {
const fd = fs.openSync(HMD_SHM_PATH, 'r+');
const buffer = Buffer.alloc(HMD_FRAME_SIZE + 1);
buffer[0] = 0; // not ready
imageBuffer.copy(buffer, 1, 0, HMD_FRAME_SIZE);
buffer[0] = 1; // ready
fs.writeSync(fd, buffer);
fs.closeSync(fd);
//console.log('Wrote HMD frame to shared memory');
} catch (err) {
console.error('Error writing HMD frame to shared memory:', err);
}
}
function destroyHmdOverlayWindow() {
if (hmdOverlayWindow && !hmdOverlayWindow.isDestroyed()) {
hmdOverlayWindow.close();
}
hmdOverlayWindow = undefined;
}
function createTray() {
let tray = null;
if (process.platform === 'darwin') {
const image = nativeImage.createFromPath(
path.join(rootDir, 'images/VRCX.png')
);
tray = new Tray(image.resize({ width: 16, height: 16 }));
} else if (process.platform === 'linux') {
const image = nativeImage.createFromPath(
path.join(rootDir, 'images/VRCX.png')
);
tray = new Tray(image.resize({ width: 64, height: 64 }));
} else {
tray = new Tray(path.join(rootDir, 'images/VRCX.ico'));
}
const contextMenu = Menu.buildFromTemplate([
{
label: 'Open',
type: 'normal',
click: function () {
mainWindow.show();
}
},
{
label: 'DevTools',
type: 'normal',
click: function () {
mainWindow.webContents.openDevTools();
}
},
{
label: 'Quit VRCX',
type: 'normal',
click: function () {
appIsQuitting = true;
app.quit();
}
}
]);
tray.setToolTip('VRCX');
tray.setContextMenu(contextMenu);
tray.on('click', () => {
mainWindow.show();
});
}
async function installVRCX() {
console.log('Home path:', homePath);
console.log('AppImage path:', appImagePath);
if (!appImagePath) {
console.error('AppImage path is not available!');
return;
}
if (noInstall) {
interopApi.getDotNetObject('Update').Init(appImagePath);
console.log('Skipping installation.');
return;
}
// rename AppImage to VRCX.AppImage
const currentName = path.basename(appImagePath);
const expectedName = 'VRCX.AppImage';
if (currentName !== expectedName) {
const newPath = path.join(path.dirname(appImagePath), expectedName);
try {
// remove existing VRCX.AppImage
if (fs.existsSync(newPath)) {
fs.unlinkSync(newPath);
}
fs.renameSync(appImagePath, newPath);
console.log('AppImage renamed to:', newPath);
appImagePath = newPath;
} catch (err) {
console.error(`Error renaming AppImage ${newPath}`, err);
dialog.showErrorBox('VRCX', `Failed to rename AppImage ${newPath}`);
return;
}
}
// ask to move AppImage to ~/Applications
const appImageHomePath = `${homePath}/Applications/VRCX.AppImage`;
if (!hasAskedToMoveAppImage && appImagePath !== appImageHomePath) {
const result = dialog.showMessageBoxSync(mainWindow, {
type: 'question',
title: 'VRCX',
message: 'Do you want to install VRCX?',
detail: 'VRCX will be moved to your ~/Applications folder.',
buttons: ['No', 'Yes']
});
if (result === 0) {
console.log('Cancel AppImage move to ~/Applications');
// don't ask again
VRCXStorage.Set('VRCX_HasAskedToMoveAppImage', 'true');
VRCXStorage.Save();
}
if (result === 1) {
console.log('Moving AppImage to ~/Applications');
try {
const applicationsPath = path.join(homePath, 'Applications');
// create ~/Applications if it doesn't exist
if (!fs.existsSync(applicationsPath)) {
fs.mkdirSync(applicationsPath);
}
// remove existing VRCX.AppImage
if (fs.existsSync(appImageHomePath)) {
fs.unlinkSync(appImageHomePath);
}
fs.renameSync(appImagePath, appImageHomePath);
appImagePath = appImageHomePath;
console.log('AppImage moved to:', appImageHomePath);
} catch (err) {
console.error(`Error moving AppImage ${appImageHomePath}`, err);
dialog.showErrorBox(
'VRCX',
`Failed to move AppImage ${appImageHomePath}`
);
return;
}
}
}
// inform .NET side about AppImage path
interopApi.getDotNetObject('Update').Init(appImagePath);
await createDesktopFile();
}
async function createDesktopFile() {
if (noDesktop) {
console.log('Skipping desktop file creation.');
return;
}
// Download the icon and save it to the target directory
const iconPath = path.join(homePath, '.local/share/icons/VRCX.png');
if (!fs.existsSync(iconPath)) {
const iconDir = path.dirname(iconPath);
if (!fs.existsSync(iconDir)) {
fs.mkdirSync(iconDir, { recursive: true });
}
const iconUrl =
'https://raw.githubusercontent.com/vrcx-team/VRCX/master/VRCX.png';
await downloadIcon(iconUrl, iconPath)
.then(() => {
console.log('Icon downloaded and saved to:', iconPath);
})
.catch((err) => {
console.error('Error downloading icon:', err);
dialog.showErrorBox('VRCX', 'Failed to download the icon.');
});
}
// Create the desktop file
const desktopFilePath = path.join(
homePath,
'.local/share/applications/VRCX.desktop'
);
const dotDesktop = {
Name: 'VRCX',
Version: version,
Comment: 'Friendship management tool for VRChat',
Exec: `${appImagePath} --ozone-platform-hint=auto %U`,
Icon: 'VRCX',
Type: 'Application',
Categories: 'Network;InstantMessaging;Game;',
Terminal: 'false',
StartupWMClass: 'VRCX',
MimeType: 'x-scheme-handler/vrcx;'
};
const desktopFile =
'[Desktop Entry]\n' +
Object.entries(dotDesktop)
.map(([key, value]) => `${key}=${value}`)
.join('\n');
try {
// Create the applications directory if it doesn't exist
const desktopDir = path.dirname(desktopFilePath);
if (!fs.existsSync(desktopDir)) {
fs.mkdirSync(desktopDir, { recursive: true });
}
// Create/update the desktop file when needed
let existingDesktopFile = '';
if (fs.existsSync(desktopFilePath)) {
existingDesktopFile = fs.readFileSync(desktopFilePath, 'utf8');
}
if (existingDesktopFile !== desktopFile) {
fs.writeFileSync(desktopFilePath, desktopFile);
console.log('Desktop file created at:', desktopFilePath);
const result = spawnSync(
'xdg-mime',
['default', 'VRCX.desktop', 'x-scheme-handler/vrcx'],
{
encoding: 'utf-8'
}
);
if (result.error) {
console.error('Error setting MIME type:', result.error);
} else {
console.log('MIME type set x-scheme-handler/vrcx');
}
}
} catch (err) {
console.error('Error creating desktop file:', err);
dialog.showErrorBox('VRCX', 'Failed to create desktop entry.');
return;
}
}
function downloadIcon(url, targetPath) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(targetPath);
https
.get(url, (response) => {
if (response.statusCode !== 200) {
reject(
new Error(
`Failed to download icon, status code: ${response.statusCode}`
)
);
return;
}
response.pipe(file);
file.on('finish', () => {
file.close(resolve);
});
})
.on('error', (err) => {
fs.unlink(targetPath, () => reject(err)); // Delete the file if error occurs
});
});
}
function getVRCXPath() {
if (process.platform === 'win32') {
return path.join(process.env.APPDATA, 'VRCX');
} else if (process.platform === 'linux') {
return path.join(process.env.HOME, '.config/VRCX');
} else if (process.platform === 'darwin') {
return path.join(process.env.HOME, 'Library/Application Support/VRCX');
}
return '';
}
function getHomePath() {
const relativeHomePath = path.join(app.getPath('home'));
try {
const absoluteHomePath = fs.realpathSync(relativeHomePath);
return absoluteHomePath;
} catch (err) {
console.error('Error resolving absolute home path:', err);
return relativeHomePath;
}
}
function getVersion() {
try {
const versionFile = fs
.readFileSync(path.join(rootDir, 'Version'), 'utf8')
.trim();
// look for trailing git hash "-22bcd96" to indicate nightly build
const version = versionFile.split('-');
console.log('Version:', versionFile);
if (version.length > 0 && version[version.length - 1].length == 7) {
return `VRCX (Linux) Nightly ${versionFile}`;
} else {
return `VRCX (Linux) ${versionFile}`;
}
} catch (err) {
console.error('Error reading Version:', err);
return 'VRCX (Linux) Nightly Build';
}
}
function isDotNetInstalled() {
let dotnetPath;
if (process.env.DOTNET_ROOT) {
dotnetPath = path.join(process.env.DOTNET_ROOT, 'dotnet');
if (!fs.existsSync(dotnetPath)) {
// fallback to command
dotnetPath = 'dotnet';
}
} else {
// fallback to command
dotnetPath = 'dotnet';
}
console.log('Checking for .NET installation at:', dotnetPath);
// Fallback to system .NET runtime
const result = spawnSync(dotnetPath, ['--list-runtimes'], {
encoding: 'utf-8'
});
if (result.error) {
console.error('Error checking .NET runtimes:', result.error);
return false;
}
return result.stdout?.includes('.NETCore.App 9.0');
}
function tryCopyFromWinePrefix() {
try {
if (!fs.existsSync(getVRCXPath())) {
// try copy from old wine path
const userName = process.env.USER || process.env.USERNAME;
const oldPath = path.join(
homePath,
'.local/share/vrcx/drive_c/users',
userName,
'AppData/Roaming/VRCX'
);
const newPath = getVRCXPath();
if (fs.existsSync(oldPath)) {
fs.mkdirSync(newPath, { recursive: true });
const files = fs.readdirSync(oldPath);
for (const file of files) {
const oldFilePath = path.join(oldPath, file);
const newFilePath = path.join(newPath, file);
if (fs.lstatSync(oldFilePath).isDirectory()) {
continue;
}
fs.copyFileSync(oldFilePath, newFilePath);
}
}
}
} catch (err) {
console.error('Error copying from wine prefix:', err);
dialog.showErrorBox(
'VRCX',
'Failed to copy database from wine prefix.'
);
}
}
function applyWindowState() {
if (VRCXStorage.Get('VRCX_StartAsMinimizedState') === 'true' && startup) {
if (isCloseToTray) {
mainWindow.hide();
return;
}
mainWindow.minimize();
return;
}
const windowState = parseInt(VRCXStorage.Get('VRCX_WindowState')) || -1;
switch (windowState) {
case -1:
break;
case 0:
mainWindow.restore();
break;
case 1:
mainWindow.minimize();
break;
case 2:
mainWindow.maximize();
break;
}
}
app.whenReady().then(() => {
createWindow();
createTray();
if (process.platform === 'linux') {
try {
createWristOverlayWindowOffscreen();
createHmdOverlayWindowOffscreen();
} catch (err) {
console.error('Error creating overlay windows:', err);
}
}
installVRCX();
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
function disposeOverlay() {
if (!isOverlayActive) {
return;
}
isOverlayActive = false;
if (wristOverlayWindow) {
wristOverlayWindow.close();
wristOverlayWindow = undefined;
}
if (hmdOverlayWindow) {
hmdOverlayWindow.close();
hmdOverlayWindow = undefined;
}
if (fs.existsSync(WRIST_SHM_PATH)) {
fs.unlinkSync(WRIST_SHM_PATH);
}
if (fs.existsSync(HMD_SHM_PATH)) {
fs.unlinkSync(HMD_SHM_PATH);
}
}
app.on('before-quit', function () {
disposeOverlay();
});
app.on('window-all-closed', function () {
disposeOverlay();
if (process.platform !== 'darwin') {
app.quit();
}
});