Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions apps/mobile/app/project/[projectId]/viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { ActivityIndicator, ScrollView, View } from "react-native"
import { useLocalSearchParams, useRouter } from "expo-router"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { useColorScheme } from "nativewind"
import * as Clipboard from "expo-clipboard"
import ArrowLeftIcon from "lucide-react-native/dist/esm/icons/arrow-left"
import CheckIcon from "lucide-react-native/dist/esm/icons/check"
import CopyIcon from "lucide-react-native/dist/esm/icons/copy"
import FileQuestionIcon from "lucide-react-native/dist/esm/icons/file-question"
import { Button } from "@/components/ui/button"
import { Text } from "@/components/ui/text"
Expand Down Expand Up @@ -31,6 +34,14 @@ export default function ViewerPage() {
const [truncated, setTruncated] = useState(false)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [copied, setCopied] = useState(false)

const copyFile = useCallback(async () => {
if (!content) return
await Clipboard.setStringAsync(content)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
}, [content])

const binary = useMemo(() => isBinaryPath(filePath), [filePath])
const language = useMemo(() => languageFromPath(filePath), [filePath])
Expand Down Expand Up @@ -83,9 +94,18 @@ export default function ViewerPage() {
<Text className="text-xs text-muted-foreground line-clamp-1">{directory || "/"}</Text>
</View>
{!loading && content !== null && (
<View className="px-2.5 py-1.5 rounded-full bg-accent/60 border border-border/50">
<Text className="text-xs text-muted-foreground uppercase">{language}</Text>
</View>
<Button
variant="ghost"
className="w-10 h-10"
onPress={copyFile}
accessibilityLabel={copied ? "File copied" : "Copy file"}
>
{copied ? (
<CheckIcon size={20} color={t.foreground} />
) : (
<CopyIcon size={20} color={t.foreground} />
)}
</Button>
)}
</View>

Expand Down Expand Up @@ -114,7 +134,7 @@ export default function ViewerPage() {
className="flex-1"
contentContainerStyle={{ padding: 12, paddingBottom: insets.bottom + 20 }}
>
<CodeBlock text={content ?? ""} language={language} theme={theme} />
<CodeBlock text={content ?? ""} language={language} theme={theme} header={false} lineNumbers />
{truncated && (
<Text className="text-xs text-muted-foreground text-center mt-3">
Preview limited to the first {MAX_LINES.toLocaleString()} lines.
Expand Down
54 changes: 53 additions & 1 deletion apps/mobile/components/code-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ interface CodeBlockProps {
text: string
language?: string
theme: "light" | "dark"
header?: boolean
lineNumbers?: boolean
}

export function CodeBlock({ text, language, theme }: CodeBlockProps) {
export function CodeBlock({ text, language, theme, header = true, lineNumbers = false }: CodeBlockProps) {
const [copied, setCopied] = useState(false)
ensurePrismLanguages()
const prismTheme = theme === "dark" ? themes.oneDark : themes.github
Expand All @@ -36,6 +38,56 @@ export function CodeBlock({ text, language, theme }: CodeBlockProps) {
setTimeout(() => setCopied(false), 1500)
}

if (!header) {
return (
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ padding: 4 }}>
<Highlight code={code} language={normalizedLanguage} theme={prismTheme}>
{({ tokens, getTokenProps }) => (
<View>
{tokens.map((line, lineIndex) => (
<View key={lineIndex} className="flex-row">
{lineNumbers && (
<Text
style={[
codeTextStyle,
{
width: 32,
marginRight: 12,
textAlign: "right",
color: THEME[theme].mutedForeground,
opacity: 0.6,
},
]}
>
{lineIndex + 1}
</Text>
)}
<View className="flex-row flex-shrink-0">
{line.map((token, tokenIndex) => {
const tokenProps = getTokenProps({ token })
return (
<Text
key={tokenIndex}
style={[
codeTextStyle,
{ color: prismTheme.plain.color },
tokenProps.style as TextStyle | undefined,
]}
>
{tokenProps.children}
</Text>
)
})}
</View>
</View>
))}
</View>
)}
</Highlight>
</ScrollView>
)
}

return (
<View
className="overflow-hidden rounded-lg border border-border/60"
Expand Down
11 changes: 10 additions & 1 deletion apps/mobile/lib/file-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,16 @@ export async function readFileContent(url: string, token: string, path: string):
},
})
if (!res.ok) throw new Error(`Failed to read file (${res.status})`)
const text = await res.text()
const raw = await res.text()
let text = raw
try {
const data = JSON.parse(raw) as { content?: unknown; text?: unknown; data?: unknown }
if (typeof data.content === "string") text = data.content
else if (typeof data.text === "string") text = data.text
else if (typeof data.data === "string") text = data.data
} catch {
text = raw
}
return text.length > MAX_CONTENT_BYTES ? text.slice(0, MAX_CONTENT_BYTES) : text
}

Expand Down
Loading