mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-09 18:03:49 +02:00
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.
953 lines
28 KiB
JavaScript
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();
|
|
}
|
|
});
|