import { useCallback, useMemo } from "react"; import { Image, Pressable, Text, View, type PressableStateCallbackType } from "react-native"; import { router } from "expo-router"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { ChevronRight } from "lucide-react-native"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { useProjectIconQuery } from "@/hooks/use-project-icon-query"; import { useProjects, type ProjectHostError } from "@/hooks/use-projects"; import { settingsStyles } from "@/styles/settings"; import { buildProjectSettingsRoute } from "@/utils/host-routes"; import type { ProjectHostEntry, ProjectSummary } from "@/utils/projects"; interface ProjectsScreenProps { view: { kind: "projects" } | { kind: "project"; projectKey: string }; } export default function ProjectsScreen({ view }: ProjectsScreenProps) { const { projects, hostErrors, isLoading } = useProjects(); const selectedProjectKey = view.kind === "project" ? view.projectKey : null; if (isLoading && projects.length === 0) { return ( ); } if (projects.length === 0) { return ( No projects yet ); } return ( {hostErrors.length > 0 ? : null} {projects.map((project, index) => ( ))} ); } function HostErrorsBanner({ errors }: { errors: ProjectHostError[] }) { return ( {errors.map((error) => ( {`Couldn't load projects from host ${error.serverName}: ${error.message}`} ))} ); } interface ProjectRowProps { project: ProjectSummary; isFirst: boolean; isSelected: boolean; } function ProjectRow({ project, isFirst, isSelected }: ProjectRowProps) { const { theme } = useUnistyles(); const { hosts, projectKey, projectName } = project; const leadingHost = hosts[0]; const handleNavigate = useCallback(() => { router.navigate(buildProjectSettingsRoute(projectKey)); }, [projectKey]); const rowStyle = useCallback( ({ pressed, hovered }: PressableStateCallbackType & { hovered?: boolean }) => [ settingsStyles.row, !isFirst && settingsStyles.rowBorder, styles.row, isSelected && styles.rowSelected, hovered && !isSelected && styles.rowHovered, pressed && styles.rowPressed, ], [isFirst, isSelected], ); return ( {projectName} ); } function ProjectRowIcon({ host, projectName, }: { host: ProjectHostEntry | undefined; projectName: string; }) { const initial = projectName.trim().charAt(0).toUpperCase() || "?"; const { icon } = useProjectIconQuery({ serverId: host?.serverId ?? "", cwd: host?.repoRoot ?? "", }); const iconDataUri = icon && icon.data && icon.mimeType ? `data:${icon.mimeType};base64,${icon.data}` : null; const imageSource = useMemo(() => ({ uri: iconDataUri ?? "" }), [iconDataUri]); if (iconDataUri) { return ; } return ( {initial} ); } const styles = StyleSheet.create((theme) => ({ centered: { flex: 1, alignItems: "center", justifyContent: "center", padding: theme.spacing[6], }, emptyText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, errorsBanner: { borderWidth: 1, borderColor: theme.colors.border, borderRadius: theme.borderRadius.lg, padding: theme.spacing[3], marginBottom: theme.spacing[3], gap: theme.spacing[1], }, errorsBannerText: { color: theme.colors.palette.red[300], fontSize: theme.fontSize.xs, }, row: { gap: theme.spacing[3], }, rowMain: { flex: 1, minWidth: 0, flexDirection: "row", alignItems: "center", gap: theme.spacing[3], }, rowHovered: { backgroundColor: theme.colors.surface2, }, rowPressed: { backgroundColor: theme.colors.surface3, }, rowSelected: { backgroundColor: theme.colors.surfaceSidebarHover, }, leading: { width: 16, height: 16, alignItems: "center", justifyContent: "center", }, iconImage: { width: 16, height: 16, borderRadius: theme.borderRadius.sm, }, iconFallback: { width: 16, height: 16, borderRadius: theme.borderRadius.sm, backgroundColor: theme.colors.surface2, alignItems: "center", justifyContent: "center", }, iconFallbackText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, }, spinnerColor: { color: theme.colors.foregroundMuted, }, }));