mirror of
https://github.com/SrIzan10/helium.git
synced 2026-06-15 00:12:24 +00:00
Compare commits
9 Commits
feat/andro
...
v0.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 7170d87bbd | |||
| f0ac8ac951 | |||
| aad1c72bef | |||
| c61e6e3679 | |||
| 21f7df22d5 | |||
| 0744922d19 | |||
| a2414aee2f | |||
| a90d7c8f8c | |||
| 1d926ce082 |
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
.git
|
||||
.nuxt
|
||||
.output
|
||||
.data
|
||||
node_modules
|
||||
dist
|
||||
dist-electron
|
||||
native-app
|
||||
coverage
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
2
.github/workflows/electron-release.yml
vendored
2
.github/workflows/electron-release.yml
vendored
@@ -30,8 +30,6 @@ jobs:
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
RUN corepack enable
|
||||
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
# Copy package.json and your lockfile, here we add pnpm-lock.yaml for illustration
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm i
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy the entire project
|
||||
COPY . ./
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useStreamerStore } from "~/state/streamer";
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const selectedValue = ref("");
|
||||
const modelValue = defineModel<string>({ default: "" });
|
||||
const presets = ref<PresetUser[]>([]);
|
||||
const loading = ref(true);
|
||||
const streamerStore = useStreamerStore();
|
||||
@@ -27,6 +28,7 @@ onMounted(async () => {
|
||||
const defaultPreset = presets.value.find((p) => p.isDefault);
|
||||
if (defaultPreset) {
|
||||
selectedValue.value = defaultPreset.presetId;
|
||||
modelValue.value = defaultPreset.presetId;
|
||||
// Load the default preset's ice servers
|
||||
loadPresetIceServers(defaultPreset.presetId);
|
||||
}
|
||||
@@ -60,9 +62,19 @@ watch(selectedValue, (newValue) => {
|
||||
if (newValue === "create-new") {
|
||||
router.push("/presets/new");
|
||||
selectedValue.value = "";
|
||||
modelValue.value = "";
|
||||
} else if (newValue) {
|
||||
modelValue.value = newValue;
|
||||
// Load ice servers for the selected preset
|
||||
loadPresetIceServers(newValue);
|
||||
} else {
|
||||
modelValue.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
watch(modelValue, (newValue) => {
|
||||
if (newValue !== selectedValue.value) {
|
||||
selectedValue.value = newValue;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -24,6 +24,28 @@ export interface VenmicLinkOptions {
|
||||
only_default_speakers?: boolean;
|
||||
}
|
||||
|
||||
export interface StreamingClosePromptOptions {
|
||||
title: string;
|
||||
message: string;
|
||||
confirmLabel: string;
|
||||
cancelLabel: string;
|
||||
}
|
||||
|
||||
export type UpdateStatus =
|
||||
| "checking"
|
||||
| "available"
|
||||
| "not-available"
|
||||
| "download-progress"
|
||||
| "downloaded"
|
||||
| "error";
|
||||
|
||||
export interface UpdateStatusPayload {
|
||||
status: UpdateStatus;
|
||||
version?: string;
|
||||
percent?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface HeliumElectronAPI {
|
||||
isElectron: boolean;
|
||||
getPlatform: () => Promise<PlatformInfo>;
|
||||
@@ -37,6 +59,13 @@ interface HeliumElectronAPI {
|
||||
venmicUnlink: () => Promise<boolean>;
|
||||
checkScreenPermission: () => Promise<string>;
|
||||
openScreenPermissionSettings: () => Promise<boolean>;
|
||||
setStreamingActive: (
|
||||
active: boolean,
|
||||
promptOptions?: StreamingClosePromptOptions,
|
||||
) => Promise<boolean>;
|
||||
checkForUpdates: () => Promise<boolean>;
|
||||
installUpdate: () => Promise<boolean>;
|
||||
onUpdateStatus: (callback: (payload: UpdateStatusPayload) => void) => () => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -235,6 +264,50 @@ export function useElectron() {
|
||||
}
|
||||
};
|
||||
|
||||
const setStreamingActive = async (
|
||||
active: boolean,
|
||||
promptOptions?: StreamingClosePromptOptions,
|
||||
): Promise<boolean> => {
|
||||
if (!checkElectron()) return false;
|
||||
|
||||
try {
|
||||
return await window.heliumElectron!.setStreamingActive(
|
||||
active,
|
||||
promptOptions,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[useElectron] Failed to set streaming status:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkForUpdates = async (): Promise<boolean> => {
|
||||
if (!checkElectron()) return false;
|
||||
|
||||
try {
|
||||
return await window.heliumElectron!.checkForUpdates();
|
||||
} catch (error) {
|
||||
console.error("[useElectron] Failed to check for updates:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const installUpdate = async (): Promise<boolean> => {
|
||||
if (!checkElectron()) return false;
|
||||
|
||||
try {
|
||||
return await window.heliumElectron!.installUpdate();
|
||||
} catch (error) {
|
||||
console.error("[useElectron] Failed to install update:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateStatus = (callback: (payload: UpdateStatusPayload) => void): (() => void) => {
|
||||
if (!checkElectron()) return () => {};
|
||||
return window.heliumElectron!.onUpdateStatus(callback);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkElectron();
|
||||
if (isElectron.value) {
|
||||
@@ -271,6 +344,10 @@ export function useElectron() {
|
||||
unlinkVenmicAudio,
|
||||
getScreenPermissionStatus,
|
||||
openScreenPermissionSettings,
|
||||
setStreamingActive,
|
||||
checkForUpdates,
|
||||
installUpdate,
|
||||
onUpdateStatus,
|
||||
|
||||
startScreenShareWithAudio,
|
||||
stopScreenShare,
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import SignInDialog from "~/components/app/SignInDialog.vue";
|
||||
import ThemeDropdown from "~/components/ui/ThemeDropdown.vue";
|
||||
import LanguageSwitcher from "~/components/app/LanguageSwitcher.vue";
|
||||
import { useElectron } from "~/composables/useElectron";
|
||||
import { useElectron, type UpdateStatusPayload } from "~/composables/useElectron";
|
||||
import "vue-sonner/style.css";
|
||||
import { toast } from "vue-sonner";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import {
|
||||
Sheet,
|
||||
@@ -17,7 +18,15 @@ import LogoSvg from "~/assets/logo.svg?component";
|
||||
|
||||
const { t } = useI18n();
|
||||
const mobileMenuOpen = ref(false);
|
||||
const { isElectron, platformInfo, getPlatformInfo } = useElectron();
|
||||
const {
|
||||
isElectron,
|
||||
platformInfo,
|
||||
getPlatformInfo,
|
||||
installUpdate,
|
||||
onUpdateStatus,
|
||||
} = useElectron();
|
||||
let removeUpdateStatusListener: (() => void) | undefined;
|
||||
let updateProgressToastId: string | number | undefined;
|
||||
|
||||
const isMacElectron = computed(() => {
|
||||
return isElectron.value && platformInfo.value?.isMac;
|
||||
@@ -32,10 +41,74 @@ onMounted(async () => {
|
||||
const navLinks = [
|
||||
{ to: "/", label: "home" },
|
||||
{ to: "/stream", label: "stream" },
|
||||
{ to: "/about", label: "about" },
|
||||
{ to: "/downloads", label: "downloads" },
|
||||
{ to: "/about", label: "about", hideInElectron: true },
|
||||
{ to: "/downloads", label: "downloads", hideInElectron: true },
|
||||
{ to: "/presets", label: "presets", requiresAuth: true },
|
||||
];
|
||||
|
||||
const visibleNavLinks = computed(() => {
|
||||
return navLinks.filter((link) => !isElectron.value || !link.hideInElectron);
|
||||
});
|
||||
|
||||
const showUpdateMessage = (payload: UpdateStatusPayload): void => {
|
||||
if (payload.status === "checking") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.status === "available") {
|
||||
toast.info(t("updateAvailable"), {
|
||||
description: payload.version
|
||||
? t("updateAvailableDescription", { version: payload.version })
|
||||
: t("updateAvailableDescriptionWithoutVersion"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.status === "not-available") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.status === "download-progress") {
|
||||
const percent = payload.percent ?? 0;
|
||||
updateProgressToastId = toast.loading(t("updateDownloading"), {
|
||||
id: updateProgressToastId,
|
||||
description: t("updateDownloadProgress", { percent }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.status === "downloaded") {
|
||||
if (updateProgressToastId) {
|
||||
toast.dismiss(updateProgressToastId);
|
||||
updateProgressToastId = undefined;
|
||||
}
|
||||
|
||||
toast.success(t("updateReady"), {
|
||||
description: payload.version
|
||||
? t("updateReadyDescription", { version: payload.version })
|
||||
: t("updateReadyDescriptionWithoutVersion"),
|
||||
action: {
|
||||
label: t("restartToUpdate"),
|
||||
onClick: () => {
|
||||
void installUpdate();
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(t("updateFailed"), {
|
||||
description: payload.message || t("updateFailedDescription"),
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
removeUpdateStatusListener = onUpdateStatus(showUpdateMessage);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
removeUpdateStatusListener?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -56,7 +129,7 @@ const navLinks = [
|
||||
<span class="leading-none">helium</span>
|
||||
</NuxtLink>
|
||||
<nav class="hidden md:flex space-x-4">
|
||||
<template v-for="link in navLinks" :key="link.to">
|
||||
<template v-for="link in visibleNavLinks" :key="link.to">
|
||||
<ClientOnly v-if="link.requiresAuth">
|
||||
<SignedIn>
|
||||
<NuxtLink
|
||||
@@ -114,7 +187,7 @@ const navLinks = [
|
||||
<SheetTitle>{{ t("menu") || "Menu" }}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<nav class="flex flex-col space-y-4 mt-6">
|
||||
<template v-for="link in navLinks" :key="link.to">
|
||||
<template v-for="link in visibleNavLinks" :key="link.to">
|
||||
<ClientOnly v-if="link.requiresAuth">
|
||||
<SignedIn>
|
||||
<NuxtLink
|
||||
|
||||
@@ -25,6 +25,13 @@ definePageMeta({
|
||||
layout: "default",
|
||||
});
|
||||
|
||||
interface PlatformDownload {
|
||||
name: string;
|
||||
icon: Component;
|
||||
formats: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface GitHubReleaseAsset {
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
@@ -35,13 +42,6 @@ interface GitHubRelease {
|
||||
assets: GitHubReleaseAsset[];
|
||||
}
|
||||
|
||||
interface PlatformDownload {
|
||||
name: string;
|
||||
icon: Component;
|
||||
formats: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const repositoryUrl = "https://github.com/SrIzan10/helium";
|
||||
@@ -76,15 +76,8 @@ function findReleaseAsset(
|
||||
});
|
||||
}
|
||||
|
||||
function getReleaseAssetUrl(
|
||||
patterns: readonly RegExp[],
|
||||
fallbackUrl?: string,
|
||||
): string {
|
||||
return (
|
||||
findReleaseAsset(patterns)?.browser_download_url ??
|
||||
fallbackUrl ??
|
||||
latestReleaseUrl.value
|
||||
);
|
||||
function getReleaseAssetUrl(patterns: readonly RegExp[]): string {
|
||||
return findReleaseAsset(patterns)?.browser_download_url ?? latestReleaseUrl.value;
|
||||
}
|
||||
|
||||
const desktopPlatforms = computed<PlatformDownload[]>(() => {
|
||||
@@ -93,19 +86,19 @@ const desktopPlatforms = computed<PlatformDownload[]>(() => {
|
||||
name: "Windows",
|
||||
icon: Laptop,
|
||||
formats: "NSIS, Portable",
|
||||
href: getReleaseAssetUrl([/-Setup-.*\\.exe$/i, /\\.exe$/i]),
|
||||
href: getReleaseAssetUrl([/-Setup-.*\.exe$/i, /\.exe$/i]),
|
||||
},
|
||||
{
|
||||
name: "macOS",
|
||||
icon: Apple,
|
||||
formats: "DMG, ZIP",
|
||||
href: getReleaseAssetUrl([/\\.dmg$/i, /-mac\\.zip$/i]),
|
||||
href: getReleaseAssetUrl([/\.dmg$/i, /-mac\.zip$/i]),
|
||||
},
|
||||
{
|
||||
name: "Linux",
|
||||
icon: Laptop,
|
||||
formats: "AppImage",
|
||||
href: getReleaseAssetUrl([/\\.AppImage$/i]),
|
||||
href: getReleaseAssetUrl([/\.AppImage$/i]),
|
||||
},
|
||||
];
|
||||
});
|
||||
@@ -115,7 +108,7 @@ const androidPlatform = computed<PlatformDownload>(() => {
|
||||
name: "Android",
|
||||
icon: Smartphone,
|
||||
formats: "APK",
|
||||
href: getReleaseAssetUrl([/^helium-android-.*\\.apk$/i]),
|
||||
href: getReleaseAssetUrl([/^helium-android-.*\.apk$/i]),
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
const { locale } = useI18n();
|
||||
const clerkLocaleVersion = useState("clerk-locale-version", () => 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<SignIn routing="path" path="/sign-in" />
|
||||
<SignIn :key="`${locale}-${clerkLocaleVersion}`" routing="path" path="/sign-in" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
const { locale } = useI18n();
|
||||
const clerkLocaleVersion = useState("clerk-locale-version", () => 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<SignUp routing="path" path="/sign-up" />
|
||||
<SignUp :key="`${locale}-${clerkLocaleVersion}`" routing="path" path="/sign-up" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,56 +1,189 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4">
|
||||
<div class="flex flex-wrap gap-4 items-center justify-center">
|
||||
<Button v-if="!localStream" @click="startScreenShare">
|
||||
{{ $t("screenshare") }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="localStream"
|
||||
@click="changeScreenShareSource"
|
||||
variant="outline"
|
||||
>
|
||||
{{ $t("changeSource") }}
|
||||
</Button>
|
||||
<PresetSelect />
|
||||
<div
|
||||
class="min-h-[80vh] flex flex-col items-center justify-start gap-8 mt-10 px-4 pb-16"
|
||||
>
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="text-4xl font-bold tracking-tight">{{ $t("stream") }}</h1>
|
||||
</div>
|
||||
|
||||
<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']!"
|
||||
<div
|
||||
class="w-full max-w-5xl grid grid-cols-1 lg:grid-cols-[380px_1fr] gap-6 items-start"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base flex items-center gap-2">
|
||||
<Monitor class="size-4 text-primary" />
|
||||
{{ $t("screenshare") }}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{{ $t("streamControlDescription") }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<Button
|
||||
v-if="!localStream"
|
||||
@click="startScreenShare"
|
||||
size="lg"
|
||||
class="w-full gap-2"
|
||||
>
|
||||
{{ source['application.name'] || source['node.name'] }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="ghost" size="sm" @click="refreshAudioSources">
|
||||
{{ $t("refreshSources") }}
|
||||
</Button>
|
||||
<Monitor class="size-4" />
|
||||
{{ $t("screenshare") }}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="localStream"
|
||||
@click="changeScreenShareSource"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="w-full gap-2"
|
||||
>
|
||||
<RefreshCw class="size-4" />
|
||||
{{ $t("changeSource") }}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="localStream"
|
||||
@click="stopStreaming"
|
||||
variant="destructive"
|
||||
size="lg"
|
||||
class="w-full gap-2"
|
||||
>
|
||||
<Square class="size-4" />
|
||||
{{ $t("stopStream") }}
|
||||
</Button>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
class="text-xs text-muted-foreground uppercase tracking-wider font-semibold"
|
||||
>
|
||||
{{ $t("selectAPreset") }}
|
||||
</Label>
|
||||
<PresetSelect v-model="selectedPresetId" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-if="streamerStore.code" class="border-primary/30">
|
||||
<CardHeader class="pb-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-base flex items-center gap-2">
|
||||
<Share2 class="size-4 text-primary" />
|
||||
{{ $t("shareCode") }}
|
||||
</CardTitle>
|
||||
<Badge class="text-xs bg-green-500/15 text-green-600 dark:text-green-400 border-0">
|
||||
● {{ $t("live") }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<p
|
||||
class="font-mono text-5xl font-bold tracking-[0.3em] text-primary text-center py-2"
|
||||
>
|
||||
{{ streamerStore.code }}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full gap-2"
|
||||
@click="copyCode"
|
||||
>
|
||||
<Copy class="size-3" />
|
||||
{{ $t("copyCode") }}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-if="isElectron && supportsAudioScreenShare">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base flex items-center gap-2">
|
||||
<Radio class="size-4 text-primary" />
|
||||
{{ $t("includeAudio") }}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="include-audio" v-model="includeAudio" />
|
||||
<Label for="include-audio" class="text-sm font-normal">
|
||||
{{ $t("includeAudio") }}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="platformInfo?.isLinux && platformInfo?.supportsVenmic && includeAudio"
|
||||
class="space-y-3 pt-2 border-t border-border"
|
||||
>
|
||||
<Select v-model="selectedAudioSource">
|
||||
<SelectTrigger class="w-full">
|
||||
<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"
|
||||
class="gap-2 w-full"
|
||||
>
|
||||
<RefreshCw class="size-3" />
|
||||
{{ $t("refreshSources") }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="relative rounded-xl overflow-hidden border shadow-sm"
|
||||
:class="[
|
||||
localStream
|
||||
? 'aspect-video bg-black'
|
||||
: 'aspect-video bg-muted/50 flex items-center justify-center',
|
||||
]"
|
||||
>
|
||||
<video
|
||||
ref="videofeedRef"
|
||||
autoplay
|
||||
playsinline
|
||||
muted
|
||||
class="w-full h-full object-contain"
|
||||
:class="{ hidden: !localStream }"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="!localStream"
|
||||
class="flex flex-col items-center gap-3 text-muted-foreground"
|
||||
>
|
||||
<div
|
||||
class="size-16 rounded-full bg-muted flex items-center justify-center"
|
||||
>
|
||||
<Monitor class="size-8 opacity-40" />
|
||||
</div>
|
||||
<div class="text-center space-y-1">
|
||||
<p class="text-sm font-medium">{{ $t("previewWaiting") }}</p>
|
||||
<p class="text-xs opacity-60">{{ $t("previewWaitingDescription") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -60,20 +193,45 @@ import { toast } from "vue-sonner";
|
||||
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 { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Copy,
|
||||
Monitor,
|
||||
Radio,
|
||||
RefreshCw,
|
||||
Share2,
|
||||
Square,
|
||||
} from "lucide-vue-next";
|
||||
import { useStreamerStore } from "~/state/streamer";
|
||||
import { useWebSocketUrl } from "~/composables/useWebSocketUrl";
|
||||
import { useElectron } from "~/composables/useElectron";
|
||||
import PresetSelect from "~/components/app/PresetSelect.vue";
|
||||
|
||||
const streamerStore = useStreamerStore();
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const videofeedRef = ref<HTMLVideoElement | null>(null);
|
||||
const localStream = ref<MediaStream | null>(null);
|
||||
const isCleaningUp = ref(false);
|
||||
const wsUrl = useWebSocketUrl();
|
||||
|
||||
const includeAudio = ref(true);
|
||||
const selectedAudioSource = ref("all");
|
||||
const selectedPresetId = ref("");
|
||||
|
||||
const {
|
||||
isElectron,
|
||||
@@ -87,19 +245,68 @@ const {
|
||||
unlinkVenmicAudio,
|
||||
getScreenPermissionStatus,
|
||||
openScreenPermissionSettings,
|
||||
setStreamingActive,
|
||||
} = useElectron();
|
||||
|
||||
onMounted(async () => {
|
||||
await getPlatformInfo();
|
||||
|
||||
if (platformInfo.value?.isLinux && platformInfo.value?.supportsVenmic) {
|
||||
await refreshAudioSources();
|
||||
}
|
||||
});
|
||||
|
||||
watch(localStream, async (stream) => {
|
||||
await nextTick();
|
||||
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = stream;
|
||||
}
|
||||
});
|
||||
|
||||
watch([localStream, locale], async ([stream]) => {
|
||||
if (!isElectron.value) return;
|
||||
|
||||
await setStreamingActive(
|
||||
!!stream,
|
||||
stream
|
||||
? {
|
||||
title: t("activeStreamCloseTitle"),
|
||||
message: t("activeStreamCloseMessage"),
|
||||
confirmLabel: t("stopStreamAndClose"),
|
||||
cancelLabel: t("keepStreaming"),
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
});
|
||||
|
||||
async function refreshAudioSources() {
|
||||
await getVenmicSources();
|
||||
}
|
||||
|
||||
async function copyCode() {
|
||||
await navigator.clipboard.writeText(streamerStore.code);
|
||||
toast.success(t("codeCopied"));
|
||||
}
|
||||
|
||||
function notifyRoomClosed() {
|
||||
if (!streamerStore.code) return;
|
||||
|
||||
send(
|
||||
JSON.stringify({
|
||||
event: "close-room",
|
||||
roomId: streamerStore.code,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function stopStreaming() {
|
||||
if (isCleaningUp.value) return;
|
||||
|
||||
notifyRoomClosed();
|
||||
await cleanupStreaming();
|
||||
}
|
||||
|
||||
const { send, close: closeWebSocket } = useWebSocket(wsUrl, {
|
||||
autoReconnect: true,
|
||||
heartbeat: {
|
||||
@@ -189,9 +396,17 @@ const { send, close: closeWebSocket } = useWebSocket(wsUrl, {
|
||||
});
|
||||
|
||||
async function startScreenShare() {
|
||||
if (!selectedPresetId.value) {
|
||||
toast.error(t("selectPresetBeforeStreaming"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isLinuxWithVenmic = isElectron.value && platformInfo.value?.isLinux && platformInfo.value?.supportsVenmic;
|
||||
|
||||
const isLinuxWithVenmic =
|
||||
isElectron.value &&
|
||||
platformInfo.value?.isLinux &&
|
||||
platformInfo.value?.supportsVenmic;
|
||||
|
||||
if (isLinuxWithVenmic && includeAudio.value) {
|
||||
if (selectedAudioSource.value === "all") {
|
||||
await linkAllAudio();
|
||||
@@ -200,33 +415,34 @@ async function startScreenShare() {
|
||||
}
|
||||
}
|
||||
|
||||
const shouldRequestAudio = isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||
const shouldRequestAudio =
|
||||
isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: true,
|
||||
audio: shouldRequestAudio ? {
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false,
|
||||
autoGainControl: false,
|
||||
} : false,
|
||||
audio: shouldRequestAudio
|
||||
? {
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false,
|
||||
autoGainControl: false,
|
||||
}
|
||||
: false,
|
||||
});
|
||||
|
||||
localStream.value = stream;
|
||||
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = stream;
|
||||
}
|
||||
|
||||
stream.getTracks().forEach((track) => {
|
||||
track.onended = () => {
|
||||
console.log("Screen sharing stopped by user");
|
||||
cleanupStreaming();
|
||||
stopStreaming();
|
||||
};
|
||||
});
|
||||
|
||||
const videoTracks = stream.getVideoTracks();
|
||||
const audioTracks = stream.getAudioTracks();
|
||||
console.log(`[Helium] Stream started - Video: ${videoTracks.length}, Audio: ${audioTracks.length}`);
|
||||
console.log(
|
||||
`[Helium] Stream started - Video: ${videoTracks.length}, Audio: ${audioTracks.length}`,
|
||||
);
|
||||
|
||||
send(
|
||||
JSON.stringify({
|
||||
@@ -242,8 +458,11 @@ async function startScreenShare() {
|
||||
|
||||
async function changeScreenShareSource() {
|
||||
try {
|
||||
const isLinuxWithVenmic = isElectron.value && platformInfo.value?.isLinux && platformInfo.value?.supportsVenmic;
|
||||
|
||||
const isLinuxWithVenmic =
|
||||
isElectron.value &&
|
||||
platformInfo.value?.isLinux &&
|
||||
platformInfo.value?.supportsVenmic;
|
||||
|
||||
if (isLinuxWithVenmic && includeAudio.value) {
|
||||
if (selectedAudioSource.value === "all") {
|
||||
await linkAllAudio();
|
||||
@@ -252,7 +471,8 @@ async function changeScreenShareSource() {
|
||||
}
|
||||
}
|
||||
|
||||
const shouldRequestAudio = isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||
const shouldRequestAudio =
|
||||
isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||
|
||||
const newStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: true,
|
||||
@@ -266,19 +486,23 @@ async function changeScreenShareSource() {
|
||||
|
||||
newVideoTrack!.onended = () => {
|
||||
console.log("Screen sharing stopped by user");
|
||||
cleanupStreaming();
|
||||
stopStreaming();
|
||||
};
|
||||
|
||||
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
||||
const senders = pc.getSenders();
|
||||
|
||||
const videoSender = senders.find((sender) => sender.track?.kind === "video");
|
||||
|
||||
const videoSender = senders.find(
|
||||
(sender) => sender.track?.kind === "video",
|
||||
);
|
||||
if (videoSender) {
|
||||
videoSender.replaceTrack(newVideoTrack!);
|
||||
}
|
||||
|
||||
|
||||
if (newAudioTrack) {
|
||||
const audioSender = senders.find((sender) => sender.track?.kind === "audio");
|
||||
const audioSender = senders.find(
|
||||
(sender) => sender.track?.kind === "audio",
|
||||
);
|
||||
if (audioSender) {
|
||||
audioSender.replaceTrack(newAudioTrack);
|
||||
} else {
|
||||
@@ -292,10 +516,6 @@ async function changeScreenShareSource() {
|
||||
});
|
||||
|
||||
localStream.value = newStream;
|
||||
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = newStream;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to change screen share source:", error);
|
||||
await handleScreenShareError(error);
|
||||
@@ -306,7 +526,11 @@ async function handleScreenShareError(error: unknown): Promise<void> {
|
||||
const isPermissionDeniedError =
|
||||
error instanceof DOMException && error.name === "NotAllowedError";
|
||||
|
||||
if (!isPermissionDeniedError || !isElectron.value || !platformInfo.value?.isMac) {
|
||||
if (
|
||||
!isPermissionDeniedError ||
|
||||
!isElectron.value ||
|
||||
!platformInfo.value?.isMac
|
||||
) {
|
||||
toast.error(t("failedToStartScreenShare"));
|
||||
return;
|
||||
}
|
||||
@@ -329,28 +553,36 @@ async function handleScreenShareError(error: unknown): Promise<void> {
|
||||
}
|
||||
|
||||
async function cleanupStreaming() {
|
||||
if (localStream.value) {
|
||||
localStream.value.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
if (isCleaningUp.value) return;
|
||||
|
||||
isCleaningUp.value = true;
|
||||
|
||||
try {
|
||||
if (localStream.value) {
|
||||
localStream.value.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
});
|
||||
localStream.value = null;
|
||||
}
|
||||
|
||||
if (isElectron.value && platformInfo.value?.isLinux) {
|
||||
await unlinkVenmicAudio();
|
||||
}
|
||||
|
||||
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
||||
pc.close();
|
||||
});
|
||||
localStream.value = null;
|
||||
|
||||
streamerStore.clearPeerConnections();
|
||||
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = null;
|
||||
}
|
||||
|
||||
streamerStore.setCode("");
|
||||
} finally {
|
||||
isCleaningUp.value = false;
|
||||
}
|
||||
|
||||
if (isElectron.value && platformInfo.value?.isLinux) {
|
||||
await unlinkVenmicAudio();
|
||||
}
|
||||
|
||||
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
||||
pc.close();
|
||||
});
|
||||
|
||||
streamerStore.clearPeerConnections();
|
||||
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = null;
|
||||
}
|
||||
|
||||
streamerStore.setCode("");
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -1,44 +1,106 @@
|
||||
import { updateClerkOptions } from "#imports";
|
||||
import { esES } from "@clerk/localizations";
|
||||
import { shadcn } from "@clerk/themes";
|
||||
import type { Ref } from "vue";
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const i18n = nuxtApp.$i18n as any;
|
||||
interface ClerkLocaleOptions {
|
||||
localization: typeof esES | undefined;
|
||||
appearance: {
|
||||
theme: typeof shadcn;
|
||||
};
|
||||
}
|
||||
|
||||
if (!i18n) return;
|
||||
interface I18nRuntime {
|
||||
locale: Ref<string>;
|
||||
}
|
||||
|
||||
interface MutablePublicConfig {
|
||||
clerk?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function getClerkLocaleOptions(locale: string): ClerkLocaleOptions {
|
||||
return {
|
||||
localization: locale === "es" ? esES : undefined,
|
||||
appearance: {
|
||||
theme: shadcn,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getCookieValue(name: string): string | undefined {
|
||||
const cookie = document.cookie
|
||||
.split("; ")
|
||||
.find((entry) => entry.startsWith(`${name}=`));
|
||||
|
||||
return cookie ? decodeURIComponent(cookie.split("=")[1] ?? "") : undefined;
|
||||
}
|
||||
|
||||
function getInitialLocale(): string {
|
||||
const cookieLocale = getCookieValue("i18n_locale");
|
||||
if (cookieLocale) return cookieLocale;
|
||||
|
||||
if (navigator.language.toLowerCase().startsWith("es")) {
|
||||
return "es";
|
||||
}
|
||||
|
||||
return "en";
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: "clerk-locale",
|
||||
enforce: "pre",
|
||||
setup(nuxtApp) {
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const clerkLocaleVersion = useState("clerk-locale-version", () => 0);
|
||||
const publicConfig = runtimeConfig.public as MutablePublicConfig;
|
||||
const initialLocale = getInitialLocale();
|
||||
|
||||
function updateRuntimeConfig(options: ClerkLocaleOptions): void {
|
||||
publicConfig.clerk = {
|
||||
...(publicConfig.clerk ?? {}),
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
updateRuntimeConfig(getClerkLocaleOptions(initialLocale));
|
||||
|
||||
function updateLocale(locale: string, attempts = 0): void {
|
||||
const options = getClerkLocaleOptions(locale);
|
||||
|
||||
updateRuntimeConfig(options);
|
||||
|
||||
nuxtApp.hook("app:mounted", () => {
|
||||
const checkClerk = () => {
|
||||
try {
|
||||
const testUpdate = () => {
|
||||
updateClerkOptions({
|
||||
localization: i18n.locale.value === "es" ? esES : undefined,
|
||||
appearance: {
|
||||
theme: shadcn,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
testUpdate();
|
||||
|
||||
watch(
|
||||
() => i18n.locale.value,
|
||||
(newLocale) => {
|
||||
const clerkLocale = newLocale === "es" ? esES : undefined;
|
||||
|
||||
updateClerkOptions({
|
||||
localization: clerkLocale,
|
||||
appearance: {
|
||||
theme: shadcn,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
setTimeout(checkClerk, 100);
|
||||
updateClerkOptions(options);
|
||||
clerkLocaleVersion.value += 1;
|
||||
} catch {
|
||||
if (attempts < 20) {
|
||||
setTimeout(() => updateLocale(locale, attempts + 1), 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
checkClerk();
|
||||
});
|
||||
nuxtApp.hook("i18n:beforeLocaleSwitch", (options) => {
|
||||
updateLocale(options.newLocale);
|
||||
});
|
||||
|
||||
nuxtApp.hook("i18n:localeSwitched", (options) => {
|
||||
updateLocale(options.newLocale);
|
||||
});
|
||||
|
||||
nuxtApp.hook("app:mounted", () => {
|
||||
const i18n = nuxtApp.$i18n as I18nRuntime | undefined;
|
||||
if (!i18n) {
|
||||
updateLocale(initialLocale);
|
||||
return;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => i18n.locale.value,
|
||||
(locale) => {
|
||||
updateLocale(locale);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,12 +3,14 @@ import {
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
desktopCapturer,
|
||||
dialog,
|
||||
session,
|
||||
shell,
|
||||
systemPreferences,
|
||||
type IpcMainInvokeEvent,
|
||||
type DesktopCapturerSource,
|
||||
} from 'electron';
|
||||
import electronUpdater, { type AppUpdater, type ProgressInfo, type UpdateInfo } from 'electron-updater';
|
||||
import path from 'path';
|
||||
import { VenmicManager, type VenmicLinkOptions } from './venmic';
|
||||
|
||||
@@ -48,11 +50,96 @@ const NUXT_DEV_URL = process.env.NUXT_DEV_URL || 'http://localhost:3000';
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let venmicManager: VenmicManager | null = null;
|
||||
let isStreamingActive = false;
|
||||
let autoUpdaterConfigured = false;
|
||||
let streamingClosePrompt = {
|
||||
title: 'Active stream',
|
||||
message: 'A stream is still active. Closing Helium will stop it for all viewers.',
|
||||
confirmLabel: 'Stop stream and close',
|
||||
cancelLabel: 'Keep streaming',
|
||||
};
|
||||
|
||||
console.log('[Helium] Platform:', process.platform);
|
||||
console.log('[Helium] Wayland:', isWayland);
|
||||
console.log('[Helium] XDG_SESSION_TYPE:', process.env.XDG_SESSION_TYPE);
|
||||
|
||||
type UpdateStatus =
|
||||
| 'checking'
|
||||
| 'available'
|
||||
| 'not-available'
|
||||
| 'download-progress'
|
||||
| 'downloaded'
|
||||
| 'error';
|
||||
|
||||
interface UpdateStatusPayload {
|
||||
status: UpdateStatus;
|
||||
version?: string;
|
||||
percent?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
function getAutoUpdater(): AppUpdater {
|
||||
const { autoUpdater } = electronUpdater;
|
||||
return autoUpdater;
|
||||
}
|
||||
|
||||
function sendUpdateStatus(payload: UpdateStatusPayload): void {
|
||||
for (const window of BrowserWindow.getAllWindows()) {
|
||||
if (!window.isDestroyed()) {
|
||||
window.webContents.send('helium:update-status', payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toUpdateStatus(status: UpdateStatus, info?: UpdateInfo): UpdateStatusPayload {
|
||||
return {
|
||||
status,
|
||||
version: info?.version,
|
||||
};
|
||||
}
|
||||
|
||||
function setupAutoUpdater(): void {
|
||||
if (isDev || !app.isPackaged || autoUpdaterConfigured) return;
|
||||
|
||||
autoUpdaterConfigured = true;
|
||||
const autoUpdater = getAutoUpdater();
|
||||
|
||||
autoUpdater.autoDownload = true;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
sendUpdateStatus({ status: 'checking' });
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', (info: UpdateInfo) => {
|
||||
sendUpdateStatus(toUpdateStatus('available', info));
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', (info: UpdateInfo) => {
|
||||
sendUpdateStatus(toUpdateStatus('not-available', info));
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (progress: ProgressInfo) => {
|
||||
sendUpdateStatus({
|
||||
status: 'download-progress',
|
||||
percent: Math.round(progress.percent),
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
|
||||
sendUpdateStatus(toUpdateStatus('downloaded', info));
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (error: Error) => {
|
||||
console.error('[Helium] Auto updater error:', error);
|
||||
sendUpdateStatus({ status: 'error', message: error.message });
|
||||
});
|
||||
|
||||
void autoUpdater.checkForUpdatesAndNotify().catch((error: unknown) => {
|
||||
console.error('[Helium] Failed to check for updates:', error);
|
||||
});
|
||||
}
|
||||
|
||||
if (isLinux) {
|
||||
try {
|
||||
venmicManager = new VenmicManager();
|
||||
@@ -89,6 +176,27 @@ function createWindow(): void {
|
||||
mainWindow.loadURL(prodUrl);
|
||||
}
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
if (!isStreamingActive || !mainWindow) return;
|
||||
|
||||
const choice = dialog.showMessageBoxSync(mainWindow, {
|
||||
type: 'warning',
|
||||
title: streamingClosePrompt.title,
|
||||
message: streamingClosePrompt.message,
|
||||
buttons: [streamingClosePrompt.cancelLabel, streamingClosePrompt.confirmLabel],
|
||||
defaultId: 0,
|
||||
cancelId: 0,
|
||||
noLink: true,
|
||||
});
|
||||
|
||||
if (choice === 0) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
isStreamingActive = false;
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
@@ -96,6 +204,10 @@ function createWindow(): void {
|
||||
mainWindow.on('page-title-updated', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
setupAutoUpdater();
|
||||
});
|
||||
}
|
||||
|
||||
function setupDisplayMediaHandler(): void {
|
||||
@@ -152,7 +264,7 @@ ipcMain.handle('helium:get-platform', () => {
|
||||
isWindows,
|
||||
isWayland,
|
||||
isElectron: true,
|
||||
supportsLoopbackAudio: isWindows || isMac,
|
||||
supportsLoopbackAudio: isWindows || isMac || isLinux,
|
||||
supportsVenmic: isLinux && (venmicManager?.isAvailable() ?? false),
|
||||
};
|
||||
});
|
||||
@@ -223,6 +335,39 @@ ipcMain.handle('helium:open-screen-permission-settings', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
'helium:set-streaming-active',
|
||||
(_event: IpcMainInvokeEvent, active: boolean, promptOptions?: typeof streamingClosePrompt) => {
|
||||
isStreamingActive = active;
|
||||
|
||||
if (promptOptions) {
|
||||
streamingClosePrompt = promptOptions;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle('helium:check-for-updates', async () => {
|
||||
if (isDev || !app.isPackaged) return false;
|
||||
|
||||
try {
|
||||
await getAutoUpdater().checkForUpdatesAndNotify();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Helium] Manual update check failed:', error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('helium:install-update', () => {
|
||||
if (isDev || !app.isPackaged) return false;
|
||||
|
||||
isStreamingActive = false;
|
||||
getAutoUpdater().quitAndInstall(false, true);
|
||||
return true;
|
||||
});
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
if (!gotTheLock) {
|
||||
|
||||
@@ -35,6 +35,28 @@ export interface VenmicLinkOptions {
|
||||
only_default_speakers?: boolean;
|
||||
}
|
||||
|
||||
export interface StreamingClosePromptOptions {
|
||||
title: string;
|
||||
message: string;
|
||||
confirmLabel: string;
|
||||
cancelLabel: string;
|
||||
}
|
||||
|
||||
export type UpdateStatus =
|
||||
| 'checking'
|
||||
| 'available'
|
||||
| 'not-available'
|
||||
| 'download-progress'
|
||||
| 'downloaded'
|
||||
| 'error';
|
||||
|
||||
export interface UpdateStatusPayload {
|
||||
status: UpdateStatus;
|
||||
version?: string;
|
||||
percent?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const heliumElectronAPI = {
|
||||
isElectron: true as const,
|
||||
getPlatform: (): Promise<PlatformInfo> => ipcRenderer.invoke('helium:get-platform'),
|
||||
@@ -59,6 +81,20 @@ const heliumElectronAPI = {
|
||||
checkScreenPermission: (): Promise<string> => ipcRenderer.invoke('helium:check-screen-permission'),
|
||||
openScreenPermissionSettings: (): Promise<boolean> =>
|
||||
ipcRenderer.invoke('helium:open-screen-permission-settings'),
|
||||
setStreamingActive: (
|
||||
active: boolean,
|
||||
promptOptions?: StreamingClosePromptOptions,
|
||||
): Promise<boolean> => ipcRenderer.invoke('helium:set-streaming-active', active, promptOptions),
|
||||
checkForUpdates: (): Promise<boolean> => ipcRenderer.invoke('helium:check-for-updates'),
|
||||
installUpdate: (): Promise<boolean> => ipcRenderer.invoke('helium:install-update'),
|
||||
onUpdateStatus: (callback: (payload: UpdateStatusPayload) => void): (() => void) => {
|
||||
const listener = (_event: IpcRendererEvent, payload: UpdateStatusPayload): void => {
|
||||
callback(payload);
|
||||
};
|
||||
|
||||
ipcRenderer.on('helium:update-status', listener);
|
||||
return () => ipcRenderer.removeListener('helium:update-status', listener);
|
||||
},
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('heliumElectron', heliumElectronAPI);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"welcome": "Welcome",
|
||||
"language": "Language",
|
||||
"selectLanguage": "Select language",
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
"contact": "Contact",
|
||||
"downloads": "Downloads",
|
||||
"stream": "Stream",
|
||||
"streamControlDescription": "Choose a source, tune your connection preset, then share the room code.",
|
||||
"shareCode": "Stream code",
|
||||
"copyCode": "Copy code",
|
||||
"codeCopied": "Code copied to clipboard",
|
||||
"live": "Live",
|
||||
"previewWaiting": "Preview waiting",
|
||||
"previewWaitingDescription": "Your selected screen will appear here once sharing starts.",
|
||||
"presets": "Presets",
|
||||
"effortlessScreensharing": "effortless screensharing powered by webrtc",
|
||||
"hostInstead": "stream instead?",
|
||||
@@ -18,6 +22,7 @@
|
||||
"screenshare": "Screenshare",
|
||||
"loadingPresets": "Loading presets...",
|
||||
"selectAPreset": "Select a preset",
|
||||
"selectPresetBeforeStreaming": "Select a preset before starting a stream.",
|
||||
"noPresetsAvailable": "No presets available",
|
||||
"default": "default",
|
||||
"createNewPreset": "Create New Preset",
|
||||
@@ -56,11 +61,15 @@
|
||||
"presetSharingDisabled": "The preset owner has not enabled sharing for this preset.",
|
||||
"goBack": "Go Back",
|
||||
"changeSource": "Change Source",
|
||||
"stopStream": "Stop Stream",
|
||||
"activeStreamCloseTitle": "Active stream",
|
||||
"activeStreamCloseMessage": "A stream is still active. Closing Helium will stop it for all viewers.",
|
||||
"stopStreamAndClose": "Stop stream and close",
|
||||
"keepStreaming": "Keep streaming",
|
||||
"includeAudio": "Include Audio",
|
||||
"audioSource": "Audio Source",
|
||||
"allSystemAudio": "All System Audio",
|
||||
"refreshSources": "Refresh Sources",
|
||||
"audioSupported": "Audio Supported",
|
||||
"failedToStartScreenShare": "Failed to start screen share.",
|
||||
"screenRecordingPermissionRequired": "macOS blocked screen capture. Allow Helium in System Settings > Privacy & Security > Screen Recording, then restart Helium.",
|
||||
"screenRecordingPermissionRequiredNoShortcut": "macOS blocked screen capture. Open System Settings > Privacy & Security > Screen Recording, allow Helium, then restart Helium.",
|
||||
@@ -70,11 +79,19 @@
|
||||
"desktopAppDescription": "The full-featured Electron app with system audio support.",
|
||||
"androidApp": "Android App",
|
||||
"androidAppDescription": "Stream directly from your Android device.",
|
||||
"desktopAppNote": "Includes advanced features like system audio capture, venmic support on Linux, and native screen recording permissions.",
|
||||
"androidAppNote": "Install the APK directly on your Android device. Make sure to allow installation from unknown sources if prompted.",
|
||||
"desktopAppNote": "Includes advanced features like system audio capture and native screen recording permissions.",
|
||||
"androidAppNote": "Install the APK directly on your Android device.",
|
||||
"downloadFromGitHub": "Download from GitHub",
|
||||
"viewSourceCode": "View Source Code",
|
||||
"preferTheBrowser": "Prefer the browser?",
|
||||
"browserVersionDescription": "The web version works great too. No installation required — just open the page and start streaming or watching.",
|
||||
"useWebVersion": "Use Web Version"
|
||||
"updateAvailable": "Update available",
|
||||
"updateAvailableDescription": "Helium {version} is downloading in the background.",
|
||||
"updateAvailableDescriptionWithoutVersion": "A new Helium update is downloading in the background.",
|
||||
"updateDownloading": "Downloading update",
|
||||
"updateDownloadProgress": "{percent}% downloaded",
|
||||
"updateReady": "Update ready",
|
||||
"updateReadyDescription": "Helium {version} is ready. Restart to install it now.",
|
||||
"updateReadyDescriptionWithoutVersion": "A Helium update is ready. Restart to install it now.",
|
||||
"restartToUpdate": "Restart",
|
||||
"updateFailed": "Update failed",
|
||||
"updateFailedDescription": "Helium could not check for or download the update."
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"welcome": "Bienvenido",
|
||||
"language": "Idioma",
|
||||
"selectLanguage": "Seleccionar idioma",
|
||||
"home": "Inicio",
|
||||
"about": "Acerca de",
|
||||
"contact": "Contacto",
|
||||
"downloads": "Descargas",
|
||||
"stream": "Transmisión",
|
||||
"streamControlDescription": "Elige una fuente, ajusta el preset de conexión y comparte el código de la sala.",
|
||||
"shareCode": "Código de la emisión",
|
||||
"copyCode": "Copiar código",
|
||||
"codeCopied": "Código copiado al portapapeles",
|
||||
"live": "En vivo",
|
||||
"previewWaiting": "Vista previa en espera",
|
||||
"previewWaitingDescription": "La pantalla seleccionada aparecerá aquí cuando empieces a compartir.",
|
||||
"presets": "Ajustes predefinidos",
|
||||
"effortlessScreensharing": "comparte pantalla sin complicaciones",
|
||||
"hostInstead": "¿prefieres transmitir pantalla?",
|
||||
@@ -18,6 +22,7 @@
|
||||
"screenshare": "Compartir pantalla",
|
||||
"loadingPresets": "Cargando ajustes...",
|
||||
"selectAPreset": "Seleccionar un ajuste",
|
||||
"selectPresetBeforeStreaming": "Selecciona un ajuste antes de iniciar una transmisión.",
|
||||
"noPresetsAvailable": "No hay ajustes disponibles",
|
||||
"default": "predeterminado",
|
||||
"createNewPreset": "Crear nuevo ajuste",
|
||||
@@ -56,11 +61,15 @@
|
||||
"presetSharingDisabled": "El propietario del ajuste no ha habilitado el uso compartido para este ajuste.",
|
||||
"goBack": "Volver",
|
||||
"changeSource": "Cambiar fuente",
|
||||
"stopStream": "Detener transmisión",
|
||||
"activeStreamCloseTitle": "Transmisión activa",
|
||||
"activeStreamCloseMessage": "Hay una transmisión activa. Cerrar Helium la detendrá para todos los espectadores.",
|
||||
"stopStreamAndClose": "Detener transmisión y cerrar",
|
||||
"keepStreaming": "Seguir transmitiendo",
|
||||
"includeAudio": "Incluir audio",
|
||||
"audioSource": "Fuente de audio",
|
||||
"allSystemAudio": "Todo el audio del sistema",
|
||||
"refreshSources": "Actualizar fuentes",
|
||||
"audioSupported": "Audio soportado",
|
||||
"failedToStartScreenShare": "No se pudo iniciar el uso compartido de pantalla.",
|
||||
"screenRecordingPermissionRequired": "macOS bloqueó la captura de pantalla. Permite Helium en Configuración del Sistema > Privacidad y seguridad > Grabación de pantalla y luego reinicia Helium.",
|
||||
"screenRecordingPermissionRequiredNoShortcut": "macOS bloqueó la captura de pantalla. Abre Configuración del Sistema > Privacidad y seguridad > Grabación de pantalla, permite Helium y luego reinicia Helium.",
|
||||
@@ -70,11 +79,19 @@
|
||||
"desktopAppDescription": "La aplicación Electron completa con soporte de audio del sistema.",
|
||||
"androidApp": "Aplicación Android",
|
||||
"androidAppDescription": "Transmite directamente desde tu dispositivo Android.",
|
||||
"desktopAppNote": "Incluye funciones avanzadas como captura de audio del sistema, soporte de venmic en Linux y permisos de grabación de pantalla nativos.",
|
||||
"androidAppNote": "Instala el APK directamente en tu dispositivo Android. Asegúrate de permitir la instalación desde fuentes desconocidas si se te solicita.",
|
||||
"desktopAppNote": "Incluye funciones avanzadas como captura de audio del sistema y permisos de grabación de pantalla nativos.",
|
||||
"androidAppNote": "Instala el APK directamente en tu dispositivo Android.",
|
||||
"downloadFromGitHub": "Descargar desde GitHub",
|
||||
"viewSourceCode": "Ver Código Fuente",
|
||||
"preferTheBrowser": "¿Prefieres el navegador?",
|
||||
"browserVersionDescription": "La versión web también funciona muy bien. No requiere instalación: simplemente abre la página y empieza a transmitir o ver.",
|
||||
"useWebVersion": "Usar Versión Web"
|
||||
"updateAvailable": "Actualización disponible",
|
||||
"updateAvailableDescription": "Helium {version} se está descargando en segundo plano.",
|
||||
"updateAvailableDescriptionWithoutVersion": "Una nueva actualización de Helium se está descargando en segundo plano.",
|
||||
"updateDownloading": "Descargando actualización",
|
||||
"updateDownloadProgress": "{percent}% descargado",
|
||||
"updateReady": "Actualización lista",
|
||||
"updateReadyDescription": "Helium {version} está listo. Reinicia para instalarlo ahora.",
|
||||
"updateReadyDescriptionWithoutVersion": "Una actualización de Helium está lista. Reinicia para instalarla ahora.",
|
||||
"restartToUpdate": "Reiniciar",
|
||||
"updateFailed": "Error al actualizar",
|
||||
"updateFailedDescription": "Helium no pudo buscar o descargar la actualización."
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "helium",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.2",
|
||||
"author": {
|
||||
"email": "helium@srizan.dev",
|
||||
"name": "eth0 software",
|
||||
@@ -8,6 +8,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"main": "./electron/dist/main/index.js",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
@@ -45,6 +46,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"electron-updater": "^6.8.9",
|
||||
"lucide-vue-next": "^0.548.0",
|
||||
"marked": "^17.0.1",
|
||||
"monaco-editor": "^0.55.1",
|
||||
|
||||
108
pnpm-lock.yaml
generated
108
pnpm-lock.yaml
generated
@@ -44,6 +44,9 @@ importers:
|
||||
drizzle-orm:
|
||||
specifier: ^0.45.1
|
||||
version: 0.45.1(@neondatabase/serverless@1.0.2)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3)
|
||||
electron-updater:
|
||||
specifier: ^6.8.9
|
||||
version: 6.8.9
|
||||
lucide-vue-next:
|
||||
specifier: ^0.548.0
|
||||
version: 0.548.0(vue@3.5.26(typescript@5.9.3))
|
||||
@@ -2028,36 +2031,42 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-minify/binding-linux-arm64-musl@0.102.0':
|
||||
resolution: {integrity: sha512-DyH/t/zSZHuX4Nn239oBteeMC4OP7B13EyXWX18Qg8aJoZ+lZo90WPGOvhP04zII33jJ7di+vrtAUhsX64lp+A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-minify/binding-linux-riscv64-gnu@0.102.0':
|
||||
resolution: {integrity: sha512-CMvzrmOg+Gs44E7TRK/IgrHYp+wwVJxVV8niUrDR2b3SsrCO3NQz5LI+7bM1qDbWnuu5Cl1aiitoMfjRY61dSg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-minify/binding-linux-s390x-gnu@0.102.0':
|
||||
resolution: {integrity: sha512-tZWr6j2s0ddm9MTfWTI3myaAArg9GDy4UgvpF00kMQAjLcGUNhEEQbB9Bd9KtCvDQzaan8HQs0GVWUp+DWrymw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-minify/binding-linux-x64-gnu@0.102.0':
|
||||
resolution: {integrity: sha512-0YEKmAIun1bS+Iy5Shx6WOTSj3GuilVuctJjc5/vP8/EMTZ/RI8j0eq0Mu3UFPoT/bMULL3MBXuHuEIXmq7Ddg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-minify/binding-linux-x64-musl@0.102.0':
|
||||
resolution: {integrity: sha512-Ew4QDpEsXoV+pG5+bJpheEy3GH436GBe6ASPB0X27Hh9cQ2gb1NVZ7cY7xJj68+fizwS/PtT8GHoG3uxyH17Pg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-minify/binding-openharmony-arm64@0.102.0':
|
||||
resolution: {integrity: sha512-wYPXS8IOu/sXiP3CGHJNPzZo4hfPAwJKevcFH2syvU2zyqUxym7hx6smfcK/mgJBiX7VchwArdGRwrEQKcBSaQ==}
|
||||
@@ -2189,108 +2198,126 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-gnu@0.93.0':
|
||||
resolution: {integrity: sha512-NoB7BJmwVGrcS/J5XXn362lBsIyeTqZF70rCFij3/XwQ2kcELfGMALY9AUulFYauLTY2AG4vcmctJQxn9Lj85g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-gnu@0.95.0':
|
||||
resolution: {integrity: sha512-0LzebARTU0ROfD6pDK4h1pFn+09meErCZ0MA2TaW08G72+GNneEsksPufOuI+9AxVSRa+jKE3fu0wavvhZgSkg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-musl@0.102.0':
|
||||
resolution: {integrity: sha512-/XWcmglH/VJ4yKAGTLRgPKSSikh3xciNxkwGiURt8dS30b+3pwc4ZZmudMu0tQ3mjSu0o7V9APZLMpbHK8Bp5w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-musl@0.93.0':
|
||||
resolution: {integrity: sha512-s+nraJJR9SuHsgsr42nbOBpAsaSAE6MhK7HGbz01svLJzDsk3Ylh9cbVUPLaS3gOlTq5WC6VjPBkQuInLo0hvQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-musl@0.95.0':
|
||||
resolution: {integrity: sha512-Pvi1lGe/G+mJZ3hUojMP/aAHAzHA25AEtVr8/iuz7UV72t/15NOgJYr9kELMUMNjPqpr3vKUgXTFmTtAxp11Qw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/binding-linux-riscv64-gnu@0.102.0':
|
||||
resolution: {integrity: sha512-2jtIq4nswvy6xdqv1ndWyvVlaRpS0yqomLCvvHdCFx3pFXo5Aoq4RZ39kgvFWrbAtpeYSYeAGFnwgnqjx9ftdw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-riscv64-gnu@0.93.0':
|
||||
resolution: {integrity: sha512-oNIQb/7HGxVNeVgtkoqNcDS1hjfxArLDuMI72V+Slp67yfBdxgvfmM2JSWE7kGR5gyiZQeTjRbG89VrRwPDtww==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-riscv64-gnu@0.95.0':
|
||||
resolution: {integrity: sha512-pUEVHIOVNDfhk4sTlLhn6mrNENhE4/dAwemxIfqpcSyBlYG0xYZND1F3jjR2yWY6DakXZ6VSuDbtiv1LPNlOLw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-s390x-gnu@0.102.0':
|
||||
resolution: {integrity: sha512-Yp6HX/574mvYryiqj0jNvNTJqo4pdAsNP2LPBTxlDQ1cU3lPd7DUA4MQZadaeLI8+AGB2Pn50mPuPyEwFIxeFg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-s390x-gnu@0.93.0':
|
||||
resolution: {integrity: sha512-YyzhzAoq5WpRtAGOngpJUu+4jKagSbknORejmpeW48vu8/+XjrVZFc/1Qe4i72EsPzLorDwCxWVkU8VftpM4iA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-s390x-gnu@0.95.0':
|
||||
resolution: {integrity: sha512-5+olaepHTE3J/+w7g0tr3nocvv5BKilAJnzj4L8tWBCLEZbL6olJcGVoldUO+3cgg1SO1xJywP5BuLhT0mDUDw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-gnu@0.102.0':
|
||||
resolution: {integrity: sha512-R4b0xZpDRhoNB2XZy0kLTSYm0ZmWeKjTii9fcv1Mk3/SIGPrrglwt4U6zEtwK54Dfi4Bve5JnQYduigR/gyDzw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-gnu@0.93.0':
|
||||
resolution: {integrity: sha512-UMXsE6c0MIlvtqDe5t5K8qwC6HqNb3wmy8zKxONo42dIx0WAhVV9ydG2Xlznt1/RhD6nLLtHVaq4yWJXRjUxcg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-gnu@0.95.0':
|
||||
resolution: {integrity: sha512-8huzHlK/N98wrnYKxIcYsK8ZGBWomQchu/Mzi6m+CtbhjWOv9DmK0jQ2fUWImtluQVpTwS0uZT06d3g7XIkJrA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-musl@0.102.0':
|
||||
resolution: {integrity: sha512-xM5A+03Ti3jvWYZoqaBRS3lusvnvIQjA46Fc9aBE/MHgvKgHSkrGEluLWg/33QEwBwxupkH25Pxc1yu97oZCtg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-musl@0.93.0':
|
||||
resolution: {integrity: sha512-0Vd0yFUq129VW+Cpcj/gJOqub4EMN5hUWnVk8UfAvUZ+lxZBFeXbYNI5483SLwzvw5umzlMmkKpYWw5OTwYFaA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-musl@0.95.0':
|
||||
resolution: {integrity: sha512-bWnrLfGDcx/fab0+UQnFbVFbiykof/btImbYf+cI2pU/1Egb2x+OKSmM5Qt0nEUiIpM5fgJmYXxTopybSZOKYA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/binding-openharmony-arm64@0.102.0':
|
||||
resolution: {integrity: sha512-AieLlsliblyaTFq7Iw9Nc618tgwV02JT4fQ6VIUd/3ZzbluHIHfPjIXa6Sds+04krw5TvCS8lsegtDYAyzcyhg==}
|
||||
@@ -2429,72 +2456,84 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-transform/binding-linux-arm64-gnu@0.95.0':
|
||||
resolution: {integrity: sha512-NLdrFuEHlmbiC1M1WESFV4luUcB/84GXi+cbnRXhgMjIW/CThRVJ989eTJy59QivkVlLcJSKTiKiKCt0O6TTlQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-transform/binding-linux-arm64-musl@0.102.0':
|
||||
resolution: {integrity: sha512-I08iWABrN7zakn3wuNIBWY3hALQGsDLPQbZT1mXws7tyiQqJNGe49uS0/O50QhX3KXj+mbRGsmjVXLXGJE1CVQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-transform/binding-linux-arm64-musl@0.95.0':
|
||||
resolution: {integrity: sha512-GL0ffCPW8JlFI0/jeSgCY665yDdojHxA0pbYG+k8oEHOWCYZUZK9AXL+r0oerNEWYJ8CRB+L5Yq87ZtU/YUitw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-transform/binding-linux-riscv64-gnu@0.102.0':
|
||||
resolution: {integrity: sha512-9+SYW1ARAF6Oj/82ayoqKRe8SI7O1qvzs3Y0kijvhIqAaaZWcFRjI5DToyWRAbnzTtHlMcSllZLXNYdmxBjFxA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-transform/binding-linux-riscv64-gnu@0.95.0':
|
||||
resolution: {integrity: sha512-tbH7LaClSmN3YFVo1UjMSe7D6gkb5f+CMIbj9i873UUZomVRmAjC4ygioObfzM+sj/tX0WoTXx5L1YOfQkHL6Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-transform/binding-linux-s390x-gnu@0.102.0':
|
||||
resolution: {integrity: sha512-HV9nTyQw0TTKYPu+gBhaJBioomiM9O4LcGXi+s5IylCGG6imP0/U13q/9xJnP267QFmiWWqnnSFcv0QAWCyh8A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-transform/binding-linux-s390x-gnu@0.95.0':
|
||||
resolution: {integrity: sha512-8jMqiURWa0iTiPMg7BWaln89VdhhWzNlPyKM90NaFVVhBIKCr2UEhrQWdpBw/E9C8uWf/4VabBEhfPMK+0yS4w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-transform/binding-linux-x64-gnu@0.102.0':
|
||||
resolution: {integrity: sha512-4wcZ08mmdFk8OjsnglyeYGu5PW3TDh87AmcMOi7tZJ3cpJjfzwDfY27KTEUx6G880OpjAiF36OFSPwdKTKgp2g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-transform/binding-linux-x64-gnu@0.95.0':
|
||||
resolution: {integrity: sha512-D5ULJ2uWipsTgfvHIvqmnGkCtB3Fyt2ZN7APRjVO+wLr+HtmnaWddKsLdrRWX/m/6nQ2xQdoQekdJrokYK9LtQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-transform/binding-linux-x64-musl@0.102.0':
|
||||
resolution: {integrity: sha512-rUHZSZBw0FUnUgOhL/Rs7xJz9KjH2eFur/0df6Lwq/isgJc/ggtBtFoZ+y4Fb8ON87a3Y2gS2LT7SEctX0XdPQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-transform/binding-linux-x64-musl@0.95.0':
|
||||
resolution: {integrity: sha512-DmCGU+FzRezES5wVAGVimZGzYIjMOapXbWpxuz8M8p3nMrfdBEQ5/tpwBp2vRlIohhABy4vhHJByl4c64ENCGQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-transform/binding-openharmony-arm64@0.102.0':
|
||||
resolution: {integrity: sha512-98y4tccTQ/pA+r2KA/MEJIZ7J8TNTJ4aCT4rX8kWK4pGOko2YsfY3Ru9DVHlLDwmVj7wP8Z4JNxdBrAXRvK+0g==}
|
||||
@@ -2565,36 +2604,42 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-wasm@2.5.1':
|
||||
resolution: {integrity: sha512-RJxlQQLkaMMIuWRozy+z2vEqbaQlCuaCgVZIUCzQLYggY22LZbP5Y1+ia+FD724Ids9e+XIyOLXLrLgQSHIthw==}
|
||||
@@ -2866,56 +2911,67 @@ packages:
|
||||
resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.52.5':
|
||||
resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.52.5':
|
||||
resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.52.5':
|
||||
resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.52.5':
|
||||
resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.52.5':
|
||||
resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==}
|
||||
@@ -3154,24 +3210,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.16':
|
||||
resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.16':
|
||||
resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.16':
|
||||
resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.16':
|
||||
resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==}
|
||||
@@ -4032,6 +4092,10 @@ packages:
|
||||
resolution: {integrity: sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
builder-util-runtime@9.7.0:
|
||||
resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
builder-util@25.1.7:
|
||||
resolution: {integrity: sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==}
|
||||
|
||||
@@ -4802,6 +4866,9 @@ packages:
|
||||
electron-to-chromium@1.5.267:
|
||||
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
||||
|
||||
electron-updater@6.8.9:
|
||||
resolution: {integrity: sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==}
|
||||
|
||||
electron@33.4.11:
|
||||
resolution: {integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==}
|
||||
engines: {node: '>= 12.20.55'}
|
||||
@@ -5973,24 +6040,28 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||
@@ -6040,12 +6111,19 @@ packages:
|
||||
lodash.difference@4.5.0:
|
||||
resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==}
|
||||
|
||||
lodash.escaperegexp@4.1.2:
|
||||
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
|
||||
|
||||
lodash.flatten@4.4.0:
|
||||
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
|
||||
|
||||
lodash.isarguments@3.1.0:
|
||||
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
||||
|
||||
lodash.isequal@4.5.0:
|
||||
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
|
||||
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
|
||||
|
||||
lodash.isplainobject@4.0.6:
|
||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||
|
||||
@@ -7871,6 +7949,9 @@ packages:
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
tiny-typed-emitter@2.1.0:
|
||||
resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==}
|
||||
|
||||
tinyexec@1.0.1:
|
||||
resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
|
||||
|
||||
@@ -8205,6 +8286,7 @@ packages:
|
||||
|
||||
uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
validate-npm-package-name@5.0.1:
|
||||
@@ -13275,6 +13357,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
builder-util-runtime@9.7.0:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
sax: 1.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
builder-util@25.1.7:
|
||||
dependencies:
|
||||
7zip-bin: 5.2.0
|
||||
@@ -14057,6 +14146,19 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.267: {}
|
||||
|
||||
electron-updater@6.8.9:
|
||||
dependencies:
|
||||
builder-util-runtime: 9.7.0
|
||||
fs-extra: 10.1.0
|
||||
js-yaml: 4.1.1
|
||||
lazy-val: 1.0.5
|
||||
lodash.escaperegexp: 4.1.2
|
||||
lodash.isequal: 4.5.0
|
||||
semver: 7.7.3
|
||||
tiny-typed-emitter: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron@33.4.11:
|
||||
dependencies:
|
||||
'@electron/get': 2.0.3
|
||||
@@ -15460,10 +15562,14 @@ snapshots:
|
||||
|
||||
lodash.difference@4.5.0: {}
|
||||
|
||||
lodash.escaperegexp@4.1.2: {}
|
||||
|
||||
lodash.flatten@4.4.0: {}
|
||||
|
||||
lodash.isarguments@3.1.0: {}
|
||||
|
||||
lodash.isequal@4.5.0: {}
|
||||
|
||||
lodash.isplainobject@4.0.6: {}
|
||||
|
||||
lodash.memoize@4.1.2: {}
|
||||
@@ -17770,6 +17876,8 @@ snapshots:
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tiny-typed-emitter@2.1.0: {}
|
||||
|
||||
tinyexec@1.0.1: {}
|
||||
|
||||
tinyexec@1.0.2: {}
|
||||
|
||||
@@ -54,6 +54,20 @@ async function deleteRoom(roomId: string) {
|
||||
await db.delete(schema.rooms).where(eq(schema.rooms.id, roomId));
|
||||
}
|
||||
|
||||
async function closeRoom(roomId: string, broadcasterId: string) {
|
||||
const room = await getRoom(roomId);
|
||||
if (!room || room.broadcaster !== broadcasterId) return;
|
||||
|
||||
room.viewers.forEach((viewerId: string) => {
|
||||
const viewer = activePeers.get(viewerId);
|
||||
if (viewer) {
|
||||
viewer.send(JSON.stringify({ event: 'room-closed' }));
|
||||
}
|
||||
});
|
||||
|
||||
await deleteRoom(roomId);
|
||||
}
|
||||
|
||||
async function addViewerToRoom(roomId: string, viewerId: string) {
|
||||
await db.insert(schema.roomViewers).values({
|
||||
roomId,
|
||||
@@ -104,6 +118,9 @@ export default defineWebSocketHandler({
|
||||
await createRoom(roomId, peer.id);
|
||||
peer.send(JSON.stringify({ event: 'room-created', roomId }));
|
||||
}
|
||||
if (msg.event === 'close-room') {
|
||||
await closeRoom(msg.roomId, peer.id);
|
||||
}
|
||||
if (msg.event === 'join-room') {
|
||||
const room = await getRoom(msg.roomId);
|
||||
if (room) {
|
||||
|
||||
Reference in New Issue
Block a user