add icon for documents without thumbnails

This commit is contained in:
m
2026-07-12 21:40:31 +02:00
parent f7d2d05418
commit 6ed51daa6b
4 changed files with 106 additions and 43 deletions
+8 -4
View File
@@ -114,10 +114,14 @@ func (h *FileHandler) Get(c *gin.Context) {
} }
api.Success(c, gin.H{ api.Success(c, gin.H{
"id": file.ID, "id": file.ID,
"name": file.Name, "name": file.Name,
"url": h.urls.GenerateDownloadURL(file.ID), "url": h.urls.GenerateDownloadURL(file.ID),
"size": file.Size, "size": file.Size,
"mimeType": file.MimeType,
"createdAt": file.CreatedAt,
"updatedAt": file.UpdatedAt,
"ocrText": file.OcrText,
}) })
} }
+10 -6
View File
@@ -12,6 +12,7 @@ import {
import { RouteProp, useRoute } from '@react-navigation/native'; import { RouteProp, useRoute } from '@react-navigation/native';
import { useFile, useFileImage } from '../hooks/useFiles'; import { useFile, useFileImage } from '../hooks/useFiles';
import { TagChip } from '../components/TagChip'; import { TagChip } from '../components/TagChip';
import { FileThumbnail } from '../components/FileThumbnail';
const SCREEN_WIDTH = Dimensions.get('window').width; const SCREEN_WIDTH = Dimensions.get('window').width;
@@ -30,14 +31,17 @@ function DetailItem({ fileId }: { fileId: string }) {
return ( return (
<ScrollView style={styles.page} contentContainerStyle={styles.pageContent}> <ScrollView style={styles.page} contentContainerStyle={styles.pageContent}>
<View style={styles.imageContainer}> <View style={styles.imageContainer}>
{imageLoading ? ( {uri ? (
<ActivityIndicator size="large" color="#1976D2" />
) : uri ? (
<Image source={{ uri }} style={styles.image} resizeMode="contain" /> <Image source={{ uri }} style={styles.image} resizeMode="contain" />
) : file ? (
<FileThumbnail
mimeType={file.mimeType ?? 'application/pdf'}
fileName={file.name ?? fileId}
size={SCREEN_WIDTH * 0.6}
isLoading={imageLoading}
/>
) : ( ) : (
<View style={styles.placeholder}> <ActivityIndicator size="large" color="#1976D2" />
<Text style={styles.placeholderText}>PDF</Text>
</View>
)} )}
</View> </View>
+9 -33
View File
@@ -1,5 +1,5 @@
import React, { useState, useMemo, useEffect } from 'react'; import React, { useState, useMemo, useEffect } from 'react';
import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Image, ActivityIndicator, KeyboardAvoidingView, Platform, Keyboard } from 'react-native'; import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, KeyboardAvoidingView, Platform, Keyboard } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
@@ -7,6 +7,7 @@ import { MaterialIcons } from '@expo/vector-icons';
import { useFiles, useFileImage } from '../hooks/useFiles'; import { useFiles, useFileImage } from '../hooks/useFiles';
import { FileItem } from '../types'; import { FileItem } from '../types';
import { SearchBar, SearchFilters } from '../components/SearchBar'; import { SearchBar, SearchFilters } from '../components/SearchBar';
import { FileThumbnail } from '../components/FileThumbnail';
const NUM_COLUMNS = 3; const NUM_COLUMNS = 3;
const SCREEN_WIDTH = Dimensions.get('window').width; const SCREEN_WIDTH = Dimensions.get('window').width;
@@ -57,21 +58,15 @@ function formatDateLabel(key: string): string {
function FileGridItem({ file, onPress }: { file: FileItem; onPress?: () => void }) { function FileGridItem({ file, onPress }: { file: FileItem; onPress?: () => void }) {
const { data, isLoading } = useFileImage(file.id); const { data, isLoading } = useFileImage(file.id);
const uri = data?.data?.url;
return ( return (
<TouchableOpacity style={styles.gridItem} onPress={onPress} activeOpacity={0.7}> <TouchableOpacity style={styles.gridItem} onPress={onPress} activeOpacity={0.7}>
{isLoading ? ( <FileThumbnail
<View style={styles.placeholder}> uri={data?.data?.url}
<ActivityIndicator size="small" color="#1976D2" /> mimeType={file.mimeType}
</View> fileName={file.name}
) : uri ? ( size={ITEM_SIZE}
<Image source={{ uri }} style={styles.thumb} /> isLoading={isLoading}
) : ( />
<View style={styles.placeholder}>
<Text style={styles.placeholderText}>PDF</Text>
</View>
)}
<Text style={styles.fileName} numberOfLines={1}>{file.name}</Text> <Text style={styles.fileName} numberOfLines={1}>{file.name}</Text>
</TouchableOpacity> </TouchableOpacity>
); );
@@ -264,25 +259,6 @@ const styles = StyleSheet.create({
gridItem: { gridItem: {
width: ITEM_SIZE, width: ITEM_SIZE,
}, },
thumb: {
width: ITEM_SIZE,
height: ITEM_SIZE,
borderRadius: 6,
backgroundColor: '#e0e0e0',
},
placeholder: {
width: ITEM_SIZE,
height: ITEM_SIZE,
borderRadius: 6,
backgroundColor: '#e3f2fd',
justifyContent: 'center',
alignItems: 'center',
},
placeholderText: {
fontSize: 12,
color: '#1976D2',
fontWeight: '600',
},
fileName: { fileName: {
fontSize: 11, fontSize: 11,
color: '#666', color: '#666',
+79
View File
@@ -0,0 +1,79 @@
import React from 'react';
import { View, Text, Image, ActivityIndicator, StyleSheet } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import type { ComponentProps } from 'react';
type IconName = ComponentProps<typeof MaterialIcons>['name'];
interface FileTypeInfo {
icon: IconName;
color: string;
bg: string;
}
function getFileInfo(mimeType: string, fileName: string): FileTypeInfo {
if (mimeType.startsWith('image/')) return { icon: 'image', color: '#4CAF50', bg: '#E8F5E9' };
if (mimeType === 'application/pdf') return { icon: 'picture-as-pdf', color: '#E53935', bg: '#FFEBEE' };
if (mimeType.includes('word') || mimeType.includes('document')) return { icon: 'description', color: '#1565C0', bg: '#E3F2FD' };
if (mimeType.includes('spreadsheet') || mimeType.includes('excel') || mimeType.includes('csv')) return { icon: 'table-chart', color: '#2E7D32', bg: '#E8F5E9' };
if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) return { icon: 'slideshow', color: '#E65100', bg: '#FFF3E0' };
if (mimeType.startsWith('text/')) return { icon: 'article', color: '#546E7A', bg: '#ECEFF1' };
if (mimeType.startsWith('video/')) return { icon: 'movie', color: '#6A1B9A', bg: '#F3E5F5' };
if (mimeType.startsWith('audio/')) return { icon: 'audiotrack', color: '#AD1457', bg: '#FCE4EC' };
return { icon: 'insert-drive-file', color: '#757575', bg: '#F5F5F5' };
}
function getExtension(fileName: string): string {
const ext = fileName.split('.').pop();
return ext ? ext.toUpperCase() : '';
}
interface FileThumbnailProps {
uri?: string;
mimeType: string;
fileName: string;
size: number;
isLoading?: boolean;
}
export function FileThumbnail({ uri, mimeType, fileName, size, isLoading }: FileThumbnailProps) {
const info = getFileInfo(mimeType, fileName);
const ext = getExtension(fileName);
if (isLoading) {
return (
<View style={[styles.container, { width: size, height: size, backgroundColor: info.bg }]}>
<ActivityIndicator size="small" color={info.color} />
</View>
);
}
if (uri && mimeType.startsWith('image/')) {
return <Image source={{ uri }} style={[styles.image, { width: size, height: size }]} />;
}
return (
<View style={[styles.container, { width: size, height: size, backgroundColor: info.bg }]}>
<MaterialIcons name={info.icon} size={size * 0.35} color={info.color} />
{ext.length <= 4 && (
<Text style={[styles.ext, { color: info.color }]}>{ext}</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
borderRadius: 6,
justifyContent: 'center',
alignItems: 'center',
gap: 2,
},
image: {
borderRadius: 6,
},
ext: {
fontSize: 11,
fontWeight: '700',
},
});