diff --git a/web/src/lib/locales/en.ts b/web/src/lib/locales/en.ts index 64fad028..79163f2e 100644 --- a/web/src/lib/locales/en.ts +++ b/web/src/lib/locales/en.ts @@ -214,6 +214,7 @@ export default { 'file.page.tab.file': 'File', 'file.page.missingPath': 'No file path provided.', 'file.page.binary': 'This looks like a binary file. It cannot be displayed.', + 'file.page.imagePreviewAlt': 'Image preview for {name}', 'file.page.empty': 'File is empty.', 'file.page.noChanges': 'No changes to display.', 'file.error.readFailed': 'Failed to read file.', diff --git a/web/src/lib/locales/zh-CN.ts b/web/src/lib/locales/zh-CN.ts index 1141a9d6..0f67e4d6 100644 --- a/web/src/lib/locales/zh-CN.ts +++ b/web/src/lib/locales/zh-CN.ts @@ -216,6 +216,7 @@ export default { 'file.page.tab.file': '文件', 'file.page.missingPath': '未提供文件路径。', 'file.page.binary': '该文件看起来是二进制文件,无法显示。', + 'file.page.imagePreviewAlt': '{name} 图片预览', 'file.page.empty': '文件为空。', 'file.page.noChanges': '没有可显示的变更。', 'file.error.readFailed': '读取文件失败。', diff --git a/web/src/routes/sessions/file.tsx b/web/src/routes/sessions/file.tsx index ee731823..ab73fa74 100644 --- a/web/src/routes/sessions/file.tsx +++ b/web/src/routes/sessions/file.tsx @@ -14,6 +14,20 @@ import { useTranslation } from '@/lib/use-translation' import { decodeBase64 } from '@/lib/utils' const MAX_COPYABLE_FILE_BYTES = 1_000_000 +const IMAGE_MIME_BY_EXTENSION: Record = { + apng: 'image/apng', + avif: 'image/avif', + bmp: 'image/bmp', + gif: 'image/gif', + ico: 'image/x-icon', + jpeg: 'image/jpeg', + jpg: 'image/jpeg', + png: 'image/png', + svg: 'image/svg+xml', + tif: 'image/tiff', + tiff: 'image/tiff', + webp: 'image/webp' +} function decodePath(value: string): string { if (!value) return '' @@ -98,6 +112,14 @@ function resolveLanguage(path: string): string | undefined { return langAlias[ext] ?? ext } +function resolveImageMimeType(path: string): string | null { + const parts = path.split('.') + if (parts.length <= 1) return null + const ext = parts[parts.length - 1]?.toLowerCase() + if (!ext) return null + return IMAGE_MIME_BY_EXTENSION[ext] ?? null +} + function getUtf8ByteLength(value: string): number { return new TextEncoder().encode(value).length } @@ -118,6 +140,20 @@ function extractCommandError(result: GitCommandResponse | undefined): string | n return result.error ?? result.stderr ?? 'Failed to load diff' } +function ImagePreview(props: { dataUrl: string; fileName: string; label: string }) { + return ( +
+ {props.label} + {props.fileName} +
+ ) +} + export default function FilePage() { const { api } = useAppContext() const { t } = useTranslation() @@ -131,6 +167,7 @@ export default function FilePage() { const filePath = useMemo(() => decodePath(encodedPath), [encodedPath]) const fileName = filePath.split('/').pop() || filePath || t('file.page.fallbackName') + const imageMimeType = useMemo(() => resolveImageMimeType(filePath), [filePath]) const diffQuery = useQuery({ queryKey: queryKeys.gitFileDiff(sessionId, filePath, staged), @@ -167,9 +204,12 @@ export default function FilePage() { const binaryFile = fileContentResult?.success ? !decodedContentResult.ok || isBinaryContent(decodedContent) : false + const imagePreviewUrl = fileContentResult?.success && fileContentResult.content && imageMimeType + ? `data:${imageMimeType};base64,${fileContentResult.content}` + : null - const language = useMemo(() => resolveLanguage(filePath), [filePath]) - const highlighted = useShikiHighlighter(decodedContent, language) + const language = useMemo(() => imageMimeType ? undefined : resolveLanguage(filePath), [filePath, imageMimeType]) + const highlighted = useShikiHighlighter(imageMimeType ? '' : decodedContent, language) const contentSizeBytes = useMemo( () => (decodedContent ? getUtf8ByteLength(decodedContent) : 0), [decodedContent] @@ -182,6 +222,10 @@ export default function FilePage() { const [displayMode, setDisplayMode] = useState<'diff' | 'file'>('diff') useEffect(() => { + if (imageMimeType) { + setDisplayMode('file') + return + } if (diffSuccess && !diffContent) { setDisplayMode('file') return @@ -189,7 +233,7 @@ export default function FilePage() { if (diffFailed) { setDisplayMode('file') } - }, [diffSuccess, diffFailed, diffContent]) + }, [diffSuccess, diffFailed, diffContent, imageMimeType]) const loading = diffQuery.isLoading || fileQuery.isLoading const fileError = fileContentResult && !fileContentResult.success @@ -266,33 +310,41 @@ export default function FilePage() { ) : fileErrorMessage ? (
{fileErrorMessage}
- ) : binaryFile ? ( -
- {t('file.page.binary')} -
) : displayMode === 'diff' && diffContent ? ( ) : displayMode === 'diff' && diffError ? (
{diffErrorMessage}
) : displayMode === 'file' ? ( - decodedContent ? ( -
- {canCopyContent ? ( - - ) : null} -
-                                    {highlighted ?? decodedContent}
-                                
+ imagePreviewUrl ? ( + + ) : binaryFile ? ( +
+ {t('file.page.binary')}
) : ( -
{t('file.page.empty')}
+ decodedContent ? ( +
+ {canCopyContent ? ( + + ) : null} +
+                                        {highlighted ?? decodedContent}
+                                    
+
+ ) : ( +
{t('file.page.empty')}
+ ) ) ) : (
{t('file.page.noChanges')}