refactor(app): restructure settings with modular section navigation
This commit is contained in:
@@ -2,6 +2,7 @@ import { View, Text } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { Check } from "lucide-react-native";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
import type { DesktopPermissionStatus } from "@/desktop/permissions/desktop-permissions";
|
||||
|
||||
export interface DesktopPermissionRowProps {
|
||||
@@ -38,9 +39,9 @@ export function DesktopPermissionRow({
|
||||
state !== "not-granted";
|
||||
|
||||
return (
|
||||
<View style={[styles.audioRow, showBorder && styles.audioRowBorder]}>
|
||||
<View style={styles.audioRowContent}>
|
||||
<Text style={styles.audioRowTitle}>{title}</Text>
|
||||
<View style={[settingsStyles.row, showBorder && settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>{title}</Text>
|
||||
</View>
|
||||
<View style={styles.permissionRowActions}>
|
||||
{isGranted ? (
|
||||
@@ -74,25 +75,6 @@ export function DesktopPermissionRow({
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
audioRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: theme.spacing[4],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
},
|
||||
audioRowBorder: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: theme.colors.border,
|
||||
},
|
||||
audioRowContent: {
|
||||
flex: 1,
|
||||
marginRight: theme.spacing[3],
|
||||
},
|
||||
audioRowTitle: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.base,
|
||||
},
|
||||
permissionRowActions: {
|
||||
alignItems: "flex-end",
|
||||
gap: theme.spacing[1],
|
||||
@@ -117,7 +99,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
},
|
||||
permissionStatusText: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
permissionDetailText: {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { DesktopPermissionRow } from "@/desktop/components/desktop-permission-row";
|
||||
import { useDesktopPermissions } from "@/desktop/permissions/use-desktop-permissions";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
import { SettingsSection } from "@/screens/settings/settings-section";
|
||||
|
||||
export function DesktopPermissionsSection() {
|
||||
const { theme } = useUnistyles();
|
||||
@@ -27,23 +28,23 @@ export function DesktopPermissionsSection() {
|
||||
const isBusy = isRefreshing || requestingPermission !== null;
|
||||
const notificationsGranted = snapshot?.notifications.state === "granted";
|
||||
|
||||
const refreshButton = (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={<RotateCw size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
||||
onPress={() => {
|
||||
void refreshPermissions();
|
||||
}}
|
||||
disabled={isBusy}
|
||||
accessibilityLabel="Refresh desktop permissions"
|
||||
>
|
||||
{isRefreshing ? "Refreshing..." : "Refresh"}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={settingsStyles.section}>
|
||||
<View style={styles.permissionSectionHeader}>
|
||||
<Text style={settingsStyles.sectionTitle}>Desktop Permissions</Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={<RotateCw size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
||||
onPress={() => {
|
||||
void refreshPermissions();
|
||||
}}
|
||||
disabled={isBusy}
|
||||
accessibilityLabel="Refresh desktop permissions"
|
||||
>
|
||||
{isRefreshing ? "Refreshing..." : "Refresh"}
|
||||
</Button>
|
||||
</View>
|
||||
<SettingsSection title="Permissions" trailing={refreshButton}>
|
||||
<View style={settingsStyles.card}>
|
||||
<DesktopPermissionRow
|
||||
title="Notifications"
|
||||
@@ -74,18 +75,11 @@ export function DesktopPermissionsSection() {
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
permissionSectionHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing[2],
|
||||
marginBottom: theme.spacing[3],
|
||||
},
|
||||
errorText: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ActivityIndicator, Alert, Text, View } from "react-native";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
import { SettingsSection } from "@/screens/settings/settings-section";
|
||||
import { ArrowUpRight, Play, Pause, RotateCw, Copy, FileText, Activity } from "lucide-react-native";
|
||||
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -18,15 +19,12 @@ import {
|
||||
stopDesktopDaemon,
|
||||
} from "@/desktop/daemon/desktop-daemon";
|
||||
import { useDaemonStatus } from "@/desktop/hooks/use-daemon-status";
|
||||
import { resolveAppVersion } from "@/utils/app-version";
|
||||
|
||||
export interface LocalDaemonSectionProps {
|
||||
appVersion: string | null;
|
||||
showLifecycleControls: boolean;
|
||||
}
|
||||
|
||||
export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalDaemonSectionProps) {
|
||||
export function LocalDaemonSection() {
|
||||
const { theme } = useUnistyles();
|
||||
const showSection = shouldUseDesktopDaemon();
|
||||
const appVersion = resolveAppVersion();
|
||||
const { settings, updateSettings } = useAppSettings();
|
||||
const { data, isLoading, error: statusError, setStatus, refetch } = useDaemonStatus();
|
||||
const [isRestartingDaemon, setIsRestartingDaemon] = useState(false);
|
||||
@@ -236,22 +234,26 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD
|
||||
return null;
|
||||
}
|
||||
|
||||
const advancedSettingsButton = (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={<ArrowUpRight size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />}
|
||||
textStyle={settingsStyles.sectionHeaderLinkText}
|
||||
style={settingsStyles.sectionHeaderLink}
|
||||
onPress={() => void openExternalUrl(ADVANCED_DAEMON_SETTINGS_URL)}
|
||||
accessibilityLabel="Open advanced daemon settings"
|
||||
>
|
||||
Advanced settings
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={settingsStyles.section}>
|
||||
<View style={settingsStyles.sectionHeader}>
|
||||
<Text style={settingsStyles.sectionHeaderTitle}>Built-in daemon</Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={<ArrowUpRight size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />}
|
||||
textStyle={settingsStyles.sectionHeaderLinkText}
|
||||
style={settingsStyles.sectionHeaderLink}
|
||||
onPress={() => void openExternalUrl(ADVANCED_DAEMON_SETTINGS_URL)}
|
||||
accessibilityLabel="Open advanced daemon settings"
|
||||
>
|
||||
Advanced settings
|
||||
</Button>
|
||||
</View>
|
||||
<SettingsSection
|
||||
title="Daemon"
|
||||
trailing={advancedSettingsButton}
|
||||
testID="host-page-daemon-lifecycle-card"
|
||||
>
|
||||
{isLoading ? (
|
||||
<View style={[settingsStyles.card, styles.loadingCard]}>
|
||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
||||
@@ -271,61 +273,57 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD
|
||||
<Text style={styles.valueSubtext}>{daemonStatusDetailText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{showLifecycleControls ? (
|
||||
<>
|
||||
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>Daemon management</Text>
|
||||
<Text style={settingsStyles.rowHint}>
|
||||
{isDaemonManagementPaused
|
||||
? "Paused. The built-in daemon stays stopped until you start it again."
|
||||
: "Enabled. Paseo can manage the built-in daemon from the desktop app."}
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
leftIcon={
|
||||
isDaemonManagementPaused ? (
|
||||
<Play size={theme.iconSize.sm} color={theme.colors.foreground} />
|
||||
) : (
|
||||
<Pause size={theme.iconSize.sm} color={theme.colors.foreground} />
|
||||
)
|
||||
}
|
||||
onPress={handleToggleDaemonManagement}
|
||||
disabled={isUpdatingDaemonManagement}
|
||||
>
|
||||
{isUpdatingDaemonManagement
|
||||
? isDaemonManagementPaused
|
||||
? "Resuming..."
|
||||
: "Pausing..."
|
||||
: isDaemonManagementPaused
|
||||
? "Resume"
|
||||
: "Pause"}
|
||||
</Button>
|
||||
</View>
|
||||
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>{daemonActionLabel}</Text>
|
||||
<Text style={settingsStyles.rowHint}>{daemonActionMessage}</Text>
|
||||
{statusMessage ? <Text style={styles.statusText}>{statusMessage}</Text> : null}
|
||||
</View>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
leftIcon={<RotateCw size={theme.iconSize.sm} color={theme.colors.foreground} />}
|
||||
onPress={handleUpdateLocalDaemon}
|
||||
disabled={isRestartingDaemon}
|
||||
>
|
||||
{isRestartingDaemon
|
||||
? daemonStatus?.status === "running"
|
||||
? "Restarting..."
|
||||
: "Starting..."
|
||||
: daemonActionLabel}
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>Daemon management</Text>
|
||||
<Text style={settingsStyles.rowHint}>
|
||||
{isDaemonManagementPaused
|
||||
? "Paused. The built-in daemon stays stopped until you start it again."
|
||||
: "Enabled. Paseo can manage the built-in daemon from the desktop app."}
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
leftIcon={
|
||||
isDaemonManagementPaused ? (
|
||||
<Play size={theme.iconSize.sm} color={theme.colors.foreground} />
|
||||
) : (
|
||||
<Pause size={theme.iconSize.sm} color={theme.colors.foreground} />
|
||||
)
|
||||
}
|
||||
onPress={handleToggleDaemonManagement}
|
||||
disabled={isUpdatingDaemonManagement}
|
||||
>
|
||||
{isUpdatingDaemonManagement
|
||||
? isDaemonManagementPaused
|
||||
? "Resuming..."
|
||||
: "Pausing..."
|
||||
: isDaemonManagementPaused
|
||||
? "Resume"
|
||||
: "Pause"}
|
||||
</Button>
|
||||
</View>
|
||||
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>{daemonActionLabel}</Text>
|
||||
<Text style={settingsStyles.rowHint}>{daemonActionMessage}</Text>
|
||||
{statusMessage ? <Text style={styles.statusText}>{statusMessage}</Text> : null}
|
||||
</View>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
leftIcon={<RotateCw size={theme.iconSize.sm} color={theme.colors.foreground} />}
|
||||
onPress={handleUpdateLocalDaemon}
|
||||
disabled={isRestartingDaemon}
|
||||
>
|
||||
{isRestartingDaemon
|
||||
? daemonStatus?.status === "running"
|
||||
? "Restarting..."
|
||||
: "Starting..."
|
||||
: daemonActionLabel}
|
||||
</Button>
|
||||
</View>
|
||||
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>Log file</Text>
|
||||
@@ -423,7 +421,7 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD
|
||||
</View>
|
||||
</View>
|
||||
</AdaptiveModalSheet>
|
||||
</View>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -448,7 +446,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
valueText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
valueSubtext: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
@@ -482,17 +479,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
||||
lineHeight: 18,
|
||||
},
|
||||
codeBlock: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
||||
lineHeight: 18,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
borderRadius: theme.borderRadius.md,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
padding: theme.spacing[3],
|
||||
},
|
||||
modalActions: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useFocusEffect } from "@react-navigation/native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { ArrowUpRight, Terminal, Blocks, Check } from "lucide-react-native";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
import { SettingsSection } from "@/screens/settings/settings-section";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { openExternalUrl } from "@/utils/open-external-url";
|
||||
import {
|
||||
@@ -79,39 +80,35 @@ export function IntegrationsSection() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trailing = (
|
||||
<View style={styles.headerLinks}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={<ArrowUpRight size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />}
|
||||
textStyle={settingsStyles.sectionHeaderLinkText}
|
||||
style={settingsStyles.sectionHeaderLink}
|
||||
onPress={() => void openExternalUrl(CLI_DOCS_URL)}
|
||||
accessibilityLabel="Open CLI documentation"
|
||||
>
|
||||
CLI docs
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={<ArrowUpRight size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />}
|
||||
textStyle={settingsStyles.sectionHeaderLinkText}
|
||||
style={settingsStyles.sectionHeaderLink}
|
||||
onPress={() => void openExternalUrl(SKILLS_DOCS_URL)}
|
||||
accessibilityLabel="Open skills documentation"
|
||||
>
|
||||
Skills docs
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={settingsStyles.section}>
|
||||
<View style={settingsStyles.sectionHeader}>
|
||||
<Text style={settingsStyles.sectionHeaderTitle}>Integrations</Text>
|
||||
<View style={styles.headerLinks}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={
|
||||
<ArrowUpRight size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
}
|
||||
textStyle={settingsStyles.sectionHeaderLinkText}
|
||||
style={settingsStyles.sectionHeaderLink}
|
||||
onPress={() => void openExternalUrl(CLI_DOCS_URL)}
|
||||
accessibilityLabel="Open CLI documentation"
|
||||
>
|
||||
CLI docs
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={
|
||||
<ArrowUpRight size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
}
|
||||
textStyle={settingsStyles.sectionHeaderLinkText}
|
||||
style={settingsStyles.sectionHeaderLink}
|
||||
onPress={() => void openExternalUrl(SKILLS_DOCS_URL)}
|
||||
accessibilityLabel="Open skills documentation"
|
||||
>
|
||||
Skills docs
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
<SettingsSection title="Integrations" trailing={trailing}>
|
||||
<View style={settingsStyles.card}>
|
||||
<View style={settingsStyles.row}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
@@ -166,7 +163,7 @@ export function IntegrationsSection() {
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||
import { PairDeviceSection } from "@/desktop/components/pair-device-section";
|
||||
|
||||
export interface PairDeviceModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
export function PairDeviceModal({ visible, onClose, testID }: PairDeviceModalProps) {
|
||||
return (
|
||||
<AdaptiveModalSheet
|
||||
title="Pair a device"
|
||||
visible={visible}
|
||||
onClose={onClose}
|
||||
snapPoints={["82%", "94%"]}
|
||||
desktopMaxWidth={640}
|
||||
testID={testID}
|
||||
>
|
||||
<PairDeviceSection />
|
||||
</AdaptiveModalSheet>
|
||||
);
|
||||
}
|
||||
@@ -45,8 +45,7 @@ export function PairDeviceSection() {
|
||||
if (!showSection) return null;
|
||||
|
||||
return (
|
||||
<View style={settingsStyles.section}>
|
||||
<Text style={settingsStyles.sectionTitle}>Pair device</Text>
|
||||
<View style={settingsStyles.section} testID="host-page-pair-device-card">
|
||||
<View style={settingsStyles.card}>
|
||||
{pairingQuery.isPending ? (
|
||||
<View style={styles.centered}>
|
||||
|
||||
Reference in New Issue
Block a user