1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-07-18 05:57:00 +00:00
fcast/receivers/electron/src/Main.ts

419 lines
15 KiB
TypeScript
Raw Normal View History

2023-06-20 08:45:01 +02:00
import { BrowserWindow, ipcMain, IpcMainEvent, nativeImage, Tray, Menu, dialog } from 'electron';
2024-12-09 00:56:55 -06:00
import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from 'common/Packets';
import { DiscoveryService } from 'common/DiscoveryService';
import { TcpListenerService } from 'common/TcpListenerService';
import { WebSocketListenerService } from 'common/WebSocketListenerService';
import { NetworkService } from 'common/NetworkService';
import { Opcode } from 'common/FCastSession';
2023-06-20 08:45:01 +02:00
import { Updater } from './Updater';
2024-01-04 12:38:39 +01:00
import * as os from 'os';
import * as path from 'path';
2024-11-11 12:24:17 -06:00
import * as log4js from "log4js";
2024-09-06 15:44:20 -05:00
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { ToastIcon } from 'common/components/Toast';
2024-12-09 00:56:55 -06:00
const cp = require('child_process');
2023-06-20 08:45:01 +02:00
2024-12-09 00:56:55 -06:00
export class Main {
2024-09-06 15:44:20 -05:00
static shouldOpenMainWindow = true;
static startFullscreen = false;
static playerWindow: Electron.BrowserWindow;
2023-06-20 08:45:01 +02:00
static mainWindow: Electron.BrowserWindow;
static application: Electron.App;
static tcpListenerService: TcpListenerService;
static webSocketListenerService: WebSocketListenerService;
2023-06-20 08:45:01 +02:00
static discoveryService: DiscoveryService;
static tray: Tray;
2024-11-11 12:24:17 -06:00
static logger: log4js.Logger;
2023-06-20 08:45:01 +02:00
private static toggleMainWindow() {
if (Main.mainWindow) {
Main.mainWindow.close();
}
else {
Main.openMainWindow();
}
}
2024-11-17 23:36:16 -06:00
private static async checkForUpdates(silent: boolean) {
if (Updater.updateDownloaded) {
Main.mainWindow.webContents.send("download-complete");
return;
}
try {
const updateAvailable = await Updater.checkForUpdates();
if (updateAvailable) {
Main.mainWindow.webContents.send("update-available");
}
else {
if (!silent) {
await dialog.showMessageBox({
type: 'info',
title: 'Already up-to-date',
message: 'The application is already on the latest version.',
buttons: ['OK'],
defaultId: 0
});
}
}
} catch (err) {
if (!silent) {
await dialog.showMessageBox({
type: 'error',
title: 'Failed to check for updates',
message: err,
buttons: ['OK'],
defaultId: 0
});
}
Main.logger.error('Failed to check for updates:', err);
}
}
2023-06-20 08:45:01 +02:00
private static createTray() {
2024-12-09 00:56:55 -06:00
const icon = (process.platform === 'win32') ? path.join(__dirname, 'assets/icons/app/icon.ico') : path.join(__dirname, 'assets/icons/app/icon.png');
2023-06-20 08:45:01 +02:00
const trayicon = nativeImage.createFromPath(icon)
const tray = new Tray(trayicon.resize({ width: 16 }));
const contextMenu = Menu.buildFromTemplate([
{
label: 'Toggle window',
click: () => { Main.toggleMainWindow(); }
},
2023-06-20 08:45:01 +02:00
{
label: 'Check for updates',
2024-11-17 23:36:16 -06:00
click: async () => { await Main.checkForUpdates(false); },
2023-06-20 08:45:01 +02:00
},
2024-11-19 09:54:50 -06:00
{
label: 'About',
click: async () => {
2024-11-21 11:51:46 -06:00
let aboutMessage = `Version: ${Main.application.getVersion()}\n`;
if (Updater.getCommit()) {
aboutMessage += `Commit: ${Updater.getCommit()}\n`;
}
aboutMessage += `Release channel: ${Updater.releaseChannel}\n`;
2024-11-19 09:54:50 -06:00
if (Updater.releaseChannel !== 'stable') {
aboutMessage += `Release channel version: ${Updater.getChannelVersion()}\n`;
}
aboutMessage += `OS: ${process.platform} ${process.arch}\n`;
await dialog.showMessageBox({
type: 'info',
title: 'Fcast Receiver',
message: aboutMessage,
buttons: ['OK'],
defaultId: 0
});
},
},
2023-06-20 08:45:01 +02:00
{
type: 'separator',
},
{
label: 'Restart',
click: () => {
this.application.relaunch();
2024-11-11 12:24:17 -06:00
this.application.exit();
2023-06-20 08:45:01 +02:00
}
},
{
label: 'Quit',
click: () => {
this.application.quit();
}
}
])
2024-09-06 15:44:20 -05:00
2023-06-20 08:45:01 +02:00
tray.setContextMenu(contextMenu);
// Left-click opens up tray menu, unlike in Windows/Linux
if (process.platform !== 'darwin') {
tray.on('click', () => { Main.toggleMainWindow(); } );
}
2023-06-20 08:45:01 +02:00
this.tray = tray;
}
private static onReady() {
Main.createTray();
Main.discoveryService = new DiscoveryService();
Main.discoveryService.start();
2024-09-06 15:44:20 -05:00
Main.tcpListenerService = new TcpListenerService();
Main.webSocketListenerService = new WebSocketListenerService();
2023-12-30 11:28:36 +01:00
const listeners = [Main.tcpListenerService, Main.webSocketListenerService];
listeners.forEach(l => {
2024-01-04 12:38:39 +01:00
l.emitter.on("play", async (message) => {
if (Main.playerWindow == null) {
Main.playerWindow = new BrowserWindow({
fullscreen: true,
autoHideMenuBar: true,
2024-11-04 09:17:20 -06:00
minWidth: 515,
minHeight: 290,
2024-10-21 20:51:07 +00:00
icon: path.join(__dirname, 'icon512.png'),
webPreferences: {
preload: path.join(__dirname, 'player/preload.js')
}
});
2024-09-06 15:44:20 -05:00
Main.playerWindow.setAlwaysOnTop(false, 'pop-up-menu');
Main.playerWindow.show();
2024-09-06 15:44:20 -05:00
Main.playerWindow.loadFile(path.join(__dirname, 'player/index.html'));
2024-01-04 12:38:39 +01:00
Main.playerWindow.on('ready-to-show', async () => {
2024-12-09 00:56:55 -06:00
Main.playerWindow?.webContents?.send("play", await NetworkService.proxyPlayIfRequired(message));
});
Main.playerWindow.on('closed', () => {
Main.playerWindow = null;
});
} else {
2024-12-09 00:56:55 -06:00
Main.playerWindow?.webContents?.send("play", await NetworkService.proxyPlayIfRequired(message));
2024-09-06 15:44:20 -05:00
}
});
2024-09-06 15:44:20 -05:00
l.emitter.on("pause", () => Main.playerWindow?.webContents?.send("pause"));
l.emitter.on("resume", () => Main.playerWindow?.webContents?.send("resume"));
2024-09-06 15:44:20 -05:00
l.emitter.on("stop", () => {
2024-11-21 00:40:15 -06:00
Main.playerWindow?.close();
Main.playerWindow = null;
});
2024-09-06 15:44:20 -05:00
l.emitter.on("seek", (message) => Main.playerWindow?.webContents?.send("seek", message));
l.emitter.on("setvolume", (message) => Main.playerWindow?.webContents?.send("setvolume", message));
l.emitter.on("setspeed", (message) => Main.playerWindow?.webContents?.send("setspeed", message));
l.emitter.on('connect', (message) => Main.mainWindow?.webContents?.send('connect', message));
l.emitter.on('disconnect', (message) => Main.mainWindow?.webContents?.send('disconnect', message));
l.emitter.on('ping', (message) => Main.mainWindow?.webContents?.send('ping', message));
l.start();
2023-06-20 08:45:01 +02:00
ipcMain.on('send-playback-error', (event: IpcMainEvent, value: PlaybackErrorMessage) => {
2023-12-30 10:55:30 +01:00
l.send(Opcode.PlaybackError, value);
});
ipcMain.on('send-playback-update', (event: IpcMainEvent, value: PlaybackUpdateMessage) => {
2023-12-30 10:55:30 +01:00
l.send(Opcode.PlaybackUpdate, value);
});
2024-09-06 15:44:20 -05:00
ipcMain.on('send-volume-update', (event: IpcMainEvent, value: VolumeUpdateMessage) => {
2023-12-30 10:55:30 +01:00
l.send(Opcode.VolumeUpdate, value);
});
2024-11-17 23:12:24 -06:00
ipcMain.on('send-download-request', async () => {
if (!Updater.isDownloading) {
try {
await Updater.downloadUpdate();
Main.mainWindow.webContents.send("download-complete");
} catch (err) {
await dialog.showMessageBox({
type: 'error',
title: 'Failed to download update',
message: err,
buttons: ['OK'],
defaultId: 0
});
Main.logger.error('Failed to download update:', err);
Main.mainWindow.webContents.send("download-failed");
}
}
});
ipcMain.on('send-restart-request', async () => {
Updater.restart();
});
2023-06-20 08:45:01 +02:00
});
2024-11-17 23:12:24 -06:00
ipcMain.handle('updater-progress', async () => { return Updater.updateProgress; });
2024-11-04 09:17:20 -06:00
ipcMain.handle('is-full-screen', async () => {
const window = Main.playerWindow;
if (!window) {
return;
}
return window.isFullScreen();
});
2023-06-20 08:45:01 +02:00
ipcMain.on('toggle-full-screen', () => {
const window = Main.playerWindow;
2023-06-20 08:45:01 +02:00
if (!window) {
return;
}
window.setFullScreen(!window.isFullScreen());
});
ipcMain.on('exit-full-screen', () => {
const window = Main.playerWindow;
2023-06-20 08:45:01 +02:00
if (!window) {
return;
}
window.setFullScreen(false);
});
if (Main.shouldOpenMainWindow) {
Main.openMainWindow();
}
2024-11-17 23:12:24 -06:00
if (Updater.updateError) {
dialog.showMessageBox({
type: 'error',
title: 'Error applying update',
message: 'Please try again later or visit https://fcast.org to update.',
buttons: ['OK'],
defaultId: 0
});
}
2024-11-17 23:36:16 -06:00
if (Updater.checkForUpdatesOnStart) {
Main.checkForUpdates(true);
}
2023-06-20 08:45:01 +02:00
}
static openMainWindow() {
if (Main.mainWindow) {
Main.mainWindow.focus();
return;
}
Main.mainWindow = new BrowserWindow({
fullscreen: Main.startFullscreen,
autoHideMenuBar: true,
2024-10-21 20:51:07 +00:00
icon: path.join(__dirname, 'icon512.png'),
minWidth: 1100,
minHeight: 800,
webPreferences: {
preload: path.join(__dirname, 'main/preload.js')
}
});
Main.mainWindow.loadFile(path.join(__dirname, 'main/index.html'));
Main.mainWindow.on('closed', () => {
Main.mainWindow = null;
});
Main.mainWindow.maximize();
Main.mainWindow.show();
Main.mainWindow.on('ready-to-show', () => {
2024-12-09 00:56:55 -06:00
Main.mainWindow.webContents.send("device-info", {name: os.hostname(), addresses: NetworkService.getAllIPv4Addresses()});
});
2024-09-06 15:44:20 -05:00
}
2024-11-11 12:24:17 -06:00
static async main(app: Electron.App) {
2024-11-15 00:43:01 -06:00
try {
Main.application = app;
2024-11-19 17:31:03 -06:00
const argv = yargs(hideBin(process.argv))
.version(app.getVersion())
.parserConfiguration({
'boolean-negation': false
})
.options({
'no-main-window': { type: 'boolean', default: false, desc: "Start minimized to tray" },
'fullscreen': { type: 'boolean', default: false, desc: "Start application in fullscreen" }
})
.parseSync();
2024-11-15 00:43:01 -06:00
const isUpdating = Updater.isUpdating();
const fileLogType = isUpdating ? 'fileSync' : 'file';
2024-11-15 00:43:01 -06:00
log4js.configure({
appenders: {
out: { type: 'stdout' },
log: { type: fileLogType, filename: path.join(app.getPath('logs'), 'fcast-receiver.log'), flags: 'a', maxLogSize: '5M' },
2024-11-15 00:43:01 -06:00
},
categories: {
default: { appenders: ['out', 'log'], level: 'info' },
},
});
Main.logger = log4js.getLogger();
2024-11-19 14:49:27 -06:00
Main.logger.info(`Starting application: ${app.name} | ${app.getAppPath()}`);
Main.logger.info(`Version: ${app.getVersion()}`);
2024-11-21 11:51:46 -06:00
Main.logger.info(`Commit: ${Updater.getCommit()}`);
2024-11-19 14:49:27 -06:00
Main.logger.info(`Release channel: ${Updater.releaseChannel} - ${Updater.getChannelVersion()}`);
2024-11-19 09:54:50 -06:00
Main.logger.info(`OS: ${process.platform} ${process.arch}`);
2024-11-11 12:24:17 -06:00
2024-11-15 00:43:01 -06:00
if (isUpdating) {
await Updater.processUpdate();
}
2024-11-11 12:24:17 -06:00
2024-11-15 00:43:01 -06:00
Main.startFullscreen = argv.fullscreen;
Main.shouldOpenMainWindow = !argv.noMainWindow;
Main.application.on('ready', Main.onReady);
Main.application.on('window-all-closed', () => { });
}
catch (err) {
Main.logger.error(`Error starting application: ${err}`);
app.exit();
2024-11-11 12:24:17 -06:00
}
2023-06-20 08:45:01 +02:00
}
2024-11-11 12:24:17 -06:00
}
2024-12-09 00:56:55 -06:00
export function getComputerName() {
switch (process.platform) {
case "win32":
return process.env.COMPUTERNAME;
case "darwin":
return cp.execSync("scutil --get ComputerName").toString().trim();
case "linux": {
let hostname: string;
// Some distro's don't work with `os.hostname()`, but work with `hostnamectl` and vice versa...
try {
hostname = os.hostname();
}
catch (err) {
Main.logger.warn('Error fetching hostname, trying different method...');
Main.logger.warn(err);
try {
hostname = cp.execSync("hostnamectl hostname").toString().trim();
}
catch (err2) {
Main.logger.warn('Error fetching hostname again, using generic name...');
Main.logger.warn(err2);
hostname = 'linux device';
}
}
return hostname;
}
default:
return os.hostname();
}
}
export async function errorHandler(err: NodeJS.ErrnoException) {
Main.logger.error("Application error:", err);
Main.mainWindow.webContents.send("toast", { message: err, icon: ToastIcon.ERROR });
2024-12-09 00:56:55 -06:00
const restartPrompt = await dialog.showMessageBox({
type: 'error',
title: 'Failed to start',
message: 'The application failed to start properly.',
buttons: ['Restart', 'Close'],
defaultId: 0,
cancelId: 1
});
if (restartPrompt.response === 0) {
Main.application.relaunch();
Main.application.exit(0);
} else {
Main.application.exit(0);
}
}