Inital commit
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
Pressable,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useSocket } from "@/contexts/SocketContext";
|
||||
import { groupByProject, projectMeta, shortenPath } from "@/lib/session-utils";
|
||||
import { colors, monoFont, tintFor } from "@/lib/theme";
|
||||
|
||||
function ProjectRow({
|
||||
name,
|
||||
path,
|
||||
meta,
|
||||
active,
|
||||
isLast,
|
||||
onPress,
|
||||
}: {
|
||||
name: string;
|
||||
path: string;
|
||||
meta: string;
|
||||
active: boolean;
|
||||
isLast: boolean;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const tint = tintFor(name);
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={[styles.tile, { backgroundColor: tint.bg }]}>
|
||||
<Text style={[styles.tileLetter, { color: tint.fg }]}>
|
||||
{name.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
{active ? <View style={styles.tileActiveDot} /> : null}
|
||||
</View>
|
||||
<View style={[styles.rowMain, !isLast && styles.rowMainDivider]}>
|
||||
<View style={styles.rowText}>
|
||||
<Text style={styles.rowTitle} numberOfLines={1}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text style={styles.rowPath} numberOfLines={1}>
|
||||
{path}
|
||||
</Text>
|
||||
<Text style={styles.rowMeta} numberOfLines={1}>
|
||||
{meta}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<View style={styles.empty}>
|
||||
<View style={styles.emptyIcon}>
|
||||
<Ionicons name="folder-open-outline" size={22} color={colors.textMuted} />
|
||||
</View>
|
||||
<Text style={styles.emptyTitle}>No projects yet</Text>
|
||||
<Text style={styles.emptyHint}>
|
||||
Add a folder from your computer to start chatting with Pi.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SessionsScreen() {
|
||||
const {
|
||||
host,
|
||||
port,
|
||||
connected,
|
||||
error,
|
||||
persistedSessions,
|
||||
liveRuntimes,
|
||||
savedProjects,
|
||||
refreshSessions,
|
||||
disconnect,
|
||||
} = useSocket();
|
||||
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const projects = useMemo(
|
||||
() => groupByProject(savedProjects, liveRuntimes, persistedSessions),
|
||||
[savedProjects, liveRuntimes, persistedSessions],
|
||||
);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
refreshSessions();
|
||||
setTimeout(() => setRefreshing(false), 600);
|
||||
}, [refreshSessions]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (connected) {
|
||||
refreshSessions();
|
||||
}
|
||||
}, [connected, refreshSessions]),
|
||||
);
|
||||
|
||||
const openProject = (cwd: string) => {
|
||||
router.push({ pathname: "/project", params: { cwd } });
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
disconnect();
|
||||
router.replace("/");
|
||||
};
|
||||
|
||||
if (!connected) {
|
||||
return (
|
||||
<View style={styles.disconnected}>
|
||||
<View style={styles.emptyIcon}>
|
||||
<Ionicons name="cloud-offline-outline" size={22} color={colors.textMuted} />
|
||||
</View>
|
||||
<Text style={styles.disconnectedTitle}>Not connected</Text>
|
||||
<Text style={styles.disconnectedHint}>
|
||||
Connect to your computer to see your projects.
|
||||
</Text>
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.secondaryButton, pressed && styles.pressedDim]}
|
||||
onPress={() => router.replace("/")}
|
||||
>
|
||||
<Text style={styles.secondaryButtonText}>Connect</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{error ? (
|
||||
<View style={styles.errorBanner}>
|
||||
<Text style={styles.errorBannerText}>{error}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scroll}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={() => void handleRefresh()}
|
||||
tintColor={colors.text}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<View style={styles.statusRow}>
|
||||
<View style={styles.statusLeft}>
|
||||
<View style={styles.connectionDot} />
|
||||
<Text style={styles.connectionText} numberOfLines={1}>
|
||||
{host}:{port}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={handleDisconnect}
|
||||
hitSlop={10}
|
||||
style={({ pressed }) => pressed && styles.pressedDim}
|
||||
>
|
||||
<Text style={styles.disconnectText}>Disconnect</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.addButton, pressed && styles.pressedDim]}
|
||||
onPress={() => router.push("/picker")}
|
||||
>
|
||||
<View style={styles.addIcon}>
|
||||
<Ionicons name="add" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.addText}>
|
||||
<Text style={styles.addTitle}>Add project</Text>
|
||||
<Text style={styles.addHint}>Choose a folder on your computer</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={16} color={colors.text} />
|
||||
</Pressable>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>
|
||||
Projects{projects.length > 0 ? ` · ${projects.length}` : ""}
|
||||
</Text>
|
||||
{projects.length === 0 ? (
|
||||
<EmptyState />
|
||||
) : (
|
||||
<View style={styles.listCard}>
|
||||
{projects.map((project, index) => (
|
||||
<ProjectRow
|
||||
key={project.cwd || "(unknown)"}
|
||||
name={project.name}
|
||||
path={shortenPath(project.cwd) || "Unknown location"}
|
||||
meta={projectMeta(project)}
|
||||
active={project.activeCount > 0}
|
||||
isLast={index === projects.length - 1}
|
||||
onPress={() => openProject(project.cwd)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scroll: {
|
||||
padding: 16,
|
||||
gap: 16,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
pressedDim: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
statusRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
statusLeft: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
flexShrink: 1,
|
||||
},
|
||||
connectionDot: {
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.success,
|
||||
},
|
||||
connectionText: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontFamily: monoFont,
|
||||
},
|
||||
disconnectText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
addButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 14,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderSubtle,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
addIcon: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 10,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.12)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
addText: {
|
||||
flex: 1,
|
||||
gap: 1,
|
||||
},
|
||||
addTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
},
|
||||
addHint: {
|
||||
color: colors.text,
|
||||
fontSize: 13,
|
||||
},
|
||||
section: {
|
||||
gap: 10,
|
||||
marginTop: 4,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 12,
|
||||
fontWeight: "700",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
fontFamily: monoFont,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
listCard: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderSubtle,
|
||||
overflow: "hidden",
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
paddingLeft: 14,
|
||||
},
|
||||
rowPressed: {
|
||||
backgroundColor: colors.surfaceDeep,
|
||||
},
|
||||
tile: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 12,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
tileLetter: {
|
||||
fontSize: 17,
|
||||
fontWeight: "700",
|
||||
},
|
||||
tileActiveDot: {
|
||||
position: "absolute",
|
||||
right: -2,
|
||||
top: -2,
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.success,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.surface,
|
||||
},
|
||||
rowMain: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
paddingVertical: 13,
|
||||
paddingRight: 14,
|
||||
},
|
||||
rowMainDivider: {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
rowText: {
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
},
|
||||
rowTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
rowPath: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontFamily: monoFont,
|
||||
},
|
||||
rowMeta: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 12,
|
||||
},
|
||||
empty: {
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
paddingVertical: 36,
|
||||
paddingHorizontal: 24,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderSubtle,
|
||||
},
|
||||
emptyIcon: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: colors.surfaceDeep,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 6,
|
||||
},
|
||||
emptyTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
},
|
||||
emptyHint: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 13,
|
||||
lineHeight: 19,
|
||||
textAlign: "center",
|
||||
},
|
||||
secondaryButton: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
alignItems: "center",
|
||||
marginTop: 8,
|
||||
},
|
||||
secondaryButtonText: {
|
||||
color: colors.text,
|
||||
fontWeight: "600",
|
||||
fontSize: 15,
|
||||
},
|
||||
disconnected: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
padding: 32,
|
||||
},
|
||||
disconnectedTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
},
|
||||
disconnectedHint: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 15,
|
||||
textAlign: "center",
|
||||
lineHeight: 22,
|
||||
},
|
||||
errorBanner: {
|
||||
backgroundColor: "#450a0a",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
errorBannerText: {
|
||||
color: colors.errorMuted,
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user