node build fixed

This commit is contained in:
ra_ma
2025-09-20 14:08:38 +01:00
parent c6ebbe069d
commit 3d298fa434
1516 changed files with 535727 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
node_modules/
dist/
binaries/
.DS_Store
web-denshi/

View File

@@ -0,0 +1,156 @@
<p align="center">
<img src="../seanime-web/public/logo_2.png" alt="preview" width="150px"/>
</p>
<h2 align="center"><b>Seanime Denshi</b></h2>
<p align="center">
Electron-based desktop client for Seanime. Embeds server and web interface. Successor to Seanime Desktop.
</p>
<p align="center">
<img src="../docs/images/4/anime-entry-torrent-stream--sq.jpg" alt="preview" width="70%"/>
</p>
---
## Prerequisites
- Go 1.24+
- Node.js 20+ and npm
---
## Seanime Denshi vs Seanime Desktop
Pros:
- Linux support
- Better consistency accross platforms (fewer bugs)
- Built-in player support for torrent/debrid streaming without transcoding
Cons:
- Greater memory usage
- Larger binary size (from ~80mb to ~300mb)
## TODO
- [ ] Built-in player
- Server: Stream subtitle extraction, thumbnail generation
- [ ] Testing on Windows (Fix titlebar in fullscreen)
- [ ] Fix crash screen
- [ ] Test server reconnection
- [ ] Test updates, auto updates
## Development
### Web Interface
```shell
# Working dir: ./seanime-web
npm run dev:denshi
```
### Sidecar
1. Build the server
```shell
# Working dir: .
# Windows
go build -o seanime.exe -trimpath -ldflags="-s -w" -tags=nosystray
# Linux, macOS
go build -o seanime -trimpath -ldflags="-s -w"
```
2. Move the binary to `./seanime-denshi/binaries`
3. Rename the binary:
- For Windows: `seanime-server-windows.exe`
- For macOS/Intel: `seanime-server-darwin-amd64`
- For macOS/ARM: `seanime-server-darwin-arm64`
- For Linux/x86_64: `seanime-server-linux-amd64`
- For Linux/ARM64: `seanime-server-linux-arm64`
### Electron
1. Setup
```shell
# Working dir: ./seanime-denshi
npm install
```
2. Run
`TEST_DATADIR` can be used in development mode, it should point to a dummy data directory for testing purposes.
```shell
# Working dir: ./seanime-desktop
TEST_DATADIR="/path/to/data/dir" npm run dev
```
---
## Build
### Web Interface
```shell
# Working dir: ./seanime-web
npm run build
npm run build:denshi
```
Move the output `./seanime-web/out` to `./web`
Move the output `./seanime-web/out-denshi` to `./seanime-denshi/web-denshi`
```shell
# UNIX command
mv ./seanime-web/out ./web
mv ./seanime-web/out-denshi ./seanime-denshi/web-denshi
```
### Sidecar
1. Build the server
```shell
# Working dir: .
# Windows
go build -o seanime.exe -trimpath -ldflags="-s -w" -tags=nosystray
# Linux, macOS
go build -o seanime -trimpath -ldflags="-s -w"
```
2. Move the binary to `./seanime-denshi/binaries`
3. Rename the binary:
- For Windows: `seanime-server-windows.exe`
- For macOS/Intel: `seanime-server-darwin-amd64`
- For macOS/ARM: `seanime-server-darwin-arm64`
- For Linux/x86_64: `seanime-server-linux-amd64`
- For Linux/ARM64: `seanime-server-linux-arm64`
### Electron
To build the desktop client for all platforms:
```
npm run build
```
To build for specific platforms:
```
npm run build:mac
npm run build:win
npm run build:linux
```
Output is in `./seanime-denshi/dist/...`

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,23 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.all</key>
<true/>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,117 @@
{
"name": "seanime-denshi",
"version": "2.10.0",
"description": "Electron-based Desktop client for Seanime",
"main": "src/main.js",
"scripts": {
"dev": "NODE_ENV=development electron .",
"build": "electron-builder build",
"build:mac": "electron-builder build --mac",
"build:win": "electron-builder build --win",
"build:linux": "electron-builder build --linux"
},
"dependencies": {
"electron-log": "^5.0.0",
"electron-serve": "^1.3.0",
"electron-updater": "^6.1.7",
"mime-types": "^2.1.35",
"strip-ansi": "^7.1.0"
},
"devDependencies": {
"cross-env": "^7.0.3",
"electron": "^36.1.2",
"electron-builder": "^24.13.3"
},
"build": {
"appId": "app.seanime.denshi",
"productName": "Seanime Denshi",
"asar": true,
"extraResources": [
{
"from": "binaries",
"to": "binaries"
}
],
"generateUpdatesFilesForAllChannels": true,
"publish": {
"provider": "generic",
"url": "https://github.com/5rahim/seanime/releases/latest/download",
"channel": "latest",
"publishAutoUpdate": true,
"useMultipleRangeRequest": false
},
"mac": {
"category": "public.app-category.entertainment",
"target": [
{
"target": "dmg",
"arch": [
"x64",
"arm64"
]
}
],
"darkModeSupport": true,
"notarize": false,
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "assets/entitlements.mac.plist",
"entitlementsInherit": "assets/entitlements.mac.plist",
"artifactName": "seanime-denshi-${version}_MacOS_${arch}.${ext}"
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"artifactName": "seanime-denshi-${version}_Windows_${arch}.${ext}"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64",
"arm64"
]
}
],
"category": "Entertainment",
"artifactName": "seanime-denshi-${version}_Linux_${arch}.${ext}"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "Seanime Denshi",
"artifactName": "seanime-denshi-${version}_Windows_${arch}.${ext}"
},
"directories": {
"buildResources": "assets",
"output": "dist"
},
"files": [
"src/**/*",
"web-denshi/**/*",
"assets/**/*",
"package.json",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
"!**/node_modules/*.d.ts",
"!**/node_modules/.bin",
"!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}",
"!.editorconfig",
"!**/._*",
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
"!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
"!**/{appveyor.yml,.travis.yml,circle.yml}",
"!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}"
]
},
"private": true
}

View File

@@ -0,0 +1,911 @@
const {app, BrowserWindow, Menu, Tray, ipcMain, shell, dialog, remote, net} = require('electron');
const path = require('path');
const serve = require('electron-serve');
const {spawn} = require('child_process');
const fs = require('fs');
let stripAnsi;
import('strip-ansi').then(module => {
stripAnsi = module.default;
});
const {autoUpdater} = require('electron-updater');
const log = require('electron-log');
function setupChromiumFlags() {
// Bypass CSP and security
app.commandLine.appendSwitch('bypasscsp-schemes');
app.commandLine.appendSwitch('no-sandbox');
app.commandLine.appendSwitch('no-zygote');
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
app.commandLine.appendSwitch('force_high_performance_gpu');
app.commandLine.appendSwitch('disk-cache-size', (400 * 1000 * 1000).toString());
app.commandLine.appendSwitch('force-effective-connection-type', '4g');
// Disable features that can interfere with playback
app.commandLine.appendSwitch('disable-features', ['Vulkan', 'WidgetLayering', 'ColorProviderRedirection', 'WebContentsForceDarkMode', // 'ForcedColors'
].join(','));
// Color management and rendering optimizations
// app.commandLine.appendSwitch('force-color-profile', 'srgb');
// app.commandLine.appendSwitch('disable-color-correct-rendering');
// app.commandLine.appendSwitch('disable-web-contents-color-extraction');
// app.commandLine.appendSwitch('disable-color-management');
// app.commandLine.appendSwitch('force-color-profile-interpretation', 'all-images');
// app.commandLine.appendSwitch('force-raster-color-profile', 'srgb');
// Hardware acceleration and GPU optimizations
app.commandLine.appendSwitch('force-high-performance-gpu');
// app.commandLine.appendSwitch('enable-gpu-rasterization');
app.commandLine.appendSwitch('enable-zero-copy');
app.commandLine.appendSwitch('enable-hardware-overlays', 'single-fullscreen,single-on-top,underlay');
app.commandLine.appendSwitch('ignore-gpu-blocklist');
// Video-specific optimizations
app.commandLine.appendSwitch('enable-accelerated-video-decode');
// Enable advanced features
app.commandLine.appendSwitch('enable-features', ['ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes', 'PlatformEncryptedDolbyVision', 'CanvasOopRasterization', 'UseSkiaRenderer', 'WebAssemblyLazyCompilation', 'RawDraw', // "Vulkan",
// 'MediaFoundationHEVC',
'PlatformHEVCDecoderSupport',].join(','));
app.commandLine.appendSwitch('enable-unsafe-webgpu');
app.commandLine.appendSwitch('enable-gpu-rasterization');
app.commandLine.appendSwitch('enable-oop-rasterization');
// Background processing optimizations
app.commandLine.appendSwitch('disable-background-timer-throttling');
app.commandLine.appendSwitch('disable-backgrounding-occluded-windows');
app.commandLine.appendSwitch('disable-renderer-backgrounding');
app.commandLine.appendSwitch('disable-background-media-suspend');
app.commandLine.appendSwitch('double-buffer-compositing');
app.commandLine.appendSwitch('disable-direct-composition-video-overlays');
}
const _development = process.env.NODE_ENV === 'development';
// const _development = false;
// Setup electron-serve for production
const appServe = !_development ? serve({
directory: path.join(__dirname, '../web-denshi')
}) : null;
// Custom protocol handler for routing in production
if (!_development) {
app.whenReady().then(() => {
const {protocol} = require('electron');
const mime = require('mime-types');
// Register a custom protocol to handle routing
protocol.handle('app', async (request) => {
const url = new URL(request.url);
let filePath = url.pathname;
// Remove leading slash
if (filePath.startsWith('/')) {
filePath = filePath.substring(1);
}
// Handle root path
if (filePath === '' || filePath === '-') {
filePath = 'index.html';
} else if (!filePath.endsWith('.html') && !filePath.includes('.') && !filePath.includes('/')) {
// If it's a route without extension, try to find corresponding HTML file
filePath = filePath + '.html';
}
// Handle subdirectories (like splashscreen/crash)
if (filePath.includes('/')) {
const parts = filePath.split('/');
if (parts.length > 1) {
// For nested routes like splashscreen/crash, try the nested structure first
const nestedPath = parts.join('/');
const nestedFullPath = path.join(__dirname, '../web-denshi', nestedPath);
if (fs.existsSync(nestedFullPath)) {
filePath = nestedPath;
} else {
// Try with .html extension
const nestedHtmlPath = nestedPath + '.html';
const nestedHtmlFullPath = path.join(__dirname, '../web-denshi', nestedHtmlPath);
if (fs.existsSync(nestedHtmlFullPath)) {
filePath = nestedHtmlPath;
} else {
// Fall back to the last part with .html
filePath = parts[parts.length - 1] + '.html';
}
}
}
}
const fullPath = path.join(__dirname, '../web-denshi', filePath);
// Check if file exists, otherwise fall back to index.html
if (!fs.existsSync(fullPath)) {
filePath = 'index.html';
}
const finalPath = path.join(__dirname, '../web-denshi', filePath);
try {
const fileContent = fs.readFileSync(finalPath);
const mimeType = mime.lookup(finalPath) || 'application/octet-stream';
return new Response(fileContent, {
headers: {
'Content-Type': mimeType,
'Cache-Control': 'no-cache'
}
});
} catch (error) {
console.error('[Protocol] Error reading file:', finalPath, error);
return new Response('File not found', {
status: 404,
headers: {'Content-Type': 'text/plain'}
});
}
});
});
}
// Setup update events for logging
autoUpdater.logger = log;
log.transports.file.level = 'debug';
// Redirect console logging to electron-log
console.log = log.info;
console.error = log.error;
function logStartupEvent(stage, detail = '') {
const message = `[STARTUP] ${stage}: ${detail}`;
log.info(message);
// console.info(message);
}
// Global error handlers to catch unhandled exceptions
process.on('uncaughtException', (error) => {
log.error('Uncaught Exception:', error);
if (app.isReady()) {
dialog.showErrorBox('An error occurred', `Uncaught Exception: ${error.message}\n\nCheck the logs for more details.`);
}
logStartupEvent('UNCAUGHT EXCEPTION', error.stack || error.message);
});
process.on('unhandledRejection', (reason, promise) => {
log.error('Unhandled Rejection at:', promise, 'reason:', reason);
logStartupEvent('UNHANDLED REJECTION', reason?.stack || reason?.message || JSON.stringify(reason));
});
// Dumps important environment information for debugging
function logEnvironmentInfo() {
logStartupEvent('NODE_ENV', process.env.NODE_ENV || 'not set');
logStartupEvent('Platform', process.platform);
logStartupEvent('Architecture', process.arch);
logStartupEvent('Node version', process.version);
logStartupEvent('Electron version', process.versions.electron);
logStartupEvent('App path', app.getAppPath());
logStartupEvent('Dir name', __dirname);
logStartupEvent('User data path', app.getPath('userData'));
logStartupEvent('Executable path', app.getPath('exe'));
if (process.resourcesPath) {
logStartupEvent('Resources path', process.resourcesPath);
try {
// const resourceFiles = fs.readdirSync(process.resourcesPath);
// logStartupEvent('Resources directory contents', JSON.stringify(resourceFiles));
// Check if binaries directory exists
const binariesDir = path.join(process.resourcesPath, 'binaries');
if (fs.existsSync(binariesDir)) {
const binariesFiles = fs.readdirSync(binariesDir);
logStartupEvent('Binaries directory contents', JSON.stringify(binariesFiles));
} else {
logStartupEvent('ERROR', 'Binaries directory not found');
}
} catch (err) {
logStartupEvent('ERROR reading resources', err.message);
}
}
// Check app directory structure
try {
const appPath = app.getAppPath();
// logStartupEvent('App directory contents', JSON.stringify(fs.readdirSync(appPath)));
const webPath = path.join(appPath, 'web-denshi');
if (fs.existsSync(webPath)) {
// logStartupEvent('Web directory contents', JSON.stringify(fs.readdirSync(webPath)));
} else {
logStartupEvent('ERROR', 'web-denshi directory not found in app path');
}
} catch (err) {
logStartupEvent('ERROR reading app directory', err.message);
}
}
const updateConfig = {
provider: 'generic',
url: 'https://github.com/5rahim/seanime/releases/latest/download',
channel: 'latest',
allowPrerelease: false,
verifyUpdateCodeSignature: false,
};
// Override with environment variable if set
if (process.env.UPDATES_URL) {
updateConfig.url = process.env.UPDATES_URL;
}
// Configure the updater
autoUpdater.setFeedURL(updateConfig);
// Enable automatic download
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true;
// App state
let mainWindow = null;
let splashScreen = null;
let crashScreen = null;
let tray = null;
let serverProcess = null;
let isShutdown = false;
let serverStarted = false;
let updateDownloaded = false;
// Setup autoUpdater events with improved error handling
autoUpdater.on('checking-for-update', () => {
autoUpdater.logger.info('Checking for update...');
});
autoUpdater.on('update-available', (info) => {
autoUpdater.logger.info('Update available:', info);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-available', {
version: info.version, releaseDate: info.releaseDate, files: info.files
});
}
});
autoUpdater.on('update-not-available', (info) => {
autoUpdater.logger.info('Update not available:', info);
});
autoUpdater.on('download-progress', (progressObj) => {
autoUpdater.logger.info(`Download progress: ${progressObj.percent}%`);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('download-progress', {
percent: progressObj.percent, bytesPerSecond: progressObj.bytesPerSecond, transferred: progressObj.transferred, total: progressObj.total
});
}
});
autoUpdater.on('update-downloaded', (info) => {
autoUpdater.logger.info('Update downloaded:', info);
updateDownloaded = true;
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-downloaded', {
version: info.version, releaseDate: info.releaseDate, files: info.files
});
}
});
autoUpdater.on('error', (err) => {
autoUpdater.logger.error('Error in auto-updater:', err);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-error', {
code: err.code || 'unknown', message: err.message, stack: err.stack
});
}
});
/**
* Create the tray icon and menu
*/
function createTray() {
let iconPath = path.join(__dirname, '../assets/icon.png');
if (process.platform === 'darwin') {
iconPath = path.join(__dirname, '../assets/18x18.png');
}
tray = new Tray(iconPath);
const contextMenu = Menu.buildFromTemplate([{
id: 'toggle_visibility', label: 'Toggle visibility', click: () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
if (process.platform === 'darwin') {
app.dock.hide();
}
} else {
mainWindow.show();
mainWindow.focus();
if (process.platform === 'darwin') {
app.dock.show();
}
}
}
}, ...(process.platform === 'darwin' ? [{
id: 'accessory_mode', label: 'Remove from dock', click: () => {
app.dock.hide();
}
}] : []), {
id: 'quit', label: 'Quit Seanime', click: () => {
cleanupAndExit();
}
}]);
tray.setToolTip('Seanime');
tray.setContextMenu(contextMenu);
tray.on('click', () => {
mainWindow.show();
mainWindow.focus();
if (process.platform === 'darwin') {
app.dock.show();
}
});
}
/**
* Launch the Seanime server
*/
async function launchSeanimeServer(isRestart) {
return new Promise((resolve, reject) => {
// TEST ONLY: Check for -no-binary flag
if (process.argv.includes('-no-binary')) {
logStartupEvent('SKIPPING SERVER LAUNCH', 'Detected -no-binary flag');
console.log('[Main] Skipping server launch due to -no-binary flag');
serverStarted = true; // Assume server is "started" for UI flow
// Resolve immediately to bypass server spawning
if (splashScreen && !splashScreen.isDestroyed()) {
splashScreen.close();
splashScreen = null;
}
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.maximize();
mainWindow.show();
}
return resolve();
}
// Determine the correct binary to use based on platform and architecture
let binaryName = '';
if (process.platform === 'win32') {
binaryName = 'seanime-server-windows.exe';
} else if (process.platform === 'darwin') {
const arch = process.arch === 'arm64' ? 'arm64' : 'amd64';
binaryName = `seanime-server-darwin-${arch}`;
} else if (process.platform === 'linux') {
const arch = process.arch === 'arm64' ? 'arm64' : 'amd64';
binaryName = `seanime-server-linux-${arch}`;
}
let binaryPath;
if (_development) {
// In development, look for binaries in the project directory
binaryPath = path.join(__dirname, '../binaries', binaryName);
} else {
// In production, use the resources path
binaryPath = path.join(process.resourcesPath, 'binaries', binaryName);
}
logStartupEvent('Using binary', `${binaryPath} (${process.arch})`);
logStartupEvent('Resources path', process.resourcesPath);
// Check if binary exists and is executable
if (!fs.existsSync(binaryPath)) {
const error = new Error(`Server binary not found at ${binaryPath}`);
logStartupEvent('ERROR', error.message);
return reject(error);
}
// Make binary executable (for macOS/Linux)
if (process.platform !== 'win32') {
try {
fs.chmodSync(binaryPath, '755');
} catch (error) {
console.error(`Failed to make binary executable: ${error}`);
}
}
// Arguments
const args = [];
// Development mode
if (_development && process.env.TEST_DATADIR) {
console.log('[Main] TEST_DATADIR', process.env.TEST_DATADIR);
args.push('-datadir', process.env.TEST_DATADIR);
}
args.push('-desktop-sidecar', 'true');
console.log('\x1b[32m[Main] Spawning server process\x1b[0m', {args, binaryPath});
// Spawn the process
try {
serverProcess = spawn(binaryPath, args);
} catch (spawnError) {
console.error('[Main] Failed to spawn server process synchronously:', spawnError);
return reject(spawnError);
}
serverProcess.stdout.on('data', (data) => {
const dataStr = data.toString();
const lineStr = stripAnsi ? stripAnsi(dataStr) : dataStr;
// // Check if mainWindow exists and is not destroyed
// if (mainWindow && !mainWindow.isDestroyed()) {
// mainWindow.webContents.send('message', lineStr);
// }
// Check if the frontend is connected
if (!serverStarted && lineStr.includes('Client connected')) {
console.log('[Main] Server started');
serverStarted = true;
setTimeout(() => {
console.log('[Main] Server started timeout');
if (splashScreen && !splashScreen.isDestroyed()) {
splashScreen.close();
splashScreen = null;
}
console.log('[Main] Server started close splash screen');
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.maximize();
mainWindow.show();
}
resolve();
}, 1000);
}
});
serverProcess.stderr.on('data', (data) => {
console.error(data.toString());
});
serverProcess.on('close', (code) => {
console.log(`[Main] Server process exited with code ${code}`);
// If the server didn't start properly and we're not in the process of shutting down
if (!serverStarted && !isShutdown) {
console.log('[Main] Server process exited before starting');
reject(new Error(`Server process exited prematurely with code ${code} before starting.`));
// close splash screen and main window
if (splashScreen && !splashScreen.isDestroyed()) {
splashScreen.close();
splashScreen = null;
}
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
// show crash screen
if (crashScreen && !crashScreen.isDestroyed()) {
crashScreen.show();
crashScreen.webContents.send('crash', `Seanime server process terminated with status: ${code}. Closing in 10 seconds.`);
setTimeout(() => {
app.exit(1);
}, 10000);
}
}
});
// Handle spawn errors
serverProcess.on('error', (err) => {
console.error('[Main] Server process spawn error event:', err);
reject(err);
});
});
}
/**
* Create main application window
*/
function createMainWindow() {
logStartupEvent('Creating main window');
const windowOptions = {
width: 800, height: 600, show: false, webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
webSecurity: false,
allowRunningInsecureContent: true,
enableBlinkFeatures: 'FontAccess, AudioVideoTracks',
backgroundThrottling: false
}
};
// contextMenu({
// showInspectElement: true
// });
// Set title bar style based on platform
if (process.platform === 'darwin') {
windowOptions.titleBarStyle = 'hiddenInset';
}
if (process.platform === 'win32') {
windowOptions.titleBarStyle = 'hidden';
}
mainWindow = new BrowserWindow(windowOptions);
// Hide the title bar on Windows
if (process.platform === 'win32' || process.platform === 'linux') {
mainWindow.setMenuBarVisibility(false);
}
mainWindow.on('render-process-gone', (event, details) => {
console.log('[Main] Render process gone', details);
if (crashScreen && !crashScreen.isDestroyed()) {
crashScreen.show();
}
});
mainWindow.webContents.setWindowOpenHandler(({url}) => {
// Open external links in the default browser
if (url.startsWith('http://') || url.startsWith('https://')) {
shell.openExternal(url);
return {action: 'deny'};
}
// Allow other URLs to open in the app
return {action: 'allow'};
})
// Load the web content
if (_development) {
// In development, load from the dev server
logStartupEvent('Loading from dev server', 'http://127.0.0.1:43210');
mainWindow.loadURL('http://127.0.0.1:43210');
// mainWindow.loadURL('chrome://gpu');
} else {
// Load from custom protocol handler in production
logStartupEvent('Loading production build with custom protocol');
mainWindow.loadURL('app://-');
}
// Development tools
if (_development) {
mainWindow.webContents.openDevTools();
}
mainWindow.on('close', (event) => {
if (!isShutdown) {
event.preventDefault();
mainWindow.hide();
if (process.platform === 'darwin') {
app.dock.hide();
}
}
});
}
/**
* Create splash screen window
*/
function createSplashScreen() {
logStartupEvent('Creating splash screen');
splashScreen = new BrowserWindow({
width: 800, height: 600, frame: false, resizable: false, webPreferences: {
nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js')
}
});
// Load the web content
if (_development) {
// In development, load from the dev server
logStartupEvent('Loading splash from dev server', 'http://127.0.0.1:43210/splashscreen');
splashScreen.loadURL('http://127.0.0.1:43210/splashscreen');
} else {
// Load from custom protocol handler
logStartupEvent('Loading splash screen with custom protocol');
splashScreen.loadURL('app://splashscreen');
}
}
/**
* Create crash screen window
*/
function createCrashScreen() {
crashScreen = new BrowserWindow({
width: 800, height: 600, frame: false, resizable: false, show: false, webPreferences: {
nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js')
}
});
// Load the web content
if (_development) {
// In development, load from the dev server
crashScreen.loadURL('http://127.0.0.1:43210/splashscreen/crash');
} else {
// Load from custom protocol handler
crashScreen.loadURL('app://splashscreen/crash');
}
}
/**
* Cleanup and exit the application gracefully
*/
function cleanupAndExit() {
console.log('[Main] Cleaning up and exiting');
isShutdown = true;
// Kill server process first
if (serverProcess) {
console.log('[Main] Killing server process');
try {
serverProcess.kill();
serverProcess = null;
} catch (err) {
console.error('[Main] Error killing server process:', err);
}
}
// Exit the app after a short delay to allow cleanup
setTimeout(() => {
app.exit(0);
}, 500);
}
// Initialize the app
app.whenReady().then(async () => {
logStartupEvent('App ready');
// Set up Chromium flags for better video playback
setupChromiumFlags();
// Log environment information
logEnvironmentInfo();
// Setup IPC handlers for update functions
ipcMain.handle('check-for-updates', async () => {
try {
console.log('[Main] Checking for updates...');
const result = await autoUpdater.checkForUpdates();
return {
updateAvailable: !!result?.updateInfo, updateInfo: result?.updateInfo
};
} catch (error) {
console.error('[Main] Error checking for updates:', error);
throw error; // Let the renderer handle the error
}
});
ipcMain.handle('install-update', async () => {
try {
if (!updateDownloaded) {
throw new Error('Update not downloaded yet');
}
console.log('[Main] Installing update...');
autoUpdater.quitAndInstall(false, true);
return true;
} catch (error) {
console.error('[Main] Error installing update:', error);
throw error;
}
});
ipcMain.handle('kill-server', async () => {
if (serverProcess) {
console.log('[Main] Killing server before update...');
serverProcess.kill();
return true;
}
return false;
});
// Linux fix for compositing mode
if (process.platform === 'linux') {
process.env.WEBKIT_DISABLE_COMPOSITING_MODE = '1';
}
// Create windows
createMainWindow();
createSplashScreen();
createCrashScreen();
// Create tray
createTray();
// Launch server
try {
logStartupEvent('Attempting to launch server');
await launchSeanimeServer(false);
logStartupEvent('Server launched successfully');
// Check for updates only after server launch and main window setup is successful
autoUpdater.checkForUpdatesAndNotify();
} catch (error) {
logStartupEvent('Server launch failed', error.message);
console.error('[Main] Failed to start server:', error);
if (splashScreen && !splashScreen.isDestroyed()) {
splashScreen.close();
splashScreen = null;
}
if (crashScreen && !crashScreen.isDestroyed()) {
crashScreen.show();
crashScreen.webContents.send('crash', `The server failed to start: ${error}. Closing in 10 seconds.`);
setTimeout(() => {
console.error('[Main] Exiting due to server start failure.');
app.exit(1);
}, 10000);
}
}
// Register Window Control IPC handlers
ipcMain.on('window:minimize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.minimize();
}
});
ipcMain.on('window:maximize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.maximize();
}
});
ipcMain.on('window:close', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
});
ipcMain.on('window:toggleMaximize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
}
});
ipcMain.on('window:setFullscreen', (_, fullscreen) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.setFullScreen(fullscreen);
}
});
ipcMain.on('window:hide', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.hide();
}
});
ipcMain.on('window:show', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.show();
}
});
ipcMain.handle('window:getCurrentWindow', () => {
const win = BrowserWindow.fromWebContents(mainWindow.webContents);
return win?.id;
});
// Window state query handlers
ipcMain.handle('window:isMaximized', () => {
return mainWindow && !mainWindow.isDestroyed() ? mainWindow.isMaximized() : false;
});
ipcMain.handle('window:isMinimizable', () => {
return mainWindow && !mainWindow.isDestroyed() ? mainWindow.minimizable : false;
});
ipcMain.handle('window:isMaximizable', () => {
return mainWindow && !mainWindow.isDestroyed() ? mainWindow.maximizable : false;
});
ipcMain.handle('window:isClosable', () => {
return mainWindow && !mainWindow.isDestroyed() ? mainWindow.closable : false;
});
ipcMain.handle('window:isFullscreen', () => {
return mainWindow && !mainWindow.isDestroyed() ? mainWindow.isFullScreen() : false;
});
ipcMain.handle('window:isVisible', () => {
return mainWindow && !mainWindow.isDestroyed() ? mainWindow.isVisible() : false;
});
// Clipboard handler
ipcMain.handle('clipboard:writeText', (_, text) => {
if (text) {
return require('electron').clipboard.writeText(text);
}
return false;
});
// Register server IPC handlers
ipcMain.on('restart-server', () => {
console.log('EVENT restart-server');
if (serverProcess) {
console.log('Killing existing server process');
serverProcess.kill();
}
// devnote: don't set this to false or it will trigger the crashscreen
// serverStarted = false;
launchSeanimeServer(true).catch(console.error);
});
ipcMain.on('kill-server', () => {
console.log('EVENT kill-server');
if (serverProcess) {
console.log('Killing server process');
serverProcess.kill();
}
});
// Watch for window events to notify renderer
if (mainWindow) {
mainWindow.on('maximize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('window:maximized');
}
});
mainWindow.on('unmaximize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('window:unmaximized');
}
});
mainWindow.on('enter-full-screen', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('window:fullscreen', true);
}
});
mainWindow.on('leave-full-screen', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('window:fullscreen', false);
}
});
}
// macOS specific events
ipcMain.on('macos-activation-policy-accessory', () => {
console.log('EVENT macos-activation-policy-accessory');
if (process.platform === 'darwin') {
app.dock.hide();
mainWindow.show();
mainWindow.setFullScreen(true);
setTimeout(() => {
mainWindow.focus();
mainWindow.webContents.send('macos-activation-policy-accessory-done', '');
}, 150);
}
});
ipcMain.on('macos-activation-policy-regular', () => {
console.log('EVENT macos-activation-policy-regular');
if (process.platform === 'darwin') {
app.dock.show();
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
}
});
app.on('before-quit', () => {
console.log('EVENT before-quit');
cleanupAndExit();
});
});

View File

@@ -0,0 +1,101 @@
const {contextBridge, ipcRenderer, ipcMain, shell, BrowserWindow} = require('electron');
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
'electron', {
// Window Controls
window: {
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
isMaximized: () => ipcRenderer.invoke('window:isMaximized'),
isMinimizable: () => ipcRenderer.invoke('window:isMinimizable'),
isMaximizable: () => ipcRenderer.invoke('window:isMaximizable'),
isClosable: () => ipcRenderer.invoke('window:isClosable'),
isFullscreen: () => ipcRenderer.invoke('window:isFullscreen'),
setFullscreen: (fullscreen) => ipcRenderer.send('window:setFullscreen', fullscreen),
toggleMaximize: () => ipcRenderer.send('window:toggleMaximize'),
hide: () => ipcRenderer.send('window:hide'),
show: () => ipcRenderer.send('window:show'),
isVisible: () => ipcRenderer.invoke('window:isVisible'),
setTitleBarStyle: (style) => ipcRenderer.send('window:setTitleBarStyle', style),
getCurrentWindow: () => ipcRenderer.invoke('window:getCurrentWindow')
},
// Event listeners
on: (channel, callback) => {
// Whitelist channels
const validChannels = [
'message',
'crash',
'window:maximized',
'window:unmaximized',
'window:fullscreen',
'update-downloaded',
'update-error',
'update-available',
'download-progress',
'window:currentWindow'
];
if (validChannels.includes(channel)) {
// Remove the event listener to avoid memory leaks
ipcRenderer.removeAllListeners(channel);
// Add the event listener
ipcRenderer.on(channel, (_, ...args) => callback(...args));
// Return a function to remove the listener
return () => {
ipcRenderer.removeAllListeners(channel);
};
}
},
// Send events
emit: (channel, data) => {
// Whitelist channels
const validChannels = [
'restart-server',
'kill-server',
'macos-activation-policy-accessory',
'macos-activation-policy-regular'
];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
// General send method for any whitelisted channel
send: (channel, ...args) => {
// Whitelist channels
const validChannels = [
'restart-app',
'quit-app'
];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, ...args);
}
},
// Platform
platform: process.platform,
// Shell functions
shell: {
open: (url) => shell.openExternal(url)
},
// Clipboard
clipboard: {
writeText: (text) => ipcRenderer.invoke('clipboard:writeText', text)
},
// Update functions
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
installUpdate: () => ipcRenderer.invoke('install-update'),
killServer: () => ipcRenderer.invoke('kill-server')
}
);
// Set __isElectronDesktop__ global variable
contextBridge.exposeInMainWorld('__isElectronDesktop__', true);