Polish desktop settings update/local-daemon sections
This commit is contained in:
@@ -1,114 +1,53 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Alert, Text, View } from "react-native";
|
import { Alert, Text, View } from "react-native";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
|
||||||
import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater";
|
|
||||||
import {
|
import {
|
||||||
buildDaemonUpdateDiagnostics,
|
buildDaemonUpdateDiagnostics,
|
||||||
findLikelyLocalDaemonHost,
|
|
||||||
formatVersionWithPrefix,
|
formatVersionWithPrefix,
|
||||||
isLikelyLocalHost,
|
getLocalDaemonVersion,
|
||||||
isVersionMismatch,
|
isVersionMismatch,
|
||||||
runLocalDaemonUpdate,
|
runLocalDaemonUpdate,
|
||||||
shouldShowDesktopUpdateSection,
|
shouldShowDesktopUpdateSection,
|
||||||
} from "@/desktop/updates/desktop-updates";
|
} from "@/desktop/updates/desktop-updates";
|
||||||
|
|
||||||
export interface DesktopUpdatesSectionProps {
|
export interface LocalDaemonSectionProps {
|
||||||
appVersion: string | null;
|
appVersion: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DesktopUpdatesSection({ appVersion }: DesktopUpdatesSectionProps) {
|
export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
|
||||||
const showDesktopUpdatesSection = shouldShowDesktopUpdateSection();
|
const showSection = shouldShowDesktopUpdateSection();
|
||||||
const { daemons } = useDaemonRegistry();
|
const [localDaemonVersion, setLocalDaemonVersion] = useState<string | null>(null);
|
||||||
|
const [localDaemonVersionError, setLocalDaemonVersionError] = useState<string | null>(null);
|
||||||
const [isUpdatingLocalDaemon, setIsUpdatingLocalDaemon] = useState(false);
|
const [isUpdatingLocalDaemon, setIsUpdatingLocalDaemon] = useState(false);
|
||||||
const [localDaemonUpdateMessage, setLocalDaemonUpdateMessage] = useState<string | null>(null);
|
const [localDaemonUpdateMessage, setLocalDaemonUpdateMessage] = useState<string | null>(null);
|
||||||
const [localDaemonUpdateDiagnostics, setLocalDaemonUpdateDiagnostics] = useState<string | null>(
|
const [localDaemonUpdateDiagnostics, setLocalDaemonUpdateDiagnostics] = useState<string | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
const appVersionText = formatVersionWithPrefix(appVersion);
|
|
||||||
const localDaemonHost = useMemo(() => findLikelyLocalDaemonHost(daemons), [daemons]);
|
|
||||||
const hasLikelyLocalDaemon = isLikelyLocalHost(localDaemonHost);
|
|
||||||
const localDaemonServerId = localDaemonHost?.serverId ?? null;
|
|
||||||
const localDaemonVersion = useSessionStore(
|
|
||||||
useCallback(
|
|
||||||
(state) =>
|
|
||||||
localDaemonServerId
|
|
||||||
? (state.sessions[localDaemonServerId]?.serverInfo?.version ?? null)
|
|
||||||
: null,
|
|
||||||
[localDaemonServerId]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const localDaemonVersionText = formatVersionWithPrefix(localDaemonVersion);
|
|
||||||
const daemonVersionMismatch = isVersionMismatch(appVersion, localDaemonVersion);
|
|
||||||
const daemonVersionTitle = hasLikelyLocalDaemon
|
|
||||||
? "Local daemon version"
|
|
||||||
: "Connected daemon version";
|
|
||||||
const daemonHostHint = localDaemonHost
|
|
||||||
? hasLikelyLocalDaemon
|
|
||||||
? `Connected host: ${localDaemonHost.label}`
|
|
||||||
: `No local daemon detected. Showing connected host: ${localDaemonHost.label}.`
|
|
||||||
: "No local daemon detected yet.";
|
|
||||||
|
|
||||||
const {
|
|
||||||
isDesktop: isDesktopUpdaterAvailable,
|
|
||||||
statusText: appUpdateStatusText,
|
|
||||||
availableUpdate,
|
|
||||||
errorMessage: appUpdateError,
|
|
||||||
isChecking: isCheckingAppUpdate,
|
|
||||||
isInstalling: isInstallingAppUpdate,
|
|
||||||
checkForUpdates,
|
|
||||||
installUpdate,
|
|
||||||
} = useDesktopAppUpdater();
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
if (!showDesktopUpdatesSection || !isDesktopUpdaterAvailable) {
|
if (!showSection) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkForUpdates({ silent: true });
|
void getLocalDaemonVersion().then((result) => {
|
||||||
|
setLocalDaemonVersion(result.version);
|
||||||
|
setLocalDaemonVersionError(result.error);
|
||||||
|
});
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [checkForUpdates, isDesktopUpdaterAvailable, showDesktopUpdatesSection])
|
}, [showSection])
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCheckForAppUpdates = useCallback(() => {
|
const localDaemonVersionText = formatVersionWithPrefix(localDaemonVersion);
|
||||||
if (!showDesktopUpdatesSection || !isDesktopUpdaterAvailable) {
|
const daemonVersionMismatch = isVersionMismatch(appVersion, localDaemonVersion);
|
||||||
return;
|
const daemonVersionHint = localDaemonVersionError ?? "Daemon installed on this computer.";
|
||||||
}
|
|
||||||
void checkForUpdates();
|
|
||||||
}, [checkForUpdates, isDesktopUpdaterAvailable, showDesktopUpdatesSection]);
|
|
||||||
|
|
||||||
const handleInstallAppUpdate = useCallback(() => {
|
|
||||||
if (!showDesktopUpdatesSection || !isDesktopUpdaterAvailable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void confirmDialog({
|
|
||||||
title: "Install desktop update",
|
|
||||||
message: "This updates Paseo on this computer.",
|
|
||||||
confirmLabel: "Install update",
|
|
||||||
cancelLabel: "Cancel",
|
|
||||||
})
|
|
||||||
.then((confirmed) => {
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void installUpdate();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("[Settings] Failed to open app update confirmation", error);
|
|
||||||
Alert.alert("Error", "Unable to open the update confirmation dialog.");
|
|
||||||
});
|
|
||||||
}, [installUpdate, isDesktopUpdaterAvailable, showDesktopUpdatesSection]);
|
|
||||||
|
|
||||||
const handleUpdateLocalDaemon = useCallback(() => {
|
const handleUpdateLocalDaemon = useCallback(() => {
|
||||||
if (!showDesktopUpdatesSection || !isDesktopUpdaterAvailable) {
|
if (!showSection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isUpdatingLocalDaemon) {
|
if (isUpdatingLocalDaemon) {
|
||||||
@@ -148,6 +87,11 @@ export function DesktopUpdatesSection({ appVersion }: DesktopUpdatesSectionProps
|
|||||||
if (result.stdout.trim().length > 0 || result.stderr.trim().length > 0) {
|
if (result.stdout.trim().length > 0 || result.stderr.trim().length > 0) {
|
||||||
setLocalDaemonUpdateDiagnostics(diagnostics);
|
setLocalDaemonUpdateDiagnostics(diagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void getLocalDaemonVersion().then((versionResult) => {
|
||||||
|
setLocalDaemonVersion(versionResult.version);
|
||||||
|
setLocalDaemonVersionError(versionResult.error);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("[Settings] Failed to update local daemon", error);
|
console.error("[Settings] Failed to update local daemon", error);
|
||||||
@@ -171,7 +115,7 @@ export function DesktopUpdatesSection({ appVersion }: DesktopUpdatesSectionProps
|
|||||||
console.error("[Settings] Failed to open daemon update confirmation", error);
|
console.error("[Settings] Failed to open daemon update confirmation", error);
|
||||||
Alert.alert("Error", "Unable to open the daemon update confirmation dialog.");
|
Alert.alert("Error", "Unable to open the daemon update confirmation dialog.");
|
||||||
});
|
});
|
||||||
}, [isDesktopUpdaterAvailable, isUpdatingLocalDaemon, showDesktopUpdatesSection]);
|
}, [isUpdatingLocalDaemon, showSection]);
|
||||||
|
|
||||||
const handleCopyDaemonDiagnostics = useCallback(() => {
|
const handleCopyDaemonDiagnostics = useCallback(() => {
|
||||||
if (!localDaemonUpdateDiagnostics) {
|
if (!localDaemonUpdateDiagnostics) {
|
||||||
@@ -188,82 +132,36 @@ export function DesktopUpdatesSection({ appVersion }: DesktopUpdatesSectionProps
|
|||||||
});
|
});
|
||||||
}, [localDaemonUpdateDiagnostics]);
|
}, [localDaemonUpdateDiagnostics]);
|
||||||
|
|
||||||
if (!showDesktopUpdatesSection) {
|
if (!showSection) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>Updates</Text>
|
<Text style={styles.sectionTitle}>Local daemon</Text>
|
||||||
<View style={styles.audioCard}>
|
<View style={styles.card}>
|
||||||
<View style={styles.audioRow}>
|
<View style={styles.row}>
|
||||||
<View style={styles.audioRowContent}>
|
<View style={styles.rowContent}>
|
||||||
<Text style={styles.audioRowTitle}>Desktop app version</Text>
|
<Text style={styles.rowTitle}>Version</Text>
|
||||||
<Text style={styles.updateHintText}>Paseo installed on this computer.</Text>
|
<Text style={styles.hintText}>{daemonVersionHint}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.aboutValue}>{appVersionText}</Text>
|
<Text style={styles.valueText}>{localDaemonVersionText}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.audioRow, styles.audioRowBorder]}>
|
<View style={[styles.row, styles.rowBorder]}>
|
||||||
<View style={styles.audioRowContent}>
|
<View style={styles.rowContent}>
|
||||||
<Text style={styles.audioRowTitle}>{daemonVersionTitle}</Text>
|
<Text style={styles.rowTitle}>Update daemon</Text>
|
||||||
<Text style={styles.updateHintText}>{daemonHostHint}</Text>
|
<Text style={styles.hintText}>
|
||||||
</View>
|
|
||||||
<Text style={styles.aboutValue}>{localDaemonVersionText}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[styles.audioRow, styles.audioRowBorder]}>
|
|
||||||
<View style={styles.audioRowContent}>
|
|
||||||
<Text style={styles.audioRowTitle}>App update status</Text>
|
|
||||||
<Text style={styles.updateStatusText}>{appUpdateStatusText}</Text>
|
|
||||||
{availableUpdate?.latestVersion ? (
|
|
||||||
<Text style={styles.updateHintText}>
|
|
||||||
New version available: {formatVersionWithPrefix(availableUpdate.latestVersion)}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
{appUpdateError ? <Text style={styles.updateErrorText}>{appUpdateError}</Text> : null}
|
|
||||||
</View>
|
|
||||||
<View style={styles.updateActions}>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onPress={handleCheckForAppUpdates}
|
|
||||||
disabled={!isDesktopUpdaterAvailable || isCheckingAppUpdate || isInstallingAppUpdate}
|
|
||||||
>
|
|
||||||
{isCheckingAppUpdate ? "Checking..." : "Check for updates"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
onPress={handleInstallAppUpdate}
|
|
||||||
disabled={
|
|
||||||
!isDesktopUpdaterAvailable ||
|
|
||||||
isCheckingAppUpdate ||
|
|
||||||
isInstallingAppUpdate ||
|
|
||||||
!availableUpdate
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isInstallingAppUpdate
|
|
||||||
? "Installing..."
|
|
||||||
: availableUpdate?.latestVersion
|
|
||||||
? `Update to ${formatVersionWithPrefix(availableUpdate.latestVersion)}`
|
|
||||||
: "Update app"}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={[styles.audioRow, styles.audioRowBorder]}>
|
|
||||||
<View style={styles.audioRowContent}>
|
|
||||||
<Text style={styles.audioRowTitle}>Update local daemon</Text>
|
|
||||||
<Text style={styles.updateHintText}>
|
|
||||||
Updates the daemon on this computer only. Requires a restart.
|
Updates the daemon on this computer only. Requires a restart.
|
||||||
</Text>
|
</Text>
|
||||||
{localDaemonUpdateMessage ? (
|
{localDaemonUpdateMessage ? (
|
||||||
<Text style={styles.updateStatusText}>{localDaemonUpdateMessage}</Text>
|
<Text style={styles.statusText}>{localDaemonUpdateMessage}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={handleUpdateLocalDaemon}
|
onPress={handleUpdateLocalDaemon}
|
||||||
disabled={!isDesktopUpdaterAvailable || isUpdatingLocalDaemon}
|
disabled={isUpdatingLocalDaemon}
|
||||||
>
|
>
|
||||||
{isUpdatingLocalDaemon ? "Updating..." : "Update daemon"}
|
{isUpdatingLocalDaemon ? "Updating..." : "Update daemon"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -271,8 +169,8 @@ export function DesktopUpdatesSection({ appVersion }: DesktopUpdatesSectionProps
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{daemonVersionMismatch ? (
|
{daemonVersionMismatch ? (
|
||||||
<View style={styles.updateWarningCard}>
|
<View style={styles.warningCard}>
|
||||||
<Text style={styles.updateWarningText}>
|
<Text style={styles.warningText}>
|
||||||
Desktop app and local daemon versions differ. Keep both on the same version to avoid
|
Desktop app and local daemon versions differ. Keep both on the same version to avoid
|
||||||
stability issues or breaking changes.
|
stability issues or breaking changes.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -280,14 +178,14 @@ export function DesktopUpdatesSection({ appVersion }: DesktopUpdatesSectionProps
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{localDaemonUpdateDiagnostics ? (
|
{localDaemonUpdateDiagnostics ? (
|
||||||
<View style={styles.updateDiagnosticsCard}>
|
<View style={styles.diagnosticsCard}>
|
||||||
<View style={styles.updateDiagnosticsHeader}>
|
<View style={styles.diagnosticsHeader}>
|
||||||
<Text style={styles.updateDiagnosticsTitle}>Daemon update diagnostics</Text>
|
<Text style={styles.diagnosticsTitle}>Daemon update diagnostics</Text>
|
||||||
<Button variant="secondary" size="sm" onPress={handleCopyDaemonDiagnostics}>
|
<Button variant="secondary" size="sm" onPress={handleCopyDaemonDiagnostics}>
|
||||||
Copy output
|
Copy output
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.updateDiagnosticsText} selectable>
|
<Text style={styles.diagnosticsText} selectable>
|
||||||
{localDaemonUpdateDiagnostics}
|
{localDaemonUpdateDiagnostics}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -307,58 +205,48 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
marginBottom: theme.spacing[3],
|
marginBottom: theme.spacing[3],
|
||||||
marginLeft: theme.spacing[1],
|
marginLeft: theme.spacing[1],
|
||||||
},
|
},
|
||||||
audioCard: {
|
card: {
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: theme.colors.border,
|
borderColor: theme.colors.border,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
audioRow: {
|
row: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
paddingVertical: theme.spacing[4],
|
paddingVertical: theme.spacing[4],
|
||||||
paddingHorizontal: theme.spacing[4],
|
paddingHorizontal: theme.spacing[4],
|
||||||
},
|
},
|
||||||
audioRowBorder: {
|
rowBorder: {
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: theme.colors.border,
|
borderTopColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
audioRowContent: {
|
rowContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
marginRight: theme.spacing[3],
|
marginRight: theme.spacing[3],
|
||||||
},
|
},
|
||||||
audioRowTitle: {
|
rowTitle: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
},
|
},
|
||||||
aboutValue: {
|
valueText: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
fontWeight: theme.fontWeight.normal,
|
fontWeight: theme.fontWeight.normal,
|
||||||
},
|
},
|
||||||
updateHintText: {
|
hintText: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
updateStatusText: {
|
statusText: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
marginTop: theme.spacing[1],
|
marginTop: theme.spacing[1],
|
||||||
},
|
},
|
||||||
updateActions: {
|
warningCard: {
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
},
|
|
||||||
updateErrorText: {
|
|
||||||
color: theme.colors.palette.red[300],
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
marginTop: theme.spacing[1],
|
|
||||||
},
|
|
||||||
updateWarningCard: {
|
|
||||||
marginTop: theme.spacing[3],
|
marginTop: theme.spacing[3],
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -367,11 +255,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
paddingVertical: theme.spacing[2],
|
paddingVertical: theme.spacing[2],
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
},
|
},
|
||||||
updateWarningText: {
|
warningText: {
|
||||||
color: theme.colors.palette.amber[500],
|
color: theme.colors.palette.amber[500],
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
},
|
},
|
||||||
updateDiagnosticsCard: {
|
diagnosticsCard: {
|
||||||
marginTop: theme.spacing[3],
|
marginTop: theme.spacing[3],
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -380,17 +268,17 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
padding: theme.spacing[3],
|
padding: theme.spacing[3],
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
},
|
},
|
||||||
updateDiagnosticsHeader: {
|
diagnosticsHeader: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
},
|
},
|
||||||
updateDiagnosticsTitle: {
|
diagnosticsTitle: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
},
|
},
|
||||||
updateDiagnosticsText: {
|
diagnosticsText: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import type { HostProfile } from '@/contexts/daemon-registry-context'
|
|
||||||
|
|
||||||
async function loadModuleForPlatform(platform: 'web' | 'ios' | 'android') {
|
async function loadModuleForPlatform(platform: 'web' | 'ios' | 'android') {
|
||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
@@ -7,47 +6,6 @@ async function loadModuleForPlatform(platform: 'web' | 'ios' | 'android') {
|
|||||||
return import('./desktop-updates')
|
return import('./desktop-updates')
|
||||||
}
|
}
|
||||||
|
|
||||||
function createHost(input: {
|
|
||||||
serverId: string
|
|
||||||
endpoint: string
|
|
||||||
type?: 'direct' | 'relay'
|
|
||||||
}): HostProfile {
|
|
||||||
const now = new Date(0).toISOString()
|
|
||||||
|
|
||||||
if (input.type === 'relay') {
|
|
||||||
return {
|
|
||||||
serverId: input.serverId,
|
|
||||||
label: input.serverId,
|
|
||||||
connections: [
|
|
||||||
{
|
|
||||||
id: `${input.serverId}-relay`,
|
|
||||||
type: 'relay',
|
|
||||||
relayEndpoint: input.endpoint,
|
|
||||||
daemonPublicKeyB64: 'test-key',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
preferredConnectionId: `${input.serverId}-relay`,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
serverId: input.serverId,
|
|
||||||
label: input.serverId,
|
|
||||||
connections: [
|
|
||||||
{
|
|
||||||
id: `${input.serverId}-direct`,
|
|
||||||
type: 'direct',
|
|
||||||
endpoint: input.endpoint,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
preferredConnectionId: `${input.serverId}-direct`,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('desktop-updates helpers', () => {
|
describe('desktop-updates helpers', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.doUnmock('react-native')
|
vi.doUnmock('react-native')
|
||||||
@@ -79,20 +37,47 @@ describe('desktop-updates helpers', () => {
|
|||||||
expect(formatVersionWithPrefix(null)).toBe('\u2014')
|
expect(formatVersionWithPrefix(null)).toBe('\u2014')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('prefers localhost direct host when selecting local daemon', async () => {
|
it('parses valid local daemon version result', async () => {
|
||||||
const { findLikelyLocalDaemonHost } = await loadModuleForPlatform('web')
|
const { parseLocalDaemonVersionResult } = await loadModuleForPlatform('web')
|
||||||
const remote = createHost({ serverId: 'remote', endpoint: '10.0.0.2:6767' })
|
|
||||||
const local = createHost({ serverId: 'local', endpoint: 'localhost:6767' })
|
|
||||||
|
|
||||||
expect(findLikelyLocalDaemonHost([remote, local])?.serverId).toBe('local')
|
expect(parseLocalDaemonVersionResult({ version: '0.1.15', error: null })).toEqual({
|
||||||
|
version: '0.1.15',
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('falls back to first host when no localhost endpoint exists', async () => {
|
it('parses local daemon version error result', async () => {
|
||||||
const { findLikelyLocalDaemonHost } = await loadModuleForPlatform('web')
|
const { parseLocalDaemonVersionResult } = await loadModuleForPlatform('web')
|
||||||
const relay = createHost({ serverId: 'relay', endpoint: 'relay.paseo.sh:443', type: 'relay' })
|
|
||||||
const remote = createHost({ serverId: 'remote', endpoint: '10.0.0.2:6767' })
|
|
||||||
|
|
||||||
expect(findLikelyLocalDaemonHost([relay, remote])?.serverId).toBe('relay')
|
expect(
|
||||||
|
parseLocalDaemonVersionResult({ version: null, error: 'paseo command not found in PATH' })
|
||||||
|
).toEqual({
|
||||||
|
version: null,
|
||||||
|
error: 'paseo command not found in PATH',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses unexpected local daemon version result', async () => {
|
||||||
|
const { parseLocalDaemonVersionResult } = await loadModuleForPlatform('web')
|
||||||
|
|
||||||
|
expect(parseLocalDaemonVersionResult(null)).toEqual({
|
||||||
|
version: null,
|
||||||
|
error: 'Unexpected response from version check.',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(parseLocalDaemonVersionResult('not an object')).toEqual({
|
||||||
|
version: null,
|
||||||
|
error: 'Unexpected response from version check.',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('trims whitespace in parsed version', async () => {
|
||||||
|
const { parseLocalDaemonVersionResult } = await loadModuleForPlatform('web')
|
||||||
|
|
||||||
|
expect(parseLocalDaemonVersionResult({ version: ' 0.1.15 ', error: null })).toEqual({
|
||||||
|
version: '0.1.15',
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('builds copyable daemon update diagnostics', async () => {
|
it('builds copyable daemon update diagnostics', async () => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Platform } from 'react-native'
|
import { Platform } from 'react-native'
|
||||||
import type { HostProfile } from '@/contexts/daemon-registry-context'
|
|
||||||
import { getTauri } from '@/utils/tauri'
|
import { getTauri } from '@/utils/tauri'
|
||||||
import { invokeDesktopCommand } from '@/desktop/tauri/invoke-desktop-command'
|
import { invokeDesktopCommand } from '@/desktop/tauri/invoke-desktop-command'
|
||||||
|
|
||||||
@@ -23,6 +22,11 @@ export interface LocalDaemonUpdateResult {
|
|||||||
stderr: string
|
stderr: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LocalDaemonVersionResult {
|
||||||
|
version: string | null
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === 'object' && value !== null
|
return typeof value === 'object' && value !== null
|
||||||
}
|
}
|
||||||
@@ -44,24 +48,26 @@ function toNumberOr(defaultValue: number, value: unknown): number {
|
|||||||
return typeof value === 'number' && Number.isFinite(value) ? value : defaultValue
|
return typeof value === 'number' && Number.isFinite(value) ? value : defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLikelyLocalDirectEndpoint(endpoint: string): boolean {
|
|
||||||
const normalized = endpoint.trim().toLowerCase()
|
|
||||||
return (
|
|
||||||
normalized === 'localhost' ||
|
|
||||||
normalized.startsWith('localhost:') ||
|
|
||||||
normalized === '127.0.0.1' ||
|
|
||||||
normalized.startsWith('127.0.0.1:') ||
|
|
||||||
normalized === '::1' ||
|
|
||||||
normalized.startsWith('::1:') ||
|
|
||||||
normalized === '[::1]' ||
|
|
||||||
normalized.startsWith('[::1]:')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldShowDesktopUpdateSection(): boolean {
|
export function shouldShowDesktopUpdateSection(): boolean {
|
||||||
return Platform.OS === 'web' && getTauri() !== null
|
return Platform.OS === 'web' && getTauri() !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseLocalDaemonVersionResult(raw: unknown): LocalDaemonVersionResult {
|
||||||
|
if (!isRecord(raw)) {
|
||||||
|
return { version: null, error: 'Unexpected response from version check.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: toStringOrNull(raw.version),
|
||||||
|
error: toStringOrNull(raw.error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLocalDaemonVersion(): Promise<LocalDaemonVersionResult> {
|
||||||
|
const result = await invokeDesktopCommand<unknown>('get_local_daemon_version')
|
||||||
|
return parseLocalDaemonVersionResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
export async function checkDesktopAppUpdate(): Promise<DesktopAppUpdateCheckResult> {
|
export async function checkDesktopAppUpdate(): Promise<DesktopAppUpdateCheckResult> {
|
||||||
const result = await invokeDesktopCommand<unknown>('check_app_update')
|
const result = await invokeDesktopCommand<unknown>('check_app_update')
|
||||||
if (!isRecord(result)) {
|
if (!isRecord(result)) {
|
||||||
@@ -135,27 +141,6 @@ export function formatVersionWithPrefix(version: string | null | undefined): str
|
|||||||
return value.startsWith('v') ? value : `v${value}`
|
return value.startsWith('v') ? value : `v${value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findLikelyLocalDaemonHost(daemons: HostProfile[]): HostProfile | null {
|
|
||||||
if (daemons.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const localhostDaemon = daemons.find((daemon) => isLikelyLocalHost(daemon))
|
|
||||||
|
|
||||||
return localhostDaemon ?? daemons[0] ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLikelyLocalHost(host: HostProfile | null | undefined): boolean {
|
|
||||||
if (!host) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return host.connections.some(
|
|
||||||
(connection) =>
|
|
||||||
connection.type === 'direct' && isLikelyLocalDirectEndpoint(connection.endpoint)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildDaemonUpdateDiagnostics(result: LocalDaemonUpdateResult): string {
|
export function buildDaemonUpdateDiagnostics(result: LocalDaemonUpdateResult): string {
|
||||||
const stdout = result.stdout.length > 0 ? result.stdout : '(empty)'
|
const stdout = result.stdout.length > 0 ? result.stdout : '(empty)'
|
||||||
const stderr = result.stderr.length > 0 ? result.stderr : '(empty)'
|
const stderr = result.stderr.length > 0 ? result.stderr : '(empty)'
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
import Constants from "expo-constants";
|
import Constants from "expo-constants";
|
||||||
|
import appPackage from "../../package.json";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { Sun, Moon, Monitor, Globe, Settings, RotateCw, Trash2 } from "lucide-react-native";
|
import { Sun, Moon, Monitor, Globe, Settings, RotateCw, Trash2 } from "lucide-react-native";
|
||||||
@@ -38,7 +40,9 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet";
|
import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet";
|
||||||
import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section";
|
import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section";
|
||||||
import { DesktopUpdatesSection } from "@/desktop/components/desktop-updates-section";
|
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section";
|
||||||
|
import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater";
|
||||||
|
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
|
||||||
|
|
||||||
const delay = (ms: number) =>
|
const delay = (ms: number) =>
|
||||||
new Promise<void>((resolve) => {
|
new Promise<void>((resolve) => {
|
||||||
@@ -319,6 +323,21 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
fontWeight: theme.fontWeight.normal,
|
fontWeight: theme.fontWeight.normal,
|
||||||
},
|
},
|
||||||
|
aboutHintText: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
aboutErrorText: {
|
||||||
|
color: theme.colors.palette.red[300],
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
marginTop: theme.spacing[1],
|
||||||
|
},
|
||||||
|
aboutUpdateActions: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
},
|
||||||
// Empty state
|
// Empty state
|
||||||
emptyCard: {
|
emptyCard: {
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
@@ -367,6 +386,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
function resolveAppVersion(): string | null {
|
function resolveAppVersion(): string | null {
|
||||||
|
const packageVersion = appPackage?.version;
|
||||||
|
if (typeof packageVersion === "string" && packageVersion.trim().length > 0) {
|
||||||
|
return packageVersion.trim();
|
||||||
|
}
|
||||||
|
|
||||||
const expoVersion = Constants.expoConfig?.version;
|
const expoVersion = Constants.expoConfig?.version;
|
||||||
if (typeof expoVersion === "string" && expoVersion.trim().length > 0) {
|
if (typeof expoVersion === "string" && expoVersion.trim().length > 0) {
|
||||||
return expoVersion.trim();
|
return expoVersion.trim();
|
||||||
@@ -404,6 +428,100 @@ function formatVersionForDisplay(version: string | null | undefined): string {
|
|||||||
return `v${value}`;
|
return `v${value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DesktopAppUpdateRow() {
|
||||||
|
const {
|
||||||
|
isDesktop,
|
||||||
|
statusText,
|
||||||
|
availableUpdate,
|
||||||
|
errorMessage,
|
||||||
|
isChecking,
|
||||||
|
isInstalling,
|
||||||
|
checkForUpdates,
|
||||||
|
installUpdate,
|
||||||
|
} = useDesktopAppUpdater();
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
if (!isDesktop) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
void checkForUpdates({ silent: true });
|
||||||
|
return undefined;
|
||||||
|
}, [checkForUpdates, isDesktop])
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCheckForUpdates = useCallback(() => {
|
||||||
|
if (!isDesktop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void checkForUpdates();
|
||||||
|
}, [checkForUpdates, isDesktop]);
|
||||||
|
|
||||||
|
const handleInstallUpdate = useCallback(() => {
|
||||||
|
if (!isDesktop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void confirmDialog({
|
||||||
|
title: "Install desktop update",
|
||||||
|
message: "This updates Paseo on this computer.",
|
||||||
|
confirmLabel: "Install update",
|
||||||
|
cancelLabel: "Cancel",
|
||||||
|
})
|
||||||
|
.then((confirmed) => {
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void installUpdate();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("[Settings] Failed to open app update confirmation", error);
|
||||||
|
Alert.alert("Error", "Unable to open the update confirmation dialog.");
|
||||||
|
});
|
||||||
|
}, [installUpdate, isDesktop]);
|
||||||
|
|
||||||
|
if (!isDesktop) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.audioRow, styles.audioRowBorder]}>
|
||||||
|
<View style={styles.audioRowContent}>
|
||||||
|
<Text style={styles.audioRowTitle}>App updates</Text>
|
||||||
|
<Text style={styles.aboutHintText}>{statusText}</Text>
|
||||||
|
{availableUpdate?.latestVersion ? (
|
||||||
|
<Text style={styles.aboutHintText}>
|
||||||
|
New version available: {formatVersionWithPrefix(availableUpdate.latestVersion)}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{errorMessage ? <Text style={styles.aboutErrorText}>{errorMessage}</Text> : null}
|
||||||
|
</View>
|
||||||
|
<View style={styles.aboutUpdateActions}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onPress={handleCheckForUpdates}
|
||||||
|
disabled={isChecking || isInstalling}
|
||||||
|
>
|
||||||
|
{isChecking ? "Checking..." : "Check"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onPress={handleInstallUpdate}
|
||||||
|
disabled={isChecking || isInstalling || !availableUpdate}
|
||||||
|
>
|
||||||
|
{isInstalling
|
||||||
|
? "Installing..."
|
||||||
|
: availableUpdate?.latestVersion
|
||||||
|
? `Update to ${formatVersionWithPrefix(availableUpdate.latestVersion)}`
|
||||||
|
: "Update"}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function SettingsScreen() {
|
export default function SettingsScreen() {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -791,7 +909,7 @@ export default function SettingsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{isDesktop ? <DesktopPermissionsSection /> : null}
|
{isDesktop ? <DesktopPermissionsSection /> : null}
|
||||||
{isDesktop ? <DesktopUpdatesSection appVersion={appVersion} /> : null}
|
{isDesktop ? <LocalDaemonSection appVersion={appVersion} /> : null}
|
||||||
|
|
||||||
{/* About */}
|
{/* About */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
@@ -803,6 +921,7 @@ export default function SettingsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
<Text style={styles.aboutValue}>{appVersionText}</Text>
|
<Text style={styles.aboutValue}>{appVersionText}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{isDesktop ? <DesktopAppUpdateRow /> : null}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ struct AppUpdateInstallResult {
|
|||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct LocalDaemonVersionResult {
|
||||||
|
version: Option<String>,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_login_shell() -> String {
|
fn resolve_login_shell() -> String {
|
||||||
std::env::var("SHELL")
|
std::env::var("SHELL")
|
||||||
.ok()
|
.ok()
|
||||||
@@ -54,6 +61,48 @@ fn resolve_login_shell() -> String {
|
|||||||
.unwrap_or_else(|| "/bin/zsh".to_string())
|
.unwrap_or_else(|| "/bin/zsh".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn execute_local_daemon_version(shell: &str) -> LocalDaemonVersionResult {
|
||||||
|
let script = r#"if command -v paseo >/dev/null 2>&1; then
|
||||||
|
paseo --version
|
||||||
|
else
|
||||||
|
echo "paseo command not found in PATH" >&2
|
||||||
|
exit 127
|
||||||
|
fi"#;
|
||||||
|
|
||||||
|
match Command::new(shell).arg("-lc").arg(script).output() {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
if version.is_empty() {
|
||||||
|
LocalDaemonVersionResult {
|
||||||
|
version: None,
|
||||||
|
error: Some("paseo --version returned empty output".to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LocalDaemonVersionResult {
|
||||||
|
version: Some(version),
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
|
LocalDaemonVersionResult {
|
||||||
|
version: None,
|
||||||
|
error: Some(if stderr.is_empty() {
|
||||||
|
format!("paseo --version exited with code {}", output.status.code().unwrap_or(1))
|
||||||
|
} else {
|
||||||
|
stderr
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => LocalDaemonVersionResult {
|
||||||
|
version: None,
|
||||||
|
error: Some(format!("Failed to run version check: {error}")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn execute_local_daemon_update(shell: &str) -> DaemonUpdateCommandResult {
|
fn execute_local_daemon_update(shell: &str) -> DaemonUpdateCommandResult {
|
||||||
let script = r#"if command -v paseo >/dev/null 2>&1; then
|
let script = r#"if command -v paseo >/dev/null 2>&1; then
|
||||||
paseo daemon update
|
paseo daemon update
|
||||||
@@ -76,6 +125,17 @@ fi"#;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_local_daemon_version() -> LocalDaemonVersionResult {
|
||||||
|
let shell = resolve_login_shell();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || execute_local_daemon_version(&shell))
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|error| LocalDaemonVersionResult {
|
||||||
|
version: None,
|
||||||
|
error: Some(format!("Version check task failed: {error}")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn run_local_daemon_update() -> DaemonUpdateCommandResult {
|
async fn run_local_daemon_update() -> DaemonUpdateCommandResult {
|
||||||
let shell = resolve_login_shell();
|
let shell = resolve_login_shell();
|
||||||
@@ -158,6 +218,7 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_websocket::init())
|
.plugin(tauri_plugin_websocket::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
get_local_daemon_version,
|
||||||
run_local_daemon_update,
|
run_local_daemon_update,
|
||||||
check_app_update,
|
check_app_update,
|
||||||
install_app_update
|
install_app_update
|
||||||
|
|||||||
Reference in New Issue
Block a user