import { useCallback, useMemo } from "react"; import { 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 { useTranslation } from "react-i18next"; import { ProjectIconView } from "@/components/project-icon-view"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { useProjects, type ProjectHostError } from "@/hooks/use-projects"; import { useProjectIconDataByProjectKey } from "@/projects/project-icons"; import { settingsStyles } from "@/styles/settings"; import { buildProjectSettingsRoute } from "@/utils/host-routes"; import type { ProjectSummary } from "@/utils/projects"; interface ProjectsScreenProps { view: { kind: "projects" } | { kind: "project"; projectKey: string }; } export default function ProjectsScreen({ view }: ProjectsScreenProps) { const { t } = useTranslation(); const { projects, hostErrors, isLoading } = useProjects(); const selectedProjectKey = view.kind === "project" ? view.projectKey : null; const iconTargets = useMemo( () => projects.flatMap((project) => { const host = project.hosts[0]; if (!host) return []; return [ { serverId: host.serverId, projectKey: project.projectKey, iconWorkingDir: host.repoRoot, }, ]; }), [projects], ); const iconDataByProjectKey = useProjectIconDataByProjectKey({ projects: iconTargets, }); if (isLoading && projects.length === 0) { return ( ); } if (projects.length === 0) { return ( {t("sidebar.project.empty.title")} ); } return ( {hostErrors.length > 0 ? : null} {projects.map((project, index) => ( ))} ); } function HostErrorsBanner({ errors }: { errors: ProjectHostError[] }) { const { t } = useTranslation(); return ( {errors.map((error) => ( {t("settings.projectList.hostLoadFailed", { hostName: error.serverName, message: error.message, })} ))} ); } interface ProjectRowProps { project: ProjectSummary; isFirst: boolean; isSelected: boolean; iconDataUri: string | null; } function ProjectRow({ project, isFirst, isSelected, iconDataUri }: ProjectRowProps) { const { t } = useTranslation(); const { theme } = useUnistyles(); const { projectKey, projectName } = project; 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({ iconDataUri, projectName, projectKey, }: { iconDataUri: string | null; projectName: string; projectKey: string; }) { const initial = projectName.trim().charAt(0).toUpperCase() || "?"; return ( ); } 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, alignItems: "center", justifyContent: "center", }, iconFallbackText: { fontSize: theme.fontSize.xs, }, spinnerColor: { color: theme.colors.foregroundMuted, }, }));