page + upload queue + folders
This commit is contained in:
+40
-1
@@ -22,6 +22,7 @@ import { deleteAsync } from 'expo-file-system/legacy';
|
||||
const NUM_COLUMNS = 3;
|
||||
const SCREEN_WIDTH = Dimensions.get('window').width;
|
||||
const ITEM_SIZE = (SCREEN_WIDTH - 16 * 2 - (NUM_COLUMNS - 1) * 6) / NUM_COLUMNS;
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
type RootStackParamList = {
|
||||
Folder: { folderId: string; folderName: string };
|
||||
@@ -75,7 +76,8 @@ export function FolderScreen() {
|
||||
const route = useRoute<FolderRouteProp>();
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const { folderId, folderName } = route.params;
|
||||
const { data, isLoading } = useFiles(folderId);
|
||||
const [page, setPage] = useState(1);
|
||||
const { data, isLoading, isFetching } = useFiles(folderId, page, PAGE_SIZE);
|
||||
const deleteFile = useDeleteFile();
|
||||
const freeLocalSpace = useFreeLocalSpace();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -87,6 +89,17 @@ export function FolderScreen() {
|
||||
const { data: foldersData } = useFolders();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (isFetching) return;
|
||||
const total = data?.meta?.total ?? 0;
|
||||
const loaded = data?.data?.length ?? 0;
|
||||
if (loaded < total) {
|
||||
setPage((p) => p + 1);
|
||||
}
|
||||
}, [isFetching, data?.meta?.total, data?.data?.length]);
|
||||
|
||||
const hasMore = (data?.data?.length ?? 0) > 0 && (data?.data?.length ?? 0) < (data?.meta?.total ?? 0);
|
||||
|
||||
const [tagModalVisible, setTagModalVisible] = useState(false);
|
||||
const [tagModalMode, setTagModalMode] = useState<'tag' | 'folder'>('tag');
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
@@ -246,12 +259,25 @@ export function FolderScreen() {
|
||||
data={files}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={styles.list}
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={0.5}
|
||||
ListEmptyComponent={
|
||||
<View style={styles.empty}>
|
||||
<MaterialIcons name="folder-open" size={48} color="#ccc" />
|
||||
<Text style={styles.emptyText}>Dossier vide</Text>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
isFetching ? (
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Chargement...</Text>
|
||||
</View>
|
||||
) : hasMore ? (
|
||||
<TouchableOpacity style={styles.footer} onPress={loadMore}>
|
||||
<Text style={styles.footerLink}>Charger plus</Text>
|
||||
</TouchableOpacity>
|
||||
) : null
|
||||
}
|
||||
renderItem={({ item: file }) => (
|
||||
<FolderGridItem
|
||||
file={file}
|
||||
@@ -496,4 +522,17 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
},
|
||||
footer: {
|
||||
paddingVertical: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 14,
|
||||
color: '#999',
|
||||
},
|
||||
footerLink: {
|
||||
fontSize: 14,
|
||||
color: '#1976D2',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -39,15 +39,15 @@ function recordsToUnifiedItems(records: ReturnType<typeof fileStore.getRootFiles
|
||||
|
||||
export function useFiles(parentId?: string | null, page: number = 1, limit: number = 100) {
|
||||
const queryKey = parentId
|
||||
? ['resources', parentId]
|
||||
? ['resources', parentId, page, limit]
|
||||
: ['resources', 'root', page, limit];
|
||||
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: async () => {
|
||||
if (parentId) {
|
||||
const backendRes = await apiClient.get<{ data: FileItem[] }>(
|
||||
`${ENDPOINTS.RESOURCES}/folders/${parentId}/resources?thumbnail=thumbnail_small`,
|
||||
const backendRes = await apiClient.get<PaginatedResponse<FileItem>>(
|
||||
`${ENDPOINTS.RESOURCES}/folders/${parentId}/resources?page=${page}&limit=${limit}&thumbnail=thumbnail_small`,
|
||||
);
|
||||
fileStore.mergeFromBackend(
|
||||
backendRes.data.map((f) => ({
|
||||
@@ -69,7 +69,7 @@ export function useFiles(parentId?: string | null, page: number = 1, limit: numb
|
||||
const children = fileStore.getChildrenByParent(parentId);
|
||||
return {
|
||||
data: children.map((r) => recordToUnifiedItem(r)!).filter(Boolean),
|
||||
meta: { page: 0, total: children.length },
|
||||
meta: { page, total: backendRes.meta?.total ?? children.length },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ export function usePullSync() {
|
||||
try {
|
||||
setIsSyncing(true);
|
||||
|
||||
const res = await apiClient.get<{ data: Array<{
|
||||
let page = 1;
|
||||
const limit = 100;
|
||||
let total = 0;
|
||||
const backendResources: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
@@ -28,9 +31,17 @@ export function usePullSync() {
|
||||
url?: string;
|
||||
thumbnailUrl?: string;
|
||||
ownerId?: string;
|
||||
}> }>(`${ENDPOINTS.RESOURCES}?page=1&limit=100&thumbnail=thumbnail_small`);
|
||||
}> = [];
|
||||
|
||||
do {
|
||||
const res = await apiClient.get<{ data: Array<typeof backendResources[number]>; meta?: { total: number } }>(
|
||||
`${ENDPOINTS.RESOURCES}?page=${page}&limit=${limit}&thumbnail=thumbnail_small`,
|
||||
);
|
||||
backendResources.push(...(res.data ?? []));
|
||||
total = res.meta?.total ?? res.data.length;
|
||||
page++;
|
||||
} while (backendResources.length < total);
|
||||
|
||||
const backendResources = res.data ?? [];
|
||||
const registry = fileStore.getAllSynced();
|
||||
const existingBackendIds = new Set(
|
||||
registry.filter((e) => e.backendId).map((e) => e.backendId)
|
||||
|
||||
@@ -7,6 +7,9 @@ import { ApiError, UploadError } from '../types';
|
||||
export type UploadFile = { uri: string; type: string; name: string };
|
||||
export type UploadResult = { name: string; id: string };
|
||||
|
||||
export const UPLOAD_MAX_RETRIES = 3;
|
||||
const BASE_RETRY_DELAY_MS = 1000;
|
||||
|
||||
export type UploadTaskStatus = 'pending' | 'uploading' | 'done' | 'error';
|
||||
|
||||
export type UploadTask = {
|
||||
@@ -16,6 +19,7 @@ export type UploadTask = {
|
||||
progress: number;
|
||||
result?: UploadResult;
|
||||
error?: string;
|
||||
retryCount: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
};
|
||||
@@ -33,6 +37,7 @@ function serialize(task: UploadTask): unknown {
|
||||
progress: task.progress,
|
||||
result: task.result ?? null,
|
||||
error: task.error ?? null,
|
||||
retryCount: task.retryCount,
|
||||
createdAt: task.createdAt,
|
||||
updatedAt: task.updatedAt,
|
||||
};
|
||||
@@ -50,6 +55,7 @@ function deserialize(data: unknown): UploadTask | null {
|
||||
progress: d.progress as number,
|
||||
result: d.result ? (d.result as UploadResult) : undefined,
|
||||
error: d.error ? (d.error as string) : undefined,
|
||||
retryCount: (d.retryCount as number) ?? 0,
|
||||
createdAt: d.createdAt as number,
|
||||
updatedAt: d.updatedAt as number,
|
||||
};
|
||||
@@ -147,6 +153,7 @@ class UploadQueue {
|
||||
file,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
retryCount: 0,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
@@ -171,6 +178,7 @@ class UploadQueue {
|
||||
if (!task || task.status !== 'error') return;
|
||||
task.status = 'pending';
|
||||
task.progress = 0;
|
||||
task.retryCount = 0;
|
||||
task.error = undefined;
|
||||
task.result = undefined;
|
||||
task.updatedAt = Date.now();
|
||||
@@ -184,6 +192,7 @@ class UploadQueue {
|
||||
if (task.status === 'error') {
|
||||
task.status = 'pending';
|
||||
task.progress = 0;
|
||||
task.retryCount = 0;
|
||||
task.error = undefined;
|
||||
task.result = undefined;
|
||||
task.updatedAt = Date.now();
|
||||
@@ -212,6 +221,7 @@ class UploadQueue {
|
||||
}
|
||||
|
||||
private async runTask(task: UploadTask) {
|
||||
let willRetry = false;
|
||||
try {
|
||||
const fsFile = new File(task.file.uri);
|
||||
const headers: Record<string, string> = {};
|
||||
@@ -251,20 +261,36 @@ class UploadQueue {
|
||||
this.notify();
|
||||
this.scheduleCleanup();
|
||||
} catch (err) {
|
||||
task.status = 'error';
|
||||
task.error =
|
||||
err instanceof UploadError
|
||||
? `${err.fileName} : ${err.message}`
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: 'Erreur inconnue';
|
||||
task.updatedAt = Date.now();
|
||||
this.persist();
|
||||
this.notify();
|
||||
task.retryCount++;
|
||||
if (task.retryCount <= UPLOAD_MAX_RETRIES) {
|
||||
willRetry = true;
|
||||
task.status = 'pending';
|
||||
task.progress = 0;
|
||||
task.error = undefined;
|
||||
task.updatedAt = Date.now();
|
||||
this.persist();
|
||||
this.notify();
|
||||
const delay = BASE_RETRY_DELAY_MS * Math.pow(2, task.retryCount - 1);
|
||||
setTimeout(() => this.processNext(), delay);
|
||||
} else {
|
||||
task.status = 'error';
|
||||
task.error =
|
||||
err instanceof UploadError
|
||||
? `${err.fileName} : ${err.message}`
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: 'Erreur inconnue';
|
||||
task.error += ` (${task.retryCount} tentative(s))`;
|
||||
task.updatedAt = Date.now();
|
||||
this.persist();
|
||||
this.notify();
|
||||
}
|
||||
} finally {
|
||||
this.active--;
|
||||
this.notify();
|
||||
this.processNext();
|
||||
if (!willRetry) {
|
||||
this.processNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user