mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(desktop): harden capture and release packaging (#2049)
This commit is contained in:
1 parent
5ee59c4675
commit
374db9ed2b
25 files changed
+1394
-1317
No files matched your search
@@ -8,12 +8,11 @@ import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import {buildAppProtocolUrl} from '@app/features/ui/utils/AppProtocol';
|
||||
import {isDesktop, openExternalUrl} from '@app/features/ui/utils/NativeUtils';
|
||||
import {checkDesktopAvailable, navigateInDesktop} from '@app/features/voice/utils/DesktopRpcClient';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {ArrowSquareOutIcon} from '@phosphor-icons/react';
|
||||
import type React from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useState} from 'react';
|
||||
|
||||
interface DesktopDeepLinkPromptProps {
|
||||
code: string;
|
||||
@@ -36,25 +35,9 @@ const FAILED_TO_OPEN_IN_DESKTOP_APP_DESCRIPTOR = msg({
|
||||
export const DesktopDeepLinkPrompt: React.FC<DesktopDeepLinkPromptProps> = ({code, kind, preferLogin = false}) => {
|
||||
const {i18n} = useLingui();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [desktopAvailable, setDesktopAvailable] = useState<boolean | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const isMobileBrowser = Platform.isMobileBrowser;
|
||||
const useProtocolLaunch = kind === 'invite';
|
||||
const shouldProbeDesktopAvailability = !useProtocolLaunch;
|
||||
useEffect(() => {
|
||||
if (isDesktop() || !shouldProbeDesktopAvailability) return;
|
||||
let cancelled = false;
|
||||
checkDesktopAvailable().then(({available}) => {
|
||||
if (!cancelled) {
|
||||
setDesktopAvailable(available);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [shouldProbeDesktopAvailability]);
|
||||
if (isDesktop() || isMobileBrowser) return null;
|
||||
if (shouldProbeDesktopAvailability && desktopAvailable !== true) return null;
|
||||
const getPath = (): string => {
|
||||
switch (kind) {
|
||||
case 'invite':
|
||||
@@ -69,20 +52,12 @@ export const DesktopDeepLinkPrompt: React.FC<DesktopDeepLinkPromptProps> = ({cod
|
||||
const handleOpen = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
if (useProtocolLaunch) {
|
||||
try {
|
||||
await openExternalUrl(buildAppProtocolUrl(path));
|
||||
} catch {
|
||||
setError(i18n._(FAILED_TO_OPEN_IN_DESKTOP_APP_DESCRIPTOR));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const result = await navigateInDesktop(path);
|
||||
setIsLoading(false);
|
||||
if (!result.success) {
|
||||
try {
|
||||
await openExternalUrl(buildAppProtocolUrl(path));
|
||||
} catch {
|
||||
setError(i18n._(FAILED_TO_OPEN_IN_DESKTOP_APP_DESCRIPTOR));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -180,10 +180,6 @@ export function handleDeepLinkUrl(rawUrl: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function handleRpcNavigation(path: string): void {
|
||||
RouterUtils.transitionTo(path);
|
||||
}
|
||||
|
||||
let listenerStarted = false;
|
||||
|
||||
export async function startDeepLinkHandling(): Promise<void> {
|
||||
@@ -206,17 +202,6 @@ export async function startDeepLinkHandling(): Promise<void> {
|
||||
logger.error(' Failed to handle URL', url, error);
|
||||
}
|
||||
});
|
||||
if (typeof electronApi.onRpcNavigate === 'function') {
|
||||
electronApi.onRpcNavigate((path: string) => {
|
||||
try {
|
||||
handleRpcNavigation(path);
|
||||
} catch (error) {
|
||||
logger.error(' Failed to handle RPC navigation', path, error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.warn(' onRpcNavigate not available on this host version');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +344,6 @@ export interface ElectronAPI {
|
||||
pasteFromClipboard: () => Promise<void>;
|
||||
onDeepLink: (callback: (url: string) => void) => () => void;
|
||||
getInitialDeepLink: () => Promise<string | null>;
|
||||
onRpcNavigate: (callback: (path: string) => void) => () => void;
|
||||
autostartEnable: () => Promise<void>;
|
||||
autostartDisable: () => Promise<void>;
|
||||
autostartIsEnabled: () => Promise<boolean>;
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import Config from '@app/features/app/config/Config';
|
||||
|
||||
const RPC_PORT_STABLE = 21863;
|
||||
const RPC_PORT_CANARY = 21864;
|
||||
const RPC_PORTS =
|
||||
Config.PUBLIC_RELEASE_CHANNEL === 'canary' ? [RPC_PORT_CANARY, RPC_PORT_STABLE] : [RPC_PORT_STABLE, RPC_PORT_CANARY];
|
||||
|
||||
interface RpcResponse<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface HealthResponse {
|
||||
status: string;
|
||||
channel: string;
|
||||
version: string;
|
||||
platform: string;
|
||||
}
|
||||
|
||||
interface NavigateResponse {
|
||||
navigated: boolean;
|
||||
path: string;
|
||||
}
|
||||
|
||||
let cachedAvailablePort: number | null = null;
|
||||
let lastHealthCheck = 0;
|
||||
|
||||
const HEALTH_CHECK_CACHE_MS = 5000;
|
||||
|
||||
async function rpcFetch<T>(port: number, endpoint: string, options?: RequestInit): Promise<RpcResponse<T> | null> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 2000);
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}${endpoint}`, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
});
|
||||
return (await response.json()) as RpcResponse<T>;
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkDesktopAvailable(): Promise<{
|
||||
available: boolean;
|
||||
port: number | null;
|
||||
info: HealthResponse | null;
|
||||
}> {
|
||||
const now = Date.now();
|
||||
if (cachedAvailablePort !== null && now - lastHealthCheck < HEALTH_CHECK_CACHE_MS) {
|
||||
const result = await rpcFetch<HealthResponse>(cachedAvailablePort, '/health');
|
||||
if (result?.success && result.data) {
|
||||
return {available: true, port: cachedAvailablePort, info: result.data};
|
||||
}
|
||||
cachedAvailablePort = null;
|
||||
}
|
||||
for (const port of RPC_PORTS) {
|
||||
const result = await rpcFetch<HealthResponse>(port, '/health');
|
||||
if (result?.success && result.data) {
|
||||
cachedAvailablePort = port;
|
||||
lastHealthCheck = now;
|
||||
return {available: true, port, info: result.data};
|
||||
}
|
||||
}
|
||||
cachedAvailablePort = null;
|
||||
return {available: false, port: null, info: null};
|
||||
}
|
||||
|
||||
export async function navigateInDesktop(path: string): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
const {available, port} = await checkDesktopAvailable();
|
||||
if (!available || port === null) {
|
||||
return {success: false, error: 'desktop_unavailable'};
|
||||
}
|
||||
const result = await rpcFetch<NavigateResponse>(port, '/navigate', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({method: 'navigate', params: {path}}),
|
||||
});
|
||||
if (!result) {
|
||||
return {success: false, error: 'desktop_rpc_unreachable'};
|
||||
}
|
||||
if (!result.success) {
|
||||
return {success: false, error: result.error ?? 'desktop_rpc_unknown'};
|
||||
}
|
||||
return {success: true};
|
||||
}
|
||||
|
||||
export async function focusDesktop(): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
const {available, port} = await checkDesktopAvailable();
|
||||
if (!available || port === null) {
|
||||
return {success: false, error: 'desktop_unavailable'};
|
||||
}
|
||||
const result = await rpcFetch(port, '/focus', {method: 'POST'});
|
||||
if (!result) {
|
||||
return {success: false, error: 'desktop_rpc_unreachable'};
|
||||
}
|
||||
if (!result.success) {
|
||||
return {success: false, error: result.error ?? 'desktop_rpc_unknown'};
|
||||
}
|
||||
return {success: true};
|
||||
}
|
||||
|
||||
export function resetDesktopRpcCache(): void {
|
||||
cachedAvailablePort = null;
|
||||
lastHealthCheck = 0;
|
||||
}
|
||||
Vendored
-1
@@ -483,7 +483,6 @@ export interface ElectronAPI {
|
||||
passkeyIsSupported?(): Promise<boolean>;
|
||||
passkeyRegister?(options: unknown, requestContext?: {pin?: string}): Promise<RegistrationResponseJSON>;
|
||||
passkeyAuthenticate?(options: unknown, requestContext?: {pin?: string}): Promise<AuthenticationResponseJSON>;
|
||||
onRpcNavigate?(callback: (path: string) => void): () => void;
|
||||
getOpenH264Status?(): Promise<OpenH264Status>;
|
||||
setOpenH264Enabled?(enabled: boolean): Promise<OpenH264Status>;
|
||||
virtmic?: VirtmicApi;
|
||||
|
||||
Reference in new issue
Block a user