mirror of
https://github.com/SrIzan10/helium.git
synced 2026-06-06 00:56:58 +00:00
feat: (mostly ai gen) electron app for native computer platforms
This commit is contained in:
25
electron/entitlements.mac.plist
Normal file
25
electron/entitlements.mac.plist
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Required for screen recording -->
|
||||
<key>com.apple.security.device.screen-capture</key>
|
||||
<true/>
|
||||
|
||||
<!-- Required for audio capture -->
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
|
||||
<!-- Allow JIT compilation (for V8) -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
|
||||
<!-- Allow unsigned executable memory (required by Electron) -->
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
|
||||
<!-- Disable library validation (required by Electron) -->
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
254
electron/main/index.ts
Normal file
254
electron/main/index.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
desktopCapturer,
|
||||
session,
|
||||
systemPreferences,
|
||||
type IpcMainInvokeEvent,
|
||||
type DesktopCapturerSource,
|
||||
} from 'electron';
|
||||
import path from 'path';
|
||||
import { VenmicManager, type VenmicLinkOptions } from './venmic';
|
||||
|
||||
const isLinux = process.platform === 'linux';
|
||||
const isMac = process.platform === 'darwin';
|
||||
const isWindows = process.platform === 'win32';
|
||||
const isWayland = isLinux && (
|
||||
process.env.XDG_SESSION_TYPE === 'wayland' ||
|
||||
process.env.WAYLAND_DISPLAY !== undefined
|
||||
);
|
||||
|
||||
const enableFeatures: Set<string> = new Set();
|
||||
const disableFeatures: Set<string> = new Set();
|
||||
|
||||
if (isLinux) {
|
||||
enableFeatures.add('PulseaudioLoopbackForScreenShare');
|
||||
disableFeatures.add('WebRtcAllowInputVolumeAdjustment');
|
||||
|
||||
if (isWayland) {
|
||||
enableFeatures.add('WebRTCPipeWireCapturer');
|
||||
disableFeatures.add('UseMultiPlaneFormatForSoftwareVideo');
|
||||
console.log('[Helium] Wayland detected, enabling PipeWire capturer');
|
||||
}
|
||||
}
|
||||
|
||||
if (enableFeatures.size > 0) {
|
||||
app.commandLine.appendSwitch('enable-features', Array.from(enableFeatures).join(','));
|
||||
}
|
||||
if (disableFeatures.size > 0) {
|
||||
app.commandLine.appendSwitch('disable-features', Array.from(disableFeatures).join(','));
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development' || process.argv.includes('--dev');
|
||||
const NUXT_DEV_URL = process.env.NUXT_DEV_URL || 'http://localhost:3000';
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let venmicManager: VenmicManager | null = null;
|
||||
|
||||
console.log('[Helium] Platform:', process.platform);
|
||||
console.log('[Helium] Wayland:', isWayland);
|
||||
console.log('[Helium] XDG_SESSION_TYPE:', process.env.XDG_SESSION_TYPE);
|
||||
|
||||
if (isLinux) {
|
||||
try {
|
||||
venmicManager = new VenmicManager();
|
||||
} catch (error) {
|
||||
console.warn('[Helium] Venmic not available:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function createWindow(): void {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
title: 'Helium',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
},
|
||||
titleBarStyle: isMac ? 'hiddenInset' : 'default',
|
||||
backgroundColor: '#1a1a2e',
|
||||
});
|
||||
|
||||
setupDisplayMediaHandler();
|
||||
|
||||
if (isDev) {
|
||||
mainWindow.loadURL(NUXT_DEV_URL);
|
||||
mainWindow.webContents.openDevTools();
|
||||
} else {
|
||||
const prodUrl = process.env.HELIUM_URL || 'http://localhost:3000';
|
||||
mainWindow.loadURL(prodUrl);
|
||||
}
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
mainWindow.on('page-title-updated', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
function setupDisplayMediaHandler(): void {
|
||||
session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => {
|
||||
try {
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['screen', 'window'],
|
||||
thumbnailSize: { width: 320, height: 180 },
|
||||
fetchWindowIcons: !isWayland,
|
||||
});
|
||||
|
||||
if (sources.length === 0) {
|
||||
console.error('[Helium] No desktop sources available');
|
||||
callback({});
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedSource: DesktopCapturerSource;
|
||||
|
||||
if (isWayland) {
|
||||
selectedSource = sources[0];
|
||||
console.log('[Helium] Wayland PipeWire source:', selectedSource.name);
|
||||
} else {
|
||||
const screenSources = sources.filter(s => s.id.startsWith('screen:'));
|
||||
selectedSource = screenSources[0] || sources[0];
|
||||
console.log('[Helium] Auto-selected source:', selectedSource.name);
|
||||
}
|
||||
|
||||
const callbackOptions: {
|
||||
video: DesktopCapturerSource;
|
||||
audio?: 'loopback' | 'loopbackWithMute';
|
||||
} = {
|
||||
video: selectedSource,
|
||||
};
|
||||
|
||||
if (isWindows || isMac || isLinux) {
|
||||
callbackOptions.audio = 'loopback';
|
||||
console.log('[Helium] Enabling loopback audio capture');
|
||||
}
|
||||
|
||||
callback(callbackOptions);
|
||||
} catch (error) {
|
||||
console.error('[Helium] Display media handler error:', error);
|
||||
callback({});
|
||||
}
|
||||
}, { useSystemPicker: false });
|
||||
}
|
||||
|
||||
ipcMain.handle('helium:get-platform', () => {
|
||||
return {
|
||||
platform: process.platform,
|
||||
isLinux,
|
||||
isMac,
|
||||
isWindows,
|
||||
isWayland,
|
||||
isElectron: true,
|
||||
supportsLoopbackAudio: isWindows || isMac,
|
||||
supportsVenmic: isLinux && (venmicManager?.isAvailable() ?? false),
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.handle('helium:get-sources', async () => {
|
||||
if (isWayland) {
|
||||
console.log('[Helium] Skipping getSources on Wayland');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['screen', 'window'],
|
||||
thumbnailSize: { width: 320, height: 180 },
|
||||
fetchWindowIcons: true,
|
||||
});
|
||||
|
||||
return sources.map((source) => ({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
thumbnail: source.thumbnail.toDataURL(),
|
||||
appIcon: source.appIcon?.toDataURL() || null,
|
||||
display_id: source.display_id,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[Helium] Failed to get sources:', error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('helium:venmic-available', () => {
|
||||
return venmicManager?.isAvailable() ?? false;
|
||||
});
|
||||
|
||||
ipcMain.handle('helium:venmic-list', async () => {
|
||||
if (!venmicManager) return [];
|
||||
return venmicManager.listSources();
|
||||
});
|
||||
|
||||
ipcMain.handle('helium:venmic-link', async (_event: IpcMainInvokeEvent, options: VenmicLinkOptions) => {
|
||||
if (!venmicManager) return false;
|
||||
return venmicManager.link(options);
|
||||
});
|
||||
|
||||
ipcMain.handle('helium:venmic-unlink', async () => {
|
||||
if (!venmicManager) return false;
|
||||
return venmicManager.unlink();
|
||||
});
|
||||
|
||||
ipcMain.handle('helium:check-screen-permission', () => {
|
||||
if (isMac) {
|
||||
return systemPreferences.getMediaAccessStatus('screen');
|
||||
}
|
||||
return 'granted';
|
||||
});
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on('second-instance', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (venmicManager) {
|
||||
venmicManager.unlink();
|
||||
}
|
||||
|
||||
if (!isMac) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (venmicManager) {
|
||||
venmicManager.unlink();
|
||||
}
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
app.on('certificate-error', (event, _webContents, _url, _error, _certificate, callback) => {
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
});
|
||||
}
|
||||
137
electron/main/venmic.ts
Normal file
137
electron/main/venmic.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
interface VenmicModule {
|
||||
PatchBay: new () => PatchBay;
|
||||
}
|
||||
|
||||
interface PatchBay {
|
||||
list(props: string[]): Record<string, string>[];
|
||||
link(options: VenmicLinkOptions): boolean;
|
||||
unlink(): void;
|
||||
}
|
||||
|
||||
export interface VenmicLinkOptions {
|
||||
include?: Record<string, string>[];
|
||||
exclude?: Record<string, string>[];
|
||||
ignore_devices?: boolean;
|
||||
only_speakers?: boolean;
|
||||
only_default_speakers?: boolean;
|
||||
workaround?: Record<string, string>[];
|
||||
}
|
||||
|
||||
export class VenmicManager {
|
||||
private venmic: VenmicModule | null = null;
|
||||
private patchBay: PatchBay | null = null;
|
||||
private available: boolean = false;
|
||||
private linked: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
this.venmic = require('@vencord/venmic') as VenmicModule;
|
||||
this.available = true;
|
||||
console.log('[Venmic] Module loaded successfully');
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.warn('[Venmic] Not available:', err.message);
|
||||
console.warn('[Venmic] Install with: pnpm add @vencord/venmic');
|
||||
this.available = false;
|
||||
}
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return this.available;
|
||||
}
|
||||
|
||||
listSources(props: string[] = ['node.name', 'application.name', 'application.process.binary']): Record<string, string>[] {
|
||||
if (!this.available || !this.venmic) return [];
|
||||
|
||||
try {
|
||||
if (!this.patchBay) {
|
||||
this.patchBay = new this.venmic.PatchBay();
|
||||
}
|
||||
return this.patchBay.list(props);
|
||||
} catch (error) {
|
||||
console.error('[Venmic] Error listing sources:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
link(options: VenmicLinkOptions = {}): boolean {
|
||||
if (!this.available || !this.venmic) return false;
|
||||
|
||||
try {
|
||||
if (!this.patchBay) {
|
||||
this.patchBay = new this.venmic.PatchBay();
|
||||
}
|
||||
|
||||
if (this.linked) {
|
||||
this.unlink();
|
||||
}
|
||||
|
||||
const linkOptions: VenmicLinkOptions = {
|
||||
ignore_devices: options.ignore_devices ?? true,
|
||||
only_speakers: options.only_speakers ?? false,
|
||||
only_default_speakers: options.only_default_speakers ?? false,
|
||||
include: options.include || [],
|
||||
exclude: options.exclude || [],
|
||||
workaround: options.workaround || [],
|
||||
};
|
||||
|
||||
console.log('[Venmic] Linking:', JSON.stringify(linkOptions));
|
||||
|
||||
const success = this.patchBay.link(linkOptions);
|
||||
this.linked = success;
|
||||
|
||||
if (success) {
|
||||
console.log('[Venmic] Audio linked successfully');
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error('[Venmic] Link error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
linkAll(): boolean {
|
||||
return this.link({
|
||||
exclude: [],
|
||||
ignore_devices: true,
|
||||
only_speakers: true,
|
||||
only_default_speakers: false,
|
||||
});
|
||||
}
|
||||
|
||||
linkApp(appName: string): boolean {
|
||||
return this.link({
|
||||
include: [{ 'application.name': appName }],
|
||||
});
|
||||
}
|
||||
|
||||
unlink(): boolean {
|
||||
if (!this.available || !this.patchBay) return false;
|
||||
|
||||
try {
|
||||
this.patchBay.unlink();
|
||||
this.linked = false;
|
||||
console.log('[Venmic] Audio unlinked');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Venmic] Unlink error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
isLinked(): boolean {
|
||||
return this.linked;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.unlink();
|
||||
this.patchBay = null;
|
||||
}
|
||||
}
|
||||
|
||||
3
electron/package.json
Normal file
3
electron/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
64
electron/preload/index.ts
Normal file
64
electron/preload/index.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Helium Electron Preload Script
|
||||
*
|
||||
* Exposes safe APIs to the Nuxt renderer for:
|
||||
* - Platform detection
|
||||
* - Desktop capture with audio
|
||||
* - Venmic control (Linux)
|
||||
*/
|
||||
|
||||
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron';
|
||||
|
||||
export interface DesktopSource {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnail: string;
|
||||
appIcon: string | null;
|
||||
display_id?: string;
|
||||
}
|
||||
|
||||
export interface PlatformInfo {
|
||||
platform: string;
|
||||
isLinux: boolean;
|
||||
isMac: boolean;
|
||||
isWindows: boolean;
|
||||
isElectron: boolean;
|
||||
supportsLoopbackAudio: boolean;
|
||||
supportsVenmic: boolean;
|
||||
}
|
||||
|
||||
export interface VenmicLinkOptions {
|
||||
include?: Record<string, string>[];
|
||||
exclude?: Record<string, string>[];
|
||||
ignore_devices?: boolean;
|
||||
only_speakers?: boolean;
|
||||
only_default_speakers?: boolean;
|
||||
}
|
||||
|
||||
const heliumElectronAPI = {
|
||||
isElectron: true as const,
|
||||
getPlatform: (): Promise<PlatformInfo> => ipcRenderer.invoke('helium:get-platform'),
|
||||
getSources: (): Promise<DesktopSource[]> => ipcRenderer.invoke('helium:get-sources'),
|
||||
onSourcesAvailable: (callback: (sources: DesktopSource[]) => void): void => {
|
||||
ipcRenderer.on('desktop-capturer-sources', (_event: IpcRendererEvent, sources: DesktopSource[]) => {
|
||||
callback(sources);
|
||||
});
|
||||
},
|
||||
selectSource: (sourceId: string | null): void => {
|
||||
ipcRenderer.send('desktop-capturer-selected', sourceId);
|
||||
},
|
||||
removeSourcesListener: (): void => {
|
||||
ipcRenderer.removeAllListeners('desktop-capturer-sources');
|
||||
},
|
||||
|
||||
venmicAvailable: (): Promise<boolean> => ipcRenderer.invoke('helium:venmic-available'),
|
||||
venmicList: (): Promise<Record<string, string>[]> => ipcRenderer.invoke('helium:venmic-list'),
|
||||
venmicLink: (options: VenmicLinkOptions): Promise<boolean> => ipcRenderer.invoke('helium:venmic-link', options),
|
||||
venmicUnlink: (): Promise<boolean> => ipcRenderer.invoke('helium:venmic-unlink'),
|
||||
|
||||
checkScreenPermission: (): Promise<string> => ipcRenderer.invoke('helium:check-screen-permission'),
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('heliumElectron', heliumElectronAPI);
|
||||
|
||||
export type HeliumElectronAPI = typeof heliumElectronAPI;
|
||||
16
electron/tsconfig.json
Normal file
16
electron/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["main/**/*.ts", "preload/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user