mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Update files
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
// App name and package ID are controlled by Gradle product flavors (dev/production)
|
||||
// See plugins/with-android-product-flavors.js for flavor configuration
|
||||
|
||||
const pkg = require("./package.json");
|
||||
|
||||
export default {
|
||||
expo: {
|
||||
name: "Paseo",
|
||||
slug: "voice-mobile",
|
||||
version: "0.1.0",
|
||||
version: pkg.version,
|
||||
orientation: "portrait",
|
||||
icon: "./assets/images/icon.png",
|
||||
scheme: "paseo",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { router, useLocalSearchParams } from "expo-router";
|
||||
import Constants from "expo-constants";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
import { Sun, Moon, Monitor, MoreVertical } from "lucide-react-native";
|
||||
import { Sun, Moon, Monitor, MoreVertical, Globe } from "lucide-react-native";
|
||||
import { Fonts } from "@/constants/theme";
|
||||
import { useAppSettings, type AppSettings } from "@/hooks/use-settings";
|
||||
import { useDaemonRegistry, type HostProfile } from "@/contexts/daemon-registry-context";
|
||||
@@ -126,16 +126,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
hostUrl: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontFamily: Fonts.mono,
|
||||
},
|
||||
hostConnections: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontFamily: Fonts.mono,
|
||||
},
|
||||
hostError: {
|
||||
color: theme.colors.palette.red[300],
|
||||
fontSize: theme.fontSize.xs,
|
||||
@@ -158,6 +148,24 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
connectionPill: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
paddingVertical: 4,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.surface3,
|
||||
maxWidth: 170,
|
||||
},
|
||||
connectionText: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
color: theme.colors.foregroundMuted,
|
||||
flexShrink: 1,
|
||||
},
|
||||
menuButton: {
|
||||
width: 36,
|
||||
height: 32,
|
||||
@@ -390,6 +398,8 @@ export default function SettingsScreen() {
|
||||
const [addConnectionTargetServerId, setAddConnectionTargetServerId] = useState<string | null>(null);
|
||||
const [pendingEditReopenServerId, setPendingEditReopenServerId] = useState<string | null>(null);
|
||||
const [pendingNameHost, setPendingNameHost] = useState<{ serverId: string; hostname: string | null } | null>(null);
|
||||
const [pendingRemoveHost, setPendingRemoveHost] = useState<HostProfile | null>(null);
|
||||
const [isRemovingHost, setIsRemovingHost] = useState(false);
|
||||
const [editingDaemon, setEditingDaemon] = useState<HostProfile | null>(null);
|
||||
const [editLabel, setEditLabel] = useState("");
|
||||
const [isSavingEdit, setIsSavingEdit] = useState(false);
|
||||
@@ -441,6 +451,19 @@ export default function SettingsScreen() {
|
||||
setEditLabel("");
|
||||
}, [isSavingEdit]);
|
||||
|
||||
const closeAddConnectionFlow = useCallback(() => {
|
||||
setIsAddHostMethodVisible(false);
|
||||
setIsDirectHostVisible(false);
|
||||
setIsPasteLinkVisible(false);
|
||||
setAddConnectionTargetServerId(null);
|
||||
}, []);
|
||||
|
||||
const goBackToAddConnectionMethods = useCallback(() => {
|
||||
setIsDirectHostVisible(false);
|
||||
setIsPasteLinkVisible(false);
|
||||
setIsAddHostMethodVisible(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const editHost = typeof params.editHost === "string" ? params.editHost.trim() : "";
|
||||
if (!editHost) return;
|
||||
@@ -498,45 +521,9 @@ export default function SettingsScreen() {
|
||||
[removeConnection]
|
||||
);
|
||||
|
||||
const handleRemoveDaemon = useCallback(
|
||||
(profile: HostProfile) => {
|
||||
if (Platform.OS === "web") {
|
||||
const hasBrowserConfirm =
|
||||
typeof globalThis !== "undefined" &&
|
||||
typeof (globalThis as any).confirm === "function";
|
||||
|
||||
const confirmed = hasBrowserConfirm ? (globalThis as any).confirm(`Remove ${profile.label}?`) : true;
|
||||
if (!confirmed) return;
|
||||
|
||||
void removeHost(profile.serverId).catch((error) => {
|
||||
console.error("[Settings] Failed to remove daemon", error);
|
||||
Alert.alert("Error", "Unable to remove host");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
"Remove host",
|
||||
`Remove ${profile.label}?`,
|
||||
[
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{
|
||||
text: "Remove",
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
try {
|
||||
await removeHost(profile.serverId);
|
||||
} catch (error) {
|
||||
console.error("[Settings] Failed to remove daemon", error);
|
||||
Alert.alert("Error", "Unable to remove host");
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
},
|
||||
[removeHost]
|
||||
);
|
||||
const handleRemoveDaemon = useCallback((profile: HostProfile) => {
|
||||
setPendingRemoveHost(profile);
|
||||
}, []);
|
||||
|
||||
const handleThemeChange = useCallback(
|
||||
(newTheme: AppSettings["theme"]) => {
|
||||
@@ -641,13 +628,20 @@ export default function SettingsScreen() {
|
||||
|
||||
<AddHostMethodModal
|
||||
visible={isAddHostMethodVisible}
|
||||
onClose={() => setIsAddHostMethodVisible(false)}
|
||||
onDirectConnection={() => setIsDirectHostVisible(true)}
|
||||
onPasteLink={() => setIsPasteLinkVisible(true)}
|
||||
onClose={closeAddConnectionFlow}
|
||||
onDirectConnection={() => {
|
||||
setIsAddHostMethodVisible(false);
|
||||
setIsDirectHostVisible(true);
|
||||
}}
|
||||
onPasteLink={() => {
|
||||
setIsAddHostMethodVisible(false);
|
||||
setIsPasteLinkVisible(true);
|
||||
}}
|
||||
onScanQr={() => {
|
||||
const target = addConnectionTargetServerId;
|
||||
const source = target ? "editHost" : "settings";
|
||||
const qs = target ? `?source=${source}&targetServerId=${encodeURIComponent(target)}` : `?source=${source}`;
|
||||
closeAddConnectionFlow();
|
||||
router.push(`/pair-scan${qs}`);
|
||||
}}
|
||||
/>
|
||||
@@ -655,7 +649,8 @@ export default function SettingsScreen() {
|
||||
<AddHostModal
|
||||
visible={isDirectHostVisible}
|
||||
targetServerId={addConnectionTargetServerId ?? undefined}
|
||||
onClose={() => setIsDirectHostVisible(false)}
|
||||
onClose={closeAddConnectionFlow}
|
||||
onCancel={goBackToAddConnectionMethods}
|
||||
onSaved={({ serverId, hostname, isNewHost }) => {
|
||||
if (isNewHost) {
|
||||
setPendingNameHost({ serverId, hostname });
|
||||
@@ -666,7 +661,8 @@ export default function SettingsScreen() {
|
||||
<PairLinkModal
|
||||
visible={isPasteLinkVisible}
|
||||
targetServerId={addConnectionTargetServerId ?? undefined}
|
||||
onClose={() => setIsPasteLinkVisible(false)}
|
||||
onClose={closeAddConnectionFlow}
|
||||
onCancel={goBackToAddConnectionMethods}
|
||||
onSaved={({ serverId, hostname, isNewHost }) => {
|
||||
if (isNewHost) {
|
||||
setPendingNameHost({ serverId, hostname });
|
||||
@@ -688,6 +684,51 @@ export default function SettingsScreen() {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{pendingRemoveHost ? (
|
||||
<AdaptiveModalSheet
|
||||
title="Remove host"
|
||||
visible
|
||||
onClose={() => {
|
||||
if (isRemovingHost) return;
|
||||
setPendingRemoveHost(null);
|
||||
}}
|
||||
testID="remove-host-confirm-modal"
|
||||
>
|
||||
<Text style={{ color: theme.colors.foregroundMuted, fontSize: 14 }}>
|
||||
Remove {pendingRemoveHost.label}? This will delete its saved connections.
|
||||
</Text>
|
||||
<View style={[styles.formActionsRow, { marginTop: theme.spacing[4] }]}>
|
||||
<Pressable
|
||||
style={[styles.formButton, isRemovingHost && styles.disabled]}
|
||||
onPress={() => setPendingRemoveHost(null)}
|
||||
disabled={isRemovingHost}
|
||||
>
|
||||
<Text style={styles.formButtonText}>Cancel</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.formButton, isRemovingHost && styles.disabled, { backgroundColor: theme.colors.destructive }]}
|
||||
onPress={() => {
|
||||
const serverId = pendingRemoveHost.serverId;
|
||||
setIsRemovingHost(true);
|
||||
void removeHost(serverId)
|
||||
.then(() => setPendingRemoveHost(null))
|
||||
.catch((error) => {
|
||||
console.error("[Settings] Failed to remove host", error);
|
||||
Alert.alert("Error", "Unable to remove host");
|
||||
})
|
||||
.finally(() => setIsRemovingHost(false));
|
||||
}}
|
||||
disabled={isRemovingHost}
|
||||
testID="remove-host-confirm"
|
||||
>
|
||||
<Text style={[styles.formButtonText, { color: theme.colors.palette.white }]}>
|
||||
Remove
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</AdaptiveModalSheet>
|
||||
) : null}
|
||||
|
||||
<AdaptiveModalSheet
|
||||
title="Edit host"
|
||||
visible={Boolean(editingDaemon)}
|
||||
@@ -995,13 +1036,15 @@ function DaemonCard({
|
||||
: statusTone === "error"
|
||||
? "rgba(248, 113, 113, 0.1)"
|
||||
: "rgba(161, 161, 170, 0.1)";
|
||||
|
||||
const connectionsSummary = (() => {
|
||||
const parts = daemon.connections.map((conn) => {
|
||||
if (conn.type === "relay") return `relay:${conn.relayEndpoint}`;
|
||||
return `direct:${conn.endpoint}`;
|
||||
});
|
||||
return parts.join(" • ");
|
||||
const connectionBadge = (() => {
|
||||
if (!activeConnection) return null;
|
||||
if (activeConnection.type === "relay") {
|
||||
return { icon: <Globe size={12} color={theme.colors.foregroundMuted} />, text: "Relay" };
|
||||
}
|
||||
return {
|
||||
icon: <Monitor size={12} color={theme.colors.foregroundMuted} />,
|
||||
text: activeConnection.display,
|
||||
};
|
||||
})();
|
||||
|
||||
return (
|
||||
@@ -1015,6 +1058,15 @@ function DaemonCard({
|
||||
<Text style={[styles.statusText, { color: statusColor }]}>{badgeText}</Text>
|
||||
</View>
|
||||
|
||||
{connectionBadge ? (
|
||||
<View style={styles.connectionPill}>
|
||||
{connectionBadge.icon}
|
||||
<Text style={styles.connectionText} numberOfLines={1}>
|
||||
{connectionBadge.text}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
testID={`daemon-menu-trigger-${daemon.serverId}`}
|
||||
@@ -1050,32 +1102,6 @@ function DaemonCard({
|
||||
</DropdownMenu>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.hostUrl}>
|
||||
{(() => {
|
||||
if (connectionStatus === "online") {
|
||||
if (activeConnection?.type === "relay") return "Connected via relay";
|
||||
if (activeConnection?.type === "direct") return `Connected via ${activeConnection.display}`;
|
||||
return "Connected";
|
||||
}
|
||||
|
||||
if (connectionStatus === "connecting" && activeConnection) {
|
||||
if (activeConnection.type === "relay") return "Trying relay";
|
||||
return `Trying ${activeConnection.display}`;
|
||||
}
|
||||
|
||||
if ((connectionStatus === "offline" || connectionStatus === "error") && activeConnection) {
|
||||
if (activeConnection.type === "relay") return "Last tried: relay";
|
||||
return `Last tried: ${activeConnection.display}`;
|
||||
}
|
||||
|
||||
const relay = daemon.connections.find((c) => c.type === "relay");
|
||||
const direct = daemon.connections.find((c) => c.type === "direct");
|
||||
if (direct) return direct.endpoint;
|
||||
if (relay) return "Relay";
|
||||
return "";
|
||||
})()}
|
||||
</Text>
|
||||
{connectionsSummary ? <Text style={styles.hostConnections}>{connectionsSummary}</Text> : null}
|
||||
{connectionError ? <Text style={styles.hostError}>{connectionError}</Text> : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -48,19 +48,16 @@ export function AddHostMethodModal({
|
||||
const { theme } = useUnistyles();
|
||||
|
||||
const handleDirect = useCallback(() => {
|
||||
onClose();
|
||||
onDirectConnection();
|
||||
}, [onClose, onDirectConnection]);
|
||||
}, [onDirectConnection]);
|
||||
|
||||
const handleScan = useCallback(() => {
|
||||
onClose();
|
||||
onScanQr();
|
||||
}, [onClose, onScanQr]);
|
||||
}, [onScanQr]);
|
||||
|
||||
const handlePaste = useCallback(() => {
|
||||
onClose();
|
||||
onPasteLink();
|
||||
}, [onClose, onPasteLink]);
|
||||
}, [onPasteLink]);
|
||||
|
||||
return (
|
||||
<AdaptiveModalSheet title="Add connection" visible={visible} onClose={onClose} testID="add-host-method-modal">
|
||||
|
||||
@@ -147,10 +147,11 @@ export interface AddHostModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
targetServerId?: string;
|
||||
onCancel?: () => void;
|
||||
onSaved?: (result: { profile: HostProfile; serverId: string; hostname: string | null; isNewHost: boolean }) => void;
|
||||
}
|
||||
|
||||
export function AddHostModal({ visible, onClose, onSaved, targetServerId }: AddHostModalProps) {
|
||||
export function AddHostModal({ visible, onClose, onCancel, onSaved, targetServerId }: AddHostModalProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const { daemons, upsertDirectConnection } = useDaemonRegistry();
|
||||
const isMobile =
|
||||
@@ -170,6 +171,13 @@ export function AddHostModal({ visible, onClose, onSaved, targetServerId }: AddH
|
||||
onClose();
|
||||
}, [isSaving, onClose]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isSaving) return;
|
||||
setEndpointRaw("");
|
||||
setErrorMessage("");
|
||||
(onCancel ?? onClose)();
|
||||
}, [isSaving, onCancel, onClose]);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (isSaving) return;
|
||||
|
||||
@@ -256,8 +264,8 @@ export function AddHostModal({ visible, onClose, onSaved, targetServerId }: AddH
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={styles.button} onPress={handleClose} disabled={isSaving}>
|
||||
<Text style={styles.buttonText}>Cancel</Text>
|
||||
<Pressable style={styles.button} onPress={handleCancel} disabled={isSaving}>
|
||||
<Text style={styles.buttonText}>{onCancel ? "Back" : "Cancel"}</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.button, styles.primaryButton, isSaving ? { opacity: 0.7 } : null]}
|
||||
|
||||
@@ -69,10 +69,11 @@ export interface PairLinkModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
targetServerId?: string;
|
||||
onCancel?: () => void;
|
||||
onSaved?: (result: { profile: HostProfile; serverId: string; hostname: string | null; isNewHost: boolean }) => void;
|
||||
}
|
||||
|
||||
export function PairLinkModal({ visible, onClose, onSaved, targetServerId }: PairLinkModalProps) {
|
||||
export function PairLinkModal({ visible, onClose, onCancel, onSaved, targetServerId }: PairLinkModalProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const { daemons, upsertDaemonFromOfferUrl } = useDaemonRegistry();
|
||||
const isMobile =
|
||||
@@ -90,6 +91,13 @@ export function PairLinkModal({ visible, onClose, onSaved, targetServerId }: Pai
|
||||
onClose();
|
||||
}, [isSaving, onClose]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isSaving) return;
|
||||
setOfferUrl("");
|
||||
setErrorMessage("");
|
||||
(onCancel ?? onClose)();
|
||||
}, [isSaving, onCancel, onClose]);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (isSaving) return;
|
||||
const raw = offerUrl.trim();
|
||||
@@ -178,14 +186,14 @@ export function PairLinkModal({ visible, onClose, onSaved, targetServerId }: Pai
|
||||
<View style={styles.actions}>
|
||||
<Pressable
|
||||
style={styles.button}
|
||||
onPress={handleClose}
|
||||
onPress={handleCancel}
|
||||
disabled={isSaving}
|
||||
testID="pair-link-cancel"
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel"
|
||||
>
|
||||
<Text style={styles.buttonText}>Cancel</Text>
|
||||
<Text style={styles.buttonText}>{onCancel ? "Back" : "Cancel"}</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.button, styles.primaryButton, isSaving ? { opacity: 0.7 } : null]}
|
||||
|
||||
Reference in New Issue
Block a user