add sync status
This commit is contained in:
+40
-3
@@ -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}>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
Generated
+22
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user