add sync status

This commit is contained in:
m
2026-08-28 23:34:35 +02:00
parent bb78d6ac95
commit ae8931ee21
5 changed files with 107 additions and 13 deletions
+40 -3
View File
@@ -86,6 +86,7 @@ export function HomeScreen() {
const debouncedSearch = useDebounce(searchQuery, 250);
const [filters, setFilters] = useState<SearchFilters>({ name: true, ocrText: true });
const [sort, setSort] = useState<SortState>({ key: 'date', direction: 'desc' });
const listRef = useRef<FlatList<UnifiedFileItem>>(null);
const [keyboardOpen, setKeyboardOpen] = useState(false);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [tagModalVisible, setTagModalVisible] = useState(false);
@@ -106,14 +107,34 @@ export function HomeScreen() {
const { tasks: uploadTasks } = useUploadQueue();
useAutoSync();
const loadingMoreRef = useRef(false);
const listLayoutHeightRef = useRef(0);
const listContentHeightRef = useRef(0);
const loadMore = useCallback(() => {
if (isFetching) return;
if (loadingMoreRef.current) return;
const total = data?.meta?.total ?? 0;
const loaded = data?.data?.length ?? 0;
if (loaded < total) {
loadingMoreRef.current = true;
setPage((p) => p + 1);
}
}, [isFetching, data?.meta?.total, data?.data?.length]);
}, [data?.meta?.total, data?.data?.length]);
useEffect(() => {
if (!isFetching) {
loadingMoreRef.current = false;
}
}, [isFetching]);
const handleEndReached = useCallback(() => {
if (listLayoutHeightRef.current > 0 &&
listContentHeightRef.current > 0 &&
listContentHeightRef.current <= listLayoutHeightRef.current) {
return;
}
loadMore();
}, [loadMore]);
const totalFiles = data?.meta?.total ?? 0;
const loadedFiles = data?.data?.length ?? 0;
@@ -441,13 +462,29 @@ export function HomeScreen() {
)}
<View style={styles.listWrapper}>
<FlatList
ref={listRef}
data={sortedFiles}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
onEndReached={loadMore}
onLayout={(e) => {
listLayoutHeightRef.current = e.nativeEvent.layout.height;
}}
onContentSizeChange={(w, h) => {
listContentHeightRef.current = h;
}}
onEndReached={handleEndReached}
onEndReachedThreshold={0.5}
onScrollToIndexFailed={({ index, averageItemLength }) => {
listRef.current?.scrollToOffset({
offset: Math.max(0, averageItemLength * index),
animated: true,
});
setTimeout(() => {
listRef.current?.scrollToIndex({ index, animated: true, viewPosition: 0 });
}, 150);
}}
ListFooterComponent={
hasMore ? (
<TouchableOpacity style={styles.loadMoreBtn} onPress={loadMore} disabled={isFetching}>
+12 -2
View File
@@ -41,7 +41,7 @@ function getExtension(fileName: string): string {
return ext ? ext.toUpperCase() : '';
}
export function FileCard({ file, onPress, onLongPress, selected }: FileCardProps) {
export const FileCard = React.memo(function FileCard({ file, onPress, onLongPress, selected }: FileCardProps) {
const info = getFileInfo(file.mimeType, file.name);
const ext = getExtension(file.name);
@@ -108,6 +108,12 @@ export function FileCard({ file, onPress, onLongPress, selected }: FileCardProps
)}
</View>
{file.syncStatus && !isUploading && (
<View style={styles.statusRow}>
<SyncStatusBadge status={file.syncStatus} showLabel />
</View>
)}
{isUploading ? (
<View style={styles.uploadRow}>
<MaterialIcons name="cloud-upload" size={14} color="#1976D2" />
@@ -131,7 +137,7 @@ export function FileCard({ file, onPress, onLongPress, selected }: FileCardProps
</View>
</TouchableOpacity>
);
}
});
const styles = StyleSheet.create({
container: {
@@ -216,6 +222,10 @@ const styles = StyleSheet.create({
fontSize: 12,
color: '#999',
},
statusRow: {
flexDirection: 'row',
marginBottom: 6,
},
preview: {
fontSize: 13,
color: '#666',
+31 -8
View File
@@ -1,25 +1,35 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { View, Text, StyleSheet } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { SyncStatus } from '../types';
interface SyncStatusBadgeProps {
status: SyncStatus;
size?: number;
showLabel?: boolean;
}
const STATUS_CONFIG: Record<SyncStatus, { icon: string; color: string; bg: string }> = {
local: { icon: 'phone-android', color: '#757575', bg: 'rgba(245,245,245,0.9)' },
syncing: { icon: 'sync', color: '#FF9800', bg: 'rgba(255,243,224,0.9)' },
synced: { icon: 'sync', color: '#4CAF50', bg: 'rgba(232,245,233,0.9)' },
cloud: { icon: 'cloud', color: '#1976D2', bg: 'rgba(227,242,253,0.9)' },
conflict: { icon: 'warning', color: '#E53935', bg: 'rgba(255,235,238,0.9)' },
const STATUS_CONFIG: Record<SyncStatus, { icon: string; color: string; bg: string; label: string }> = {
local: { icon: 'phone-android', color: '#757575', bg: '#F5F5F5', label: 'Local' },
syncing: { icon: 'sync', color: '#FF9800', bg: '#FFF3E0', label: 'Sync...' },
synced: { icon: 'sync', color: '#4CAF50', bg: '#E8F5E9', label: 'Les deux' },
cloud: { icon: 'cloud', color: '#1976D2', bg: '#E3F2FD', label: 'Cloud' },
conflict: { icon: 'warning', color: '#E53935', bg: '#FFEBEE', label: 'Conflit' },
};
export function SyncStatusBadge({ status, size = 16 }: SyncStatusBadgeProps) {
export function SyncStatusBadge({ status, size = 16, showLabel }: SyncStatusBadgeProps) {
const config = STATUS_CONFIG[status];
const iconSize = Math.round(size * 0.7);
if (showLabel) {
return (
<View style={[styles.pill, { backgroundColor: config.bg }]}>
<MaterialIcons name={config.icon as any} size={12} color={config.color} />
<Text style={[styles.pillText, { color: config.color }]}>{config.label}</Text>
</View>
);
}
return (
<View style={[styles.badge, { width: size, height: size, borderRadius: size / 2, backgroundColor: config.bg }]}>
<MaterialIcons name={config.icon as any} size={iconSize} color={config.color} />
@@ -37,4 +47,17 @@ const styles = StyleSheet.create({
borderWidth: 0.5,
borderColor: 'rgba(0,0,0,0.1)',
},
pill: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'flex-start',
gap: 4,
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 10,
},
pillText: {
fontSize: 12,
fontWeight: '600',
},
});
+22
View File
@@ -19,8 +19,10 @@
"drizzle-orm": "^0.45.2",
"expo": "~57.0.8",
"expo-background-task": "~57.0.6",
"expo-blur": "~57.0.2",
"expo-document-picker": "~57.0.1",
"expo-file-system": "~57.0.1",
"expo-haptics": "~57.0.2",
"expo-image": "~57.0.1",
"expo-image-manipulator": "~57.0.14",
"expo-print": "~57.0.1",
@@ -4321,6 +4323,17 @@
"expo": "*"
}
},
"node_modules/expo-blur": {
"version": "57.0.2",
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-57.0.2.tgz",
"integrity": "sha512-Aoud8H8lmlNkbRufyvRLefmGFELdBf1n5Te/Xm+Zx8ORINH+aXL+gKb5mbftFSha860+I7pMArz77TBYz8HDVg==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-document-picker": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-57.0.1.tgz",
@@ -4355,6 +4368,15 @@
"react-native": "*"
}
},
"node_modules/expo-haptics": {
"version": "57.0.2",
"resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-57.0.2.tgz",
"integrity": "sha512-vPths6zTxxaGaemC7D1GKbw1iOnOAAL5oAcCFVSgVNWnuLCBefL1LkhRBaJsepiMXRh2y3vwPiXkPaf6d16blA==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-image": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-image/-/expo-image-57.0.1.tgz",
+2
View File
@@ -14,8 +14,10 @@
"drizzle-orm": "^0.45.2",
"expo": "~57.0.8",
"expo-background-task": "~57.0.6",
"expo-blur": "~57.0.2",
"expo-document-picker": "~57.0.1",
"expo-file-system": "~57.0.1",
"expo-haptics": "~57.0.2",
"expo-image": "~57.0.1",
"expo-image-manipulator": "~57.0.14",
"expo-print": "~57.0.1",