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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,6 +6,10 @@
|
|||||||
.cache
|
.cache
|
||||||
dist
|
dist
|
||||||
|
|
||||||
|
# Electron compiled output
|
||||||
|
electron/dist
|
||||||
|
dist-electron
|
||||||
|
|
||||||
# Node dependencies
|
# Node dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
|||||||
254
app/composables/useElectron.ts
Normal file
254
app/composables/useElectron.ts
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
export interface PlatformInfo {
|
||||||
|
platform: string;
|
||||||
|
isLinux: boolean;
|
||||||
|
isMac: boolean;
|
||||||
|
isWindows: boolean;
|
||||||
|
isElectron: boolean;
|
||||||
|
supportsLoopbackAudio: boolean;
|
||||||
|
supportsVenmic: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DesktopSource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
thumbnail: string;
|
||||||
|
appIcon: string | null;
|
||||||
|
display_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VenmicLinkOptions {
|
||||||
|
include?: Record<string, string>[];
|
||||||
|
exclude?: Record<string, string>[];
|
||||||
|
ignore_devices?: boolean;
|
||||||
|
only_speakers?: boolean;
|
||||||
|
only_default_speakers?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeliumElectronAPI {
|
||||||
|
isElectron: boolean;
|
||||||
|
getPlatform: () => Promise<PlatformInfo>;
|
||||||
|
getSources: () => Promise<DesktopSource[]>;
|
||||||
|
onSourcesAvailable: (callback: (sources: DesktopSource[]) => void) => void;
|
||||||
|
selectSource: (sourceId: string | null) => void;
|
||||||
|
removeSourcesListener: () => void;
|
||||||
|
venmicAvailable: () => Promise<boolean>;
|
||||||
|
venmicList: () => Promise<Record<string, string>[]>;
|
||||||
|
venmicLink: (options: VenmicLinkOptions) => Promise<boolean>;
|
||||||
|
venmicUnlink: () => Promise<boolean>;
|
||||||
|
checkScreenPermission: () => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
heliumElectron?: HeliumElectronAPI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useElectron() {
|
||||||
|
const isElectron = ref(false);
|
||||||
|
const platformInfo = ref<PlatformInfo | null>(null);
|
||||||
|
const audioSources = ref<Record<string, string>[]>([]);
|
||||||
|
const isVenmicLinked = ref(false);
|
||||||
|
|
||||||
|
const checkElectron = () => {
|
||||||
|
if (import.meta.client) {
|
||||||
|
isElectron.value = !!window.heliumElectron?.isElectron;
|
||||||
|
}
|
||||||
|
return isElectron.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlatformInfo = async (): Promise<PlatformInfo | null> => {
|
||||||
|
if (!checkElectron()) {
|
||||||
|
return {
|
||||||
|
platform: 'browser',
|
||||||
|
isLinux: false,
|
||||||
|
isMac: false,
|
||||||
|
isWindows: false,
|
||||||
|
isElectron: false,
|
||||||
|
supportsLoopbackAudio: false,
|
||||||
|
supportsVenmic: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
platformInfo.value = await window.heliumElectron!.getPlatform();
|
||||||
|
return platformInfo.value;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useElectron] Failed to get platform info:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDesktopSources = async (): Promise<DesktopSource[]> => {
|
||||||
|
if (!checkElectron()) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await window.heliumElectron!.getSources();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useElectron] Failed to get sources:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSourcesAvailable = (callback: (sources: DesktopSource[]) => void) => {
|
||||||
|
if (!checkElectron()) return;
|
||||||
|
window.heliumElectron!.onSourcesAvailable(callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectSource = (sourceId: string | null) => {
|
||||||
|
if (!checkElectron()) return;
|
||||||
|
window.heliumElectron!.selectSource(sourceId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSourcesListener = () => {
|
||||||
|
if (!checkElectron()) return;
|
||||||
|
window.heliumElectron!.removeSourcesListener();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isVenmicAvailable = async (): Promise<boolean> => {
|
||||||
|
if (!checkElectron()) return false;
|
||||||
|
try {
|
||||||
|
return await window.heliumElectron!.venmicAvailable();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVenmicSources = async (): Promise<Record<string, string>[]> => {
|
||||||
|
if (!checkElectron()) return [];
|
||||||
|
try {
|
||||||
|
const sources = await window.heliumElectron!.venmicList();
|
||||||
|
audioSources.value = sources;
|
||||||
|
return sources;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useElectron] Failed to list venmic sources:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkVenmicAudio = async (options: VenmicLinkOptions = {}): Promise<boolean> => {
|
||||||
|
if (!checkElectron()) return false;
|
||||||
|
try {
|
||||||
|
const success = await window.heliumElectron!.venmicLink(options);
|
||||||
|
isVenmicLinked.value = success;
|
||||||
|
return success;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useElectron] Failed to link venmic:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkAllAudio = async (): Promise<boolean> => {
|
||||||
|
return linkVenmicAudio({
|
||||||
|
exclude: [],
|
||||||
|
ignore_devices: true,
|
||||||
|
only_speakers: true,
|
||||||
|
only_default_speakers: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkAppAudio = async (appName: string): Promise<boolean> => {
|
||||||
|
return linkVenmicAudio({
|
||||||
|
include: [{ 'application.name': appName }],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const unlinkVenmicAudio = async (): Promise<boolean> => {
|
||||||
|
if (!checkElectron()) return false;
|
||||||
|
try {
|
||||||
|
const success = await window.heliumElectron!.venmicUnlink();
|
||||||
|
isVenmicLinked.value = !success;
|
||||||
|
return success;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useElectron] Failed to unlink venmic:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startScreenShareWithAudio = async (options: {
|
||||||
|
video?: boolean | MediaTrackConstraints;
|
||||||
|
audioSource?: 'all' | 'none' | string;
|
||||||
|
} = {}): Promise<MediaStream | null> => {
|
||||||
|
const { video = true, audioSource = 'all' } = options;
|
||||||
|
const platform = await getPlatformInfo();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (platform?.isLinux && platform.supportsVenmic && audioSource !== 'none') {
|
||||||
|
if (audioSource === 'all') {
|
||||||
|
await linkAllAudio();
|
||||||
|
} else {
|
||||||
|
await linkAppAudio(audioSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video,
|
||||||
|
audio: audioSource !== 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useElectron] Failed to start screen share:', error);
|
||||||
|
if (platform?.isLinux && isVenmicLinked.value) {
|
||||||
|
await unlinkVenmicAudio();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopScreenShare = async (stream: MediaStream | null) => {
|
||||||
|
if (stream) {
|
||||||
|
stream.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform = platformInfo.value;
|
||||||
|
if (platform?.isLinux && isVenmicLinked.value) {
|
||||||
|
await unlinkVenmicAudio();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const supportsAudioScreenShare = computed(() => {
|
||||||
|
if (!platformInfo.value) return false;
|
||||||
|
return platformInfo.value.supportsLoopbackAudio || platformInfo.value.supportsVenmic;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkElectron();
|
||||||
|
if (isElectron.value) {
|
||||||
|
getPlatformInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
removeSourcesListener();
|
||||||
|
if (isVenmicLinked.value) {
|
||||||
|
unlinkVenmicAudio();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isElectron: readonly(isElectron),
|
||||||
|
platformInfo: readonly(platformInfo),
|
||||||
|
audioSources: readonly(audioSources),
|
||||||
|
isVenmicLinked: readonly(isVenmicLinked),
|
||||||
|
supportsAudioScreenShare,
|
||||||
|
|
||||||
|
checkElectron,
|
||||||
|
getPlatformInfo,
|
||||||
|
getDesktopSources,
|
||||||
|
onSourcesAvailable,
|
||||||
|
selectSource,
|
||||||
|
removeSourcesListener,
|
||||||
|
|
||||||
|
isVenmicAvailable,
|
||||||
|
getVenmicSources,
|
||||||
|
linkVenmicAudio,
|
||||||
|
linkAllAudio,
|
||||||
|
linkAppAudio,
|
||||||
|
unlinkVenmicAudio,
|
||||||
|
|
||||||
|
startScreenShareWithAudio,
|
||||||
|
stopScreenShare,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4">
|
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4">
|
||||||
<div class="flex space-x-4 items-center">
|
<div class="flex flex-wrap gap-4 items-center justify-center">
|
||||||
<Button v-if="!localStream" @click="startScreenShare">
|
<Button v-if="!localStream" @click="startScreenShare">
|
||||||
{{ $t("screenshare") }}
|
{{ $t("screenshare") }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -13,16 +13,56 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<PresetSelect />
|
<PresetSelect />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="streamerStore.code" class="font-mono">{{ streamerStore.code }}</p>
|
|
||||||
<video ref="videofeedRef" autoplay playsinline muted></video>
|
<div v-if="isElectron && supportsAudioScreenShare" class="flex flex-col items-center gap-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Switch id="include-audio" v-model:checked="includeAudio" />
|
||||||
|
<Label for="include-audio" class="text-sm">{{ $t("includeAudio") }}</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="platformInfo?.isLinux && platformInfo?.supportsVenmic && includeAudio" class="flex flex-col items-center gap-2">
|
||||||
|
<Select v-model="selectedAudioSource">
|
||||||
|
<SelectTrigger class="w-[200px]">
|
||||||
|
<SelectValue :placeholder="$t('audioSource')" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">{{ $t("allSystemAudio") }}</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
v-for="source in audioSources"
|
||||||
|
:key="source['node.name']"
|
||||||
|
:value="source['application.name']! || source['node.name']!"
|
||||||
|
>
|
||||||
|
{{ source['application.name'] || source['node.name'] }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button variant="ghost" size="sm" @click="refreshAudioSources">
|
||||||
|
{{ $t("refreshSources") }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isElectron" class="text-xs text-muted-foreground">
|
||||||
|
<span v-if="platformInfo?.isWindows">Windows</span>
|
||||||
|
<span v-else-if="platformInfo?.isMac">macOS</span>
|
||||||
|
<span v-else-if="platformInfo?.isLinux">Linux</span>
|
||||||
|
<span v-if="supportsAudioScreenShare" class="ml-2">• {{ $t("audioSupported") }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="streamerStore.code" class="font-mono text-lg">{{ streamerStore.code }}</p>
|
||||||
|
<video ref="videofeedRef" autoplay playsinline muted class="max-w-full rounded-lg"></video>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useWebSocket } from "@vueuse/core";
|
import { useWebSocket } from "@vueuse/core";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { useStreamerStore } from "~/state/streamer";
|
import { useStreamerStore } from "~/state/streamer";
|
||||||
import { useWebSocketUrl } from "~/composables/useWebSocketUrl";
|
import { useWebSocketUrl } from "~/composables/useWebSocketUrl";
|
||||||
|
import { useElectron } from "~/composables/useElectron";
|
||||||
import PresetSelect from "~/components/app/PresetSelect.vue";
|
import PresetSelect from "~/components/app/PresetSelect.vue";
|
||||||
|
|
||||||
const streamerStore = useStreamerStore();
|
const streamerStore = useStreamerStore();
|
||||||
@@ -30,6 +70,32 @@ const videofeedRef = ref<HTMLVideoElement | null>(null);
|
|||||||
const localStream = ref<MediaStream | null>(null);
|
const localStream = ref<MediaStream | null>(null);
|
||||||
const wsUrl = useWebSocketUrl();
|
const wsUrl = useWebSocketUrl();
|
||||||
|
|
||||||
|
const includeAudio = ref(true);
|
||||||
|
const selectedAudioSource = ref("all");
|
||||||
|
|
||||||
|
const {
|
||||||
|
isElectron,
|
||||||
|
platformInfo,
|
||||||
|
audioSources,
|
||||||
|
supportsAudioScreenShare,
|
||||||
|
getPlatformInfo,
|
||||||
|
getVenmicSources,
|
||||||
|
linkAllAudio,
|
||||||
|
linkAppAudio,
|
||||||
|
unlinkVenmicAudio,
|
||||||
|
} = useElectron();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getPlatformInfo();
|
||||||
|
if (platformInfo.value?.isLinux && platformInfo.value?.supportsVenmic) {
|
||||||
|
await refreshAudioSources();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refreshAudioSources() {
|
||||||
|
await getVenmicSources();
|
||||||
|
}
|
||||||
|
|
||||||
const { send, close: closeWebSocket } = useWebSocket(wsUrl, {
|
const { send, close: closeWebSocket } = useWebSocket(wsUrl, {
|
||||||
autoReconnect: true,
|
autoReconnect: true,
|
||||||
heartbeat: {
|
heartbeat: {
|
||||||
@@ -120,9 +186,25 @@ const { send, close: closeWebSocket } = useWebSocket(wsUrl, {
|
|||||||
|
|
||||||
async function startScreenShare() {
|
async function startScreenShare() {
|
||||||
try {
|
try {
|
||||||
|
const isLinuxWithVenmic = isElectron.value && platformInfo.value?.isLinux && platformInfo.value?.supportsVenmic;
|
||||||
|
|
||||||
|
if (isLinuxWithVenmic && includeAudio.value) {
|
||||||
|
if (selectedAudioSource.value === "all") {
|
||||||
|
await linkAllAudio();
|
||||||
|
} else {
|
||||||
|
await linkAppAudio(selectedAudioSource.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldRequestAudio = isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
video: true,
|
video: true,
|
||||||
audio: false,
|
audio: shouldRequestAudio ? {
|
||||||
|
echoCancellation: false,
|
||||||
|
noiseSuppression: false,
|
||||||
|
autoGainControl: false,
|
||||||
|
} : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
localStream.value = stream;
|
localStream.value = stream;
|
||||||
@@ -138,6 +220,10 @@ async function startScreenShare() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const videoTracks = stream.getVideoTracks();
|
||||||
|
const audioTracks = stream.getAudioTracks();
|
||||||
|
console.log(`[Helium] Stream started - Video: ${videoTracks.length}, Audio: ${audioTracks.length}`);
|
||||||
|
|
||||||
send(
|
send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
event: "create-room",
|
event: "create-room",
|
||||||
@@ -151,27 +237,48 @@ async function startScreenShare() {
|
|||||||
|
|
||||||
async function changeScreenShareSource() {
|
async function changeScreenShareSource() {
|
||||||
try {
|
try {
|
||||||
|
const isLinuxWithVenmic = isElectron.value && platformInfo.value?.isLinux && platformInfo.value?.supportsVenmic;
|
||||||
|
|
||||||
|
if (isLinuxWithVenmic && includeAudio.value) {
|
||||||
|
if (selectedAudioSource.value === "all") {
|
||||||
|
await linkAllAudio();
|
||||||
|
} else {
|
||||||
|
await linkAppAudio(selectedAudioSource.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldRequestAudio = isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||||
|
|
||||||
const newStream = await navigator.mediaDevices.getDisplayMedia({
|
const newStream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
video: true,
|
video: true,
|
||||||
audio: false,
|
audio: shouldRequestAudio,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!localStream.value) return;
|
if (!localStream.value) return;
|
||||||
|
|
||||||
const newVideoTrack = newStream.getVideoTracks()[0];
|
const newVideoTrack = newStream.getVideoTracks()[0];
|
||||||
|
const newAudioTrack = newStream.getAudioTracks()[0];
|
||||||
|
|
||||||
newVideoTrack.onended = () => {
|
newVideoTrack!.onended = () => {
|
||||||
console.log("Screen sharing stopped by user");
|
console.log("Screen sharing stopped by user");
|
||||||
cleanupStreaming();
|
cleanupStreaming();
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
||||||
const senders = pc.getSenders();
|
const senders = pc.getSenders();
|
||||||
const videoSender = senders.find(
|
|
||||||
(sender) => sender.track?.kind === "video",
|
const videoSender = senders.find((sender) => sender.track?.kind === "video");
|
||||||
);
|
|
||||||
if (videoSender) {
|
if (videoSender) {
|
||||||
videoSender.replaceTrack(newVideoTrack);
|
videoSender.replaceTrack(newVideoTrack!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newAudioTrack) {
|
||||||
|
const audioSender = senders.find((sender) => sender.track?.kind === "audio");
|
||||||
|
if (audioSender) {
|
||||||
|
audioSender.replaceTrack(newAudioTrack);
|
||||||
|
} else {
|
||||||
|
pc.addTrack(newAudioTrack, newStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -189,7 +296,7 @@ async function changeScreenShareSource() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupStreaming() {
|
async function cleanupStreaming() {
|
||||||
if (localStream.value) {
|
if (localStream.value) {
|
||||||
localStream.value.getTracks().forEach((track) => {
|
localStream.value.getTracks().forEach((track) => {
|
||||||
track.stop();
|
track.stop();
|
||||||
@@ -197,6 +304,10 @@ function cleanupStreaming() {
|
|||||||
localStream.value = null;
|
localStream.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isElectron.value && platformInfo.value?.isLinux) {
|
||||||
|
await unlinkVenmicAudio();
|
||||||
|
}
|
||||||
|
|
||||||
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
||||||
pc.close();
|
pc.close();
|
||||||
});
|
});
|
||||||
|
|||||||
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"]
|
||||||
|
}
|
||||||
@@ -54,5 +54,10 @@
|
|||||||
"presetCannotBeShared": "This preset cannot be shared",
|
"presetCannotBeShared": "This preset cannot be shared",
|
||||||
"presetSharingDisabled": "The preset owner has not enabled sharing for this preset.",
|
"presetSharingDisabled": "The preset owner has not enabled sharing for this preset.",
|
||||||
"goBack": "Go Back",
|
"goBack": "Go Back",
|
||||||
"changeSource": "Change Source"
|
"changeSource": "Change Source",
|
||||||
|
"includeAudio": "Include Audio",
|
||||||
|
"audioSource": "Audio Source",
|
||||||
|
"allSystemAudio": "All System Audio",
|
||||||
|
"refreshSources": "Refresh Sources",
|
||||||
|
"audioSupported": "Audio Supported"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,5 +54,10 @@
|
|||||||
"presetCannotBeShared": "Este ajuste no se puede compartir",
|
"presetCannotBeShared": "Este ajuste no se puede compartir",
|
||||||
"presetSharingDisabled": "El propietario del ajuste no ha habilitado el uso compartido para este ajuste.",
|
"presetSharingDisabled": "El propietario del ajuste no ha habilitado el uso compartido para este ajuste.",
|
||||||
"goBack": "Volver",
|
"goBack": "Volver",
|
||||||
"changeSource": "Cambiar fuente"
|
"changeSource": "Cambiar fuente",
|
||||||
|
"includeAudio": "Incluir audio",
|
||||||
|
"audioSource": "Fuente de audio",
|
||||||
|
"allSystemAudio": "Todo el audio del sistema",
|
||||||
|
"refreshSources": "Actualizar fuentes",
|
||||||
|
"audioSupported": "Audio soportado"
|
||||||
}
|
}
|
||||||
|
|||||||
49
package.json
49
package.json
@@ -2,6 +2,7 @@
|
|||||||
"name": "helium",
|
"name": "helium",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"main": "./electron/dist/main/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
@@ -9,7 +10,13 @@
|
|||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare",
|
||||||
"ui:add": "pnpm dlx shadcn-vue@latest add",
|
"ui:add": "pnpm dlx shadcn-vue@latest add",
|
||||||
"db:migrate": "drizzle-kit generate && drizzle-kit migrate"
|
"db:migrate": "drizzle-kit generate && drizzle-kit migrate",
|
||||||
|
"electron:compile": "tsc -p electron/tsconfig.json",
|
||||||
|
"electron:dev": "pnpm electron:compile && concurrently \"nuxt dev --host\" \"wait-on http://localhost:3000 && electron . --dev\"",
|
||||||
|
"electron:build": "pnpm electron:compile && nuxt build && electron-builder",
|
||||||
|
"electron:build:win": "pnpm electron:compile && nuxt build && electron-builder --win",
|
||||||
|
"electron:build:mac": "pnpm electron:compile && nuxt build && electron-builder --mac",
|
||||||
|
"electron:build:linux": "pnpm electron:compile && nuxt build && electron-builder --linux"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clerk/localizations": "^3.34.0",
|
"@clerk/localizations": "^3.34.0",
|
||||||
@@ -47,10 +54,48 @@
|
|||||||
"@nuxtjs/color-mode": "^3.5.2",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"@types/node": "^24.9.2",
|
"@types/node": "^24.9.2",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.16.0",
|
||||||
|
"concurrently": "^9.1.2",
|
||||||
"drizzle-kit": "^0.31.8",
|
"drizzle-kit": "^0.31.8",
|
||||||
|
"electron": "^33.0.0",
|
||||||
|
"electron-builder": "^25.1.8",
|
||||||
"nuxi": "^3.29.3",
|
"nuxi": "^3.29.3",
|
||||||
"nuxt-cron": "^1.8.0",
|
"nuxt-cron": "^1.8.0",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3",
|
||||||
|
"wait-on": "^8.0.3"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@vencord/venmic": "^6.1.0"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.helium.app",
|
||||||
|
"productName": "Helium",
|
||||||
|
"directories": {
|
||||||
|
"output": "dist-electron"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"electron/dist/**/*",
|
||||||
|
".output/**/*"
|
||||||
|
],
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": ".output",
|
||||||
|
"to": "app"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mac": {
|
||||||
|
"category": "public.app-category.utilities",
|
||||||
|
"target": ["dmg", "zip"],
|
||||||
|
"hardenedRuntime": true,
|
||||||
|
"entitlements": "electron/entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "electron/entitlements.mac.plist"
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": ["nsis", "portable"]
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": ["AppImage", "deb"],
|
||||||
|
"category": "Utility"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2479
pnpm-lock.yaml
generated
2479
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- '@vencord/venmic'
|
||||||
|
- electron
|
||||||
Reference in New Issue
Block a user