feat(app): inline desktop file explorer preview
This commit is contained in:
@@ -16,18 +16,17 @@ import {
|
|||||||
View,
|
View,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { Fonts } from "@/constants/theme";
|
import { Fonts } from "@/constants/theme";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import {
|
import {
|
||||||
BottomSheetModal,
|
BottomSheetModal,
|
||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
BottomSheetBackdrop,
|
BottomSheetBackdrop,
|
||||||
BottomSheetView,
|
|
||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import {
|
import {
|
||||||
|
ArrowLeft,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Download,
|
|
||||||
File,
|
File,
|
||||||
FileText,
|
FileText,
|
||||||
Folder,
|
Folder,
|
||||||
@@ -64,6 +63,8 @@ export function FileExplorerPane({
|
|||||||
agentId,
|
agentId,
|
||||||
}: FileExplorerPaneProps) {
|
}: FileExplorerPaneProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
|
const isMobile =
|
||||||
|
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
const { connectionStates } = useDaemonConnections();
|
const { connectionStates } = useDaemonConnections();
|
||||||
const daemonProfile = connectionStates.get(serverId)?.daemon;
|
const daemonProfile = connectionStates.get(serverId)?.daemon;
|
||||||
|
|
||||||
@@ -335,12 +336,15 @@ export function FileExplorerPane({
|
|||||||
|
|
||||||
// Open/close preview sheet based on selection
|
// Open/close preview sheet based on selection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isMobile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (selectedEntryPath) {
|
if (selectedEntryPath) {
|
||||||
previewSheetRef.current?.present();
|
previewSheetRef.current?.present();
|
||||||
} else {
|
} else {
|
||||||
previewSheetRef.current?.dismiss();
|
previewSheetRef.current?.dismiss();
|
||||||
}
|
}
|
||||||
}, [selectedEntryPath]);
|
}, [isMobile, selectedEntryPath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldShowPreview) {
|
if (shouldShowPreview) {
|
||||||
@@ -484,6 +488,12 @@ export function FileExplorerPane({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const inlinePreviewTitle = useMemo(() => {
|
||||||
|
return selectedEntryPath?.split("/").pop() ?? "Preview";
|
||||||
|
}, [selectedEntryPath]);
|
||||||
|
|
||||||
|
const shouldShowInlinePreview = !isMobile && Boolean(selectedEntryPath);
|
||||||
|
|
||||||
const handleRetryDirectory = useCallback(() => {
|
const handleRetryDirectory = useCallback(() => {
|
||||||
if (!agentId || !requestDirectoryListing) {
|
if (!agentId || !requestDirectoryListing) {
|
||||||
return;
|
return;
|
||||||
@@ -693,7 +703,68 @@ export function FileExplorerPane({
|
|||||||
<View style={styles.container} onLayout={handleContainerLayout}>
|
<View style={styles.container} onLayout={handleContainerLayout}>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<View style={styles.listSection}>
|
<View style={styles.listSection}>
|
||||||
{error ? (
|
{shouldShowInlinePreview ? (
|
||||||
|
<View style={styles.inlinePreviewContainer}>
|
||||||
|
<View style={styles.inlinePreviewHeader}>
|
||||||
|
<Pressable
|
||||||
|
onPress={handleClosePreviewSheet}
|
||||||
|
style={styles.inlinePreviewBackButton}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Back to files"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={18} color={theme.colors.foregroundMuted} />
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
<View style={styles.inlinePreviewTitleContainer}>
|
||||||
|
<Text style={styles.inlinePreviewTitle} numberOfLines={1}>
|
||||||
|
{inlinePreviewTitle}
|
||||||
|
</Text>
|
||||||
|
{selectedEntryPath ? (
|
||||||
|
<Text style={styles.inlinePreviewSubtitle} numberOfLines={1}>
|
||||||
|
{selectedEntryPath}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{isPreviewLoading && !preview ? (
|
||||||
|
<View style={styles.sheetCenterState}>
|
||||||
|
<ActivityIndicator size="small" />
|
||||||
|
<Text style={styles.loadingText}>Loading file...</Text>
|
||||||
|
</View>
|
||||||
|
) : !preview ? (
|
||||||
|
<View style={styles.sheetCenterState}>
|
||||||
|
<Text style={styles.emptyText}>No preview available yet</Text>
|
||||||
|
</View>
|
||||||
|
) : preview.kind === "text" ? (
|
||||||
|
<ScrollView
|
||||||
|
style={styles.sheetContent}
|
||||||
|
contentContainerStyle={styles.sheetScrollContent}
|
||||||
|
>
|
||||||
|
<ScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator>
|
||||||
|
<Text style={styles.codeText}>{preview.content}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
</ScrollView>
|
||||||
|
) : preview.kind === "image" && preview.content ? (
|
||||||
|
<ScrollView contentContainerStyle={styles.sheetImageScrollContent}>
|
||||||
|
<RNImage
|
||||||
|
source={{
|
||||||
|
uri: `data:${preview.mimeType ?? "image/png"};base64,${preview.content}`,
|
||||||
|
}}
|
||||||
|
style={styles.sheetImage}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
</ScrollView>
|
||||||
|
) : (
|
||||||
|
<View style={styles.sheetCenterState}>
|
||||||
|
<Text style={styles.emptyText}>Binary preview unavailable</Text>
|
||||||
|
<Text style={styles.entryMenuMeta}>
|
||||||
|
{formatFileSize({ size: preview.size })}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
) : error ? (
|
||||||
<View style={styles.centerState}>
|
<View style={styles.centerState}>
|
||||||
<Text style={styles.errorText}>{error}</Text>
|
<Text style={styles.errorText}>{error}</Text>
|
||||||
<View style={styles.errorActions}>
|
<View style={styles.errorActions}>
|
||||||
@@ -809,64 +880,66 @@ export function FileExplorerPane({
|
|||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<BottomSheetModal
|
{isMobile ? (
|
||||||
ref={previewSheetRef}
|
<BottomSheetModal
|
||||||
snapPoints={previewSnapPoints}
|
ref={previewSheetRef}
|
||||||
index={0}
|
snapPoints={previewSnapPoints}
|
||||||
enableDynamicSizing={false}
|
index={0}
|
||||||
onChange={handlePreviewSheetChange}
|
enableDynamicSizing={false}
|
||||||
backdropComponent={renderPreviewBackdrop}
|
onChange={handlePreviewSheetChange}
|
||||||
enablePanDownToClose
|
backdropComponent={renderPreviewBackdrop}
|
||||||
backgroundStyle={styles.sheetBackground}
|
enablePanDownToClose
|
||||||
handleIndicatorStyle={styles.handleIndicator}
|
backgroundStyle={styles.sheetBackground}
|
||||||
>
|
handleIndicatorStyle={styles.handleIndicator}
|
||||||
<View style={styles.sheetHeader}>
|
>
|
||||||
<Text style={styles.sheetTitle} numberOfLines={1}>
|
<View style={styles.sheetHeader}>
|
||||||
{selectedEntryPath?.split("/").pop() ?? "Preview"}
|
<Text style={styles.sheetTitle} numberOfLines={1}>
|
||||||
</Text>
|
{selectedEntryPath?.split("/").pop() ?? "Preview"}
|
||||||
<Pressable onPress={handleClosePreviewSheet} style={styles.sheetCloseButton}>
|
|
||||||
<X size={20} color={theme.colors.foregroundMuted} />
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
{isPreviewLoading && !preview ? (
|
|
||||||
<View style={styles.sheetCenterState}>
|
|
||||||
<ActivityIndicator size="small" />
|
|
||||||
<Text style={styles.loadingText}>Loading file...</Text>
|
|
||||||
</View>
|
|
||||||
) : !preview ? (
|
|
||||||
<View style={styles.sheetCenterState}>
|
|
||||||
<Text style={styles.emptyText}>No preview available yet</Text>
|
|
||||||
</View>
|
|
||||||
) : preview.kind === "text" ? (
|
|
||||||
<BottomSheetScrollView
|
|
||||||
style={styles.sheetContent}
|
|
||||||
contentContainerStyle={styles.sheetScrollContent}
|
|
||||||
>
|
|
||||||
<ScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator>
|
|
||||||
<Text style={styles.codeText}>{preview.content}</Text>
|
|
||||||
</ScrollView>
|
|
||||||
</BottomSheetScrollView>
|
|
||||||
) : preview.kind === "image" && preview.content ? (
|
|
||||||
<BottomSheetScrollView
|
|
||||||
contentContainerStyle={styles.sheetImageScrollContent}
|
|
||||||
>
|
|
||||||
<RNImage
|
|
||||||
source={{
|
|
||||||
uri: `data:${preview.mimeType ?? "image/png"};base64,${preview.content}`,
|
|
||||||
}}
|
|
||||||
style={styles.sheetImage}
|
|
||||||
resizeMode="contain"
|
|
||||||
/>
|
|
||||||
</BottomSheetScrollView>
|
|
||||||
) : (
|
|
||||||
<View style={styles.sheetCenterState}>
|
|
||||||
<Text style={styles.emptyText}>Binary preview unavailable</Text>
|
|
||||||
<Text style={styles.entryMenuMeta}>
|
|
||||||
{formatFileSize({ size: preview.size })}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
<Pressable onPress={handleClosePreviewSheet} style={styles.sheetCloseButton}>
|
||||||
|
<X size={20} color={theme.colors.foregroundMuted} />
|
||||||
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)}
|
{isPreviewLoading && !preview ? (
|
||||||
</BottomSheetModal>
|
<View style={styles.sheetCenterState}>
|
||||||
|
<ActivityIndicator size="small" />
|
||||||
|
<Text style={styles.loadingText}>Loading file...</Text>
|
||||||
|
</View>
|
||||||
|
) : !preview ? (
|
||||||
|
<View style={styles.sheetCenterState}>
|
||||||
|
<Text style={styles.emptyText}>No preview available yet</Text>
|
||||||
|
</View>
|
||||||
|
) : preview.kind === "text" ? (
|
||||||
|
<BottomSheetScrollView
|
||||||
|
style={styles.sheetContent}
|
||||||
|
contentContainerStyle={styles.sheetScrollContent}
|
||||||
|
>
|
||||||
|
<ScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator>
|
||||||
|
<Text style={styles.codeText}>{preview.content}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
</BottomSheetScrollView>
|
||||||
|
) : preview.kind === "image" && preview.content ? (
|
||||||
|
<BottomSheetScrollView
|
||||||
|
contentContainerStyle={styles.sheetImageScrollContent}
|
||||||
|
>
|
||||||
|
<RNImage
|
||||||
|
source={{
|
||||||
|
uri: `data:${preview.mimeType ?? "image/png"};base64,${preview.content}`,
|
||||||
|
}}
|
||||||
|
style={styles.sheetImage}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
</BottomSheetScrollView>
|
||||||
|
) : (
|
||||||
|
<View style={styles.sheetCenterState}>
|
||||||
|
<Text style={styles.emptyText}>Binary preview unavailable</Text>
|
||||||
|
<Text style={styles.entryMenuMeta}>
|
||||||
|
{formatFileSize({ size: preview.size })}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</BottomSheetModal>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1220,6 +1293,40 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
},
|
},
|
||||||
|
inlinePreviewContainer: {
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: theme.borderRadius.lg,
|
||||||
|
borderWidth: theme.borderWidth[1],
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
backgroundColor: theme.colors.surface2,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
inlinePreviewHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[1],
|
||||||
|
paddingHorizontal: theme.spacing[3],
|
||||||
|
paddingVertical: theme.spacing[2],
|
||||||
|
borderBottomWidth: theme.borderWidth[1],
|
||||||
|
borderBottomColor: theme.colors.border,
|
||||||
|
},
|
||||||
|
inlinePreviewBackButton: {
|
||||||
|
padding: theme.spacing[1],
|
||||||
|
},
|
||||||
|
inlinePreviewTitleContainer: {
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
},
|
||||||
|
inlinePreviewTitle: {
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
fontSize: theme.fontSize.base,
|
||||||
|
fontWeight: theme.fontWeight.semibold,
|
||||||
|
},
|
||||||
|
inlinePreviewSubtitle: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
fontFamily: Fonts.mono,
|
||||||
|
},
|
||||||
sheetBackground: {
|
sheetBackground: {
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user