mirror of
https://github.com/vrcx-team/VRCX.git
synced 2026-04-06 00:32:02 +02:00
940 lines
28 KiB
JavaScript
940 lines
28 KiB
JavaScript
require('hazardous');
|
|
const path = require('path');
|
|
const {
|
|
BrowserWindow,
|
|
ipcMain,
|
|
app,
|
|
clipboard,
|
|
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;
|
|
const rootDir = app.getAppPath();
|
|
|
|
let tray = null;
|
|
let trayIcon = null;
|
|
let trayIconNotify = null;
|
|
|
|
// 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');
|
|
const debug = args.includes('--hot-reload');
|
|
const noUpdater =
|
|
args.includes('--no-updater') ||
|
|
fs.existsSync(path.join(rootDir, '.no-updater'));
|
|
if (process.defaultApp && process.platform !== 'win32') {
|
|
if (process.argv.length >= 2) {
|
|
app.setAsDefaultProtocolClient(VRCX_URI_PREFIX, process.execPath, [
|
|
path.resolve(process.argv[1])
|
|
]);
|
|
} else {
|
|
app.setAsDefaultProtocolClient(VRCX_URI_PREFIX);
|
|
}
|
|
}
|
|
|
|
const version = getVersion();
|
|
const homePath = getHomePath();
|
|
tryRelaunchWithArgs(args);
|
|
tryCopyFromWinePrefix();
|
|
const userDataPath = getElectronUserDataPath();
|
|
console.log('Electron userData path:', userDataPath);
|
|
if (!fs.existsSync(userDataPath)) {
|
|
fs.mkdirSync(userDataPath, { recursive: true });
|
|
}
|
|
app.setPath('userData', userDataPath);
|
|
|
|
const armPath = path.join(rootDir, 'build/Electron/VRCX-Electron-arm64.cjs');
|
|
if (process.arch === 'arm64' && fs.existsSync(armPath)) {
|
|
require(armPath);
|
|
} else {
|
|
require(path.join(rootDir, 'build/Electron/VRCX-Electron.cjs'));
|
|
}
|
|
|
|
const InteropApi = require('./InteropApi');
|
|
const interopApi = new InteropApi();
|
|
|
|
const OVERLAY_WRIST_FRAME_WIDTH = 512;
|
|
const OVERLAY_WRIST_FRAME_HEIGHT = 512;
|
|
const OVERLAY_HMD_FRAME_WIDTH = 1024;
|
|
const OVERLAY_HMD_FRAME_HEIGHT = 1024;
|
|
const OVERLAY_SHARED_HEIGHT =
|
|
OVERLAY_WRIST_FRAME_HEIGHT + OVERLAY_HMD_FRAME_HEIGHT;
|
|
const OVERLAY_SHARED_WIDTH = Math.max(
|
|
OVERLAY_WRIST_FRAME_WIDTH,
|
|
OVERLAY_HMD_FRAME_WIDTH
|
|
);
|
|
const OVERLAY_FRAME_SIZE = OVERLAY_SHARED_WIDTH * OVERLAY_SHARED_HEIGHT * 4;
|
|
const OVERLAY_SHM_PATH = '/dev/shm/vrcx_overlay';
|
|
|
|
function createOverlayWindowShm() {
|
|
fs.writeFileSync(OVERLAY_SHM_PATH, Buffer.alloc(OVERLAY_FRAME_SIZE + 1));
|
|
}
|
|
|
|
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';
|
|
|
|
function getCloseToTray() {
|
|
if (process.platform === 'darwin') {
|
|
return true;
|
|
}
|
|
return 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('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);
|
|
destroyTray();
|
|
app.exit(0);
|
|
} else {
|
|
app.relaunch();
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('app:getOverlayWindow', () => {
|
|
if (overlayWindow && overlayWindow.webContents) {
|
|
return (
|
|
!overlayWindow.webContents.isLoading() &&
|
|
overlayWindow.webContents.isPainting()
|
|
);
|
|
}
|
|
return false;
|
|
});
|
|
|
|
ipcMain.handle(
|
|
'app:updateVr',
|
|
(event, active, hmdOverlay, wristOverlay, menuButton, overlayHand) => {
|
|
if (!active || (!hmdOverlay && !wristOverlay)) {
|
|
disposeOverlay();
|
|
return;
|
|
}
|
|
if (active && !overlayWindow) {
|
|
try {
|
|
createOverlayWindowOffscreen();
|
|
} catch (err) {
|
|
console.error('Error creating overlay windows:', err);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
ipcMain.handle('app:getArch', () => {
|
|
return process.arch.toString();
|
|
});
|
|
ipcMain.handle('app:getClipboardText', () => {
|
|
return clipboard.readText();
|
|
});
|
|
|
|
ipcMain.handle('app:getNoUpdater', () => {
|
|
return noUpdater;
|
|
});
|
|
|
|
ipcMain.handle('app:setTrayIconNotification', (event, notify) => {
|
|
setTrayIconNotification(notify);
|
|
});
|
|
|
|
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();
|
|
|
|
destroyTray();
|
|
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;
|
|
const zoomLevel = parseFloat(VRCXStorage.Get('VRCX_ZoomLevel')) || 0;
|
|
mainWindow = new BrowserWindow({
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
icon: path.join(rootDir, 'images/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);
|
|
if (debug) {
|
|
mainWindow.loadURL('http://localhost:9000/index.html');
|
|
mainWindow.webContents.openDevTools();
|
|
}
|
|
|
|
// 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('did-finish-load', () => {
|
|
mainWindow.webContents.setZoomLevel(zoomLevel);
|
|
});
|
|
|
|
mainWindow.webContents.on('before-input-event', (event, input) => {
|
|
if (input.control && input.key === '=') {
|
|
mainWindow.webContents.setZoomLevel(
|
|
mainWindow.webContents.getZoomLevel() + 1
|
|
);
|
|
}
|
|
if (input.control && input.key === '-') {
|
|
mainWindow.webContents.setZoomLevel(
|
|
mainWindow.webContents.getZoomLevel() - 1
|
|
);
|
|
}
|
|
});
|
|
|
|
mainWindow.webContents.on('zoom-changed', (event, zoomDirection) => {
|
|
let currentZoom = mainWindow.webContents.getZoomLevel();
|
|
if (zoomDirection === 'in') {
|
|
mainWindow.webContents.setZoomLevel(++currentZoom);
|
|
} else {
|
|
mainWindow.webContents.setZoomLevel(--currentZoom);
|
|
}
|
|
VRCXStorage.Set('VRCX_ZoomLevel', currentZoom.toString());
|
|
});
|
|
mainWindow.webContents.setVisualZoomLevelLimits(1, 5);
|
|
|
|
mainWindow.on('close', (event) => {
|
|
if (getCloseToTray() && !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 overlayWindow = undefined;
|
|
|
|
function createOverlayWindowOffscreen() {
|
|
if (process.platform !== 'linux') {
|
|
console.error('Offscreen overlay is only supported on Linux.');
|
|
return;
|
|
}
|
|
isOverlayActive = true;
|
|
if (!fs.existsSync(OVERLAY_SHM_PATH)) {
|
|
createOverlayWindowShm();
|
|
}
|
|
|
|
const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0;
|
|
const y = parseInt(VRCXStorage.Get('VRCX_LocationY')) || 0;
|
|
const width = OVERLAY_SHARED_WIDTH;
|
|
const height = OVERLAY_SHARED_HEIGHT;
|
|
|
|
overlayWindow = new BrowserWindow({
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
icon: path.join(rootDir, 'images/VRCX.png'),
|
|
autoHideMenuBar: true,
|
|
transparent: true,
|
|
frame: false,
|
|
show: false,
|
|
webPreferences: {
|
|
partition: 'vrcx-vr-overlay',
|
|
offscreen: true,
|
|
preload: path.join(__dirname, 'preload.js')
|
|
}
|
|
});
|
|
overlayWindow.webContents.setFrameRate(48);
|
|
|
|
let fileUrl = `file://${path.join(rootDir, 'build/html/vr.html')}`;
|
|
if (debug) {
|
|
fileUrl = 'http://localhost:9000/vr.html';
|
|
}
|
|
overlayWindow.loadURL(fileUrl, { userAgent: version });
|
|
// Use paint event for offscreen rendering
|
|
overlayWindow.webContents.on('paint', (event, dirty, image) => {
|
|
const buffer = image.toBitmap();
|
|
//console.log('Captured frame via paint event, size:', buffer.length);
|
|
writeOverlayFrame(buffer);
|
|
});
|
|
}
|
|
|
|
function writeOverlayFrame(imageBuffer) {
|
|
try {
|
|
const fd = fs.openSync(OVERLAY_SHM_PATH, 'r+');
|
|
const buffer = Buffer.alloc(OVERLAY_FRAME_SIZE + 1);
|
|
buffer[0] = 0; // not ready
|
|
imageBuffer.copy(buffer, 1, 0, OVERLAY_FRAME_SIZE);
|
|
buffer[0] = 1; // ready
|
|
fs.writeSync(fd, buffer);
|
|
fs.closeSync(fd);
|
|
//console.log('Wrote frame to shared memory');
|
|
} catch (err) {
|
|
console.error('Error writing frame to shared memory:', err);
|
|
}
|
|
}
|
|
|
|
|
|
function destroyTray() {
|
|
if (tray) {
|
|
tray.destroy();
|
|
tray = null;
|
|
}
|
|
}
|
|
function createTray() {
|
|
if (process.platform === 'darwin') {
|
|
const image = nativeImage.createFromPath(
|
|
path.join(rootDir, 'images/VRCX.png')
|
|
);
|
|
trayIcon = image.resize({ width: 16, height: 16 });
|
|
|
|
const imageNotify = nativeImage.createFromPath(
|
|
path.join(rootDir, 'images/VRCX_notify.png')
|
|
);
|
|
trayIconNotify = imageNotify.resize({ width: 16, height: 16 });
|
|
} else if (process.platform === 'linux') {
|
|
const image = nativeImage.createFromPath(
|
|
path.join(rootDir, 'images/VRCX.png')
|
|
);
|
|
trayIcon = image.resize({ width: 64, height: 64 });
|
|
|
|
const imageNotify = nativeImage.createFromPath(
|
|
path.join(rootDir, 'images/VRCX_notify.png')
|
|
);
|
|
trayIconNotify = imageNotify.resize({ width: 64, height: 64 });
|
|
} else {
|
|
trayIcon = path.join(rootDir, 'images/VRCX.ico');
|
|
trayIconNotify = path.join(rootDir, 'images/VRCX_notify.ico');
|
|
}
|
|
tray = new Tray(trayIcon);
|
|
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();
|
|
});
|
|
}
|
|
|
|
function setTrayIconNotification(notify) {
|
|
if (tray) {
|
|
tray.setImage(notify ? trayIconNotify : trayIcon);
|
|
}
|
|
}
|
|
|
|
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) || fs.statSync(iconPath).size === 0) {
|
|
const iconDir = path.dirname(iconPath);
|
|
if (!fs.existsSync(iconDir)) {
|
|
fs.mkdirSync(iconDir, { recursive: true });
|
|
}
|
|
const iconUrl =
|
|
'https://raw.githubusercontent.com/vrcx-team/VRCX/master/images/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 getElectronUserDataPath() {
|
|
const electronUserData = 'ElectronUserData';
|
|
if (process.platform === 'win32') {
|
|
return path.join(getVRCXPath(), electronUserData);
|
|
}
|
|
if (process.platform === 'darwin') {
|
|
return path.join(
|
|
process.env.HOME,
|
|
'Library/Caches/VRCX',
|
|
electronUserData
|
|
);
|
|
}
|
|
// Linux or other
|
|
let cacheHome = process.env.XDG_CACHE_HOME;
|
|
if (!cacheHome) {
|
|
cacheHome = path.join(process.env.HOME, '.cache');
|
|
}
|
|
return path.join(cacheHome, 'VRCX', electronUserData);
|
|
}
|
|
|
|
function getVRCXPath() {
|
|
if (process.platform === 'win32') {
|
|
return path.join(process.env.APPDATA, 'VRCX');
|
|
} else if (process.platform === 'darwin') {
|
|
return path.join(process.env.HOME, 'Library/Application Support/VRCX');
|
|
}
|
|
// Linux or other
|
|
let configHome = process.env.XDG_CONFIG_HOME;
|
|
if (!configHome) {
|
|
configHome = path.join(process.env.HOME, '.config');
|
|
}
|
|
return path.join(configHome, 'VRCX');
|
|
}
|
|
|
|
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 (getCloseToTray()) {
|
|
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();
|
|
installVRCX();
|
|
|
|
app.on('activate', function () {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow();
|
|
} else {
|
|
// Ensure main window shows when clicking Dock icon (critical for macOS)
|
|
if (mainWindow && !mainWindow.isVisible()) {
|
|
mainWindow.show();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
function disposeOverlay() {
|
|
if (!isOverlayActive) {
|
|
return;
|
|
}
|
|
if (overlayWindow && !overlayWindow.isDestroyed()) {
|
|
overlayWindow.close();
|
|
}
|
|
overlayWindow = undefined;
|
|
isOverlayActive = false;
|
|
if (fs.existsSync(OVERLAY_SHM_PATH)) {
|
|
fs.unlinkSync(OVERLAY_SHM_PATH);
|
|
}
|
|
}
|
|
|
|
app.on('before-quit', function () {
|
|
// Mark it as a quitting state to make macOS Dock's "Quit" action take effect.
|
|
appIsQuitting = true;
|
|
disposeOverlay();
|
|
destroyTray();
|
|
});
|
|
|
|
app.on('window-all-closed', function () {
|
|
disposeOverlay();
|
|
|
|
if (process.platform !== 'darwin') {
|
|
app.quit();
|
|
}
|
|
});
|