diff --git a/mobile/.claude/settings.json b/mobile/.claude/settings.json
deleted file mode 100644
index 176e6a5..0000000
--- a/mobile/.claude/settings.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "enabledPlugins": {
- "expo@claude-plugins-official": true
- }
-}
diff --git a/mobile/App.tsx b/mobile/App.tsx
index f84964d..2f97f5f 100644
--- a/mobile/App.tsx
+++ b/mobile/App.tsx
@@ -1,144 +1,24 @@
-import React, { useEffect } from 'react';
import { StatusBar } from 'expo-status-bar';
-import { ActivityIndicator, View } from 'react-native';
-import { GestureHandlerRootView } from 'react-native-gesture-handler';
-import { NavigationContainer } from '@react-navigation/native';
-import { createNativeStackNavigator } from '@react-navigation/native-stack';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
-import { AuthProvider, useAuth } from './contexts/AuthContext';
-import { DeviceProvider, useDevice } from './contexts/DeviceContext';
-import { SseProvider } from './contexts/SseContext';
-import { SseOcrListener } from './hooks/usePollOcr';
-import { SseResourceListener } from './hooks/useSSEResource';
-import { registerBackgroundUpload } from './services/backgroundUpload';
-import { LoginScreen } from './app/login';
-import { RegisterScreen } from './app/register';
-import { HomeScreen } from './app/index';
-import { ScanScreen } from './app/scan';
-import { SearchScreen } from './app/search';
-import { BatchReviewScreen } from './app/batch-review';
-import { PendingReviewScreen } from './app/pending-review';
-import { FileDetailScreen } from './app/file-detail';
-import { FileEditScreen } from './app/file-edit';
-import { FolderScreen } from './app/folder';
-import { OnboardingScreen } from './app/onboarding';
-import { SyncDetailScreen } from './app/sync-detail';
-import { onboardingStorage } from './services/onboardingStorage';
-import { initDB } from './services/fileStore';
-import { migrateFromLegacy } from './services/fileStore/migrate';
-import { createMMKVPersister } from './services/mmkvPersister';
-import { resetSyncState } from './hooks/useSyncQueue';
-
-initDB();
-migrateFromLegacy();
-resetSyncState();
-
-const Stack = createNativeStackNavigator();
-const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- gcTime: 1000 * 60 * 60 * 24, // 24h
- },
- },
-});
-const persister = createMMKVPersister();
-
-function AppNavigator() {
- const { user, isLoading: authLoading } = useAuth();
- const { isLoading: deviceLoading } = useDevice();
-
- useEffect(() => {
- if (user) {
- registerBackgroundUpload();
- }
- }, [user]);
-
- if (authLoading || (user && deviceLoading)) {
- return (
-
-
-
- );
- }
-
- const needsOnboarding = user && onboardingStorage.needsOnboarding();
-
- return (
-
-
- {user ? (
- <>
-
-
-
-
-
-
-
-
-
-
- >
- ) : (
- <>
-
-
- >
- )}
-
-
- );
-}
-
-function AppContent() {
- return (
-
-
-
-
-
-
-
-
-
-
- );
-}
+import { StyleSheet, Text, View } from 'react-native';
export default function App() {
return (
-
-
-
-
-
+
+ Dot.
+
+
);
}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#fff',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ title: {
+ fontSize: 32,
+ fontWeight: '600',
+ },
+});
\ No newline at end of file
diff --git a/mobile/CLAUDE.md b/mobile/CLAUDE.md
deleted file mode 100644
index 43c994c..0000000
--- a/mobile/CLAUDE.md
+++ /dev/null
@@ -1 +0,0 @@
-@AGENTS.md
diff --git a/mobile/api/client.ts b/mobile/api/client.ts
deleted file mode 100644
index 0c23538..0000000
--- a/mobile/api/client.ts
+++ /dev/null
@@ -1,176 +0,0 @@
-import { API_BASE_URL, ENDPOINTS } from '../constants/api';
-import { ApiError, HttpError } from '../types';
-import { tokenStorage } from './secureStorage';
-
-class ApiClient {
- private baseUrl: string;
- private accessToken: string | null = null;
-
- constructor(baseUrl: string) {
- this.baseUrl = baseUrl;
- }
-
- setAccessToken(token: string | null) {
- this.accessToken = token;
- }
-
- getAccessToken(): string | null {
- return this.accessToken;
- }
-
- private async request(
- endpoint: string,
- options: RequestInit = {},
- isRetry = false
- ): Promise {
- const url = `${this.baseUrl}${endpoint}`;
- const headers: Record = {
- 'Content-Type': 'application/json',
- ...(options.headers as Record),
- };
-
- if (this.accessToken) {
- headers['Authorization'] = `Bearer ${this.accessToken}`;
- }
-
- const response = await fetch(url, {
- ...options,
- headers,
- });
-
- if (response.status === 401 && !isRetry) {
- const refreshed = await this.tryRefreshToken();
- if (refreshed) {
- return this.request(endpoint, options, true);
- }
- throw new HttpError(401, 'Session expirée', 'UNAUTHORIZED');
- }
-
- if (!response.ok) {
- let message = 'Request failed';
- let code: string | undefined;
- try {
- const body: ApiError = await response.json();
- message = body.error?.message || message;
- code = body.error?.code;
- } catch {}
- throw new HttpError(response.status, message, code);
- }
-
- return response.json();
- }
-
- private async tryRefreshToken(): Promise {
- try {
- const refreshToken = await tokenStorage.getRefreshToken();
- if (!refreshToken) return false;
-
- const url = `${this.baseUrl}${ENDPOINTS.AUTH_REFRESH}`;
- const response = await fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ refresh_token: refreshToken }),
- });
-
- if (!response.ok) {
- await this.clearAuth();
- return false;
- }
-
- const data = await response.json();
- this.accessToken = data.access_token;
- await tokenStorage.setAccessToken(data.access_token);
- await tokenStorage.setRefreshToken(data.refresh_token);
- return true;
- } catch {
- await this.clearAuth();
- return false;
- }
- }
-
- private async clearAuth() {
- this.accessToken = null;
- await tokenStorage.deleteAccessToken();
- await tokenStorage.deleteRefreshToken();
- await tokenStorage.deleteUser();
- }
-
- async subscribeEvents(
- onEvent: (event: string, data: string) => void,
- onError: (err: Error) => void,
- ): Promise<() => void> {
- const abortController = new AbortController();
-
- const connect = async () => {
- try {
- const headers: Record = {
- Accept: 'text/event-stream',
- };
- if (this.accessToken) {
- headers['Authorization'] = `Bearer ${this.accessToken}`;
- }
-
- const response = await fetch(`${this.baseUrl}${ENDPOINTS.EVENTS}`, {
- headers,
- signal: abortController.signal,
- });
-
- if (!response.ok) {
- throw new Error(`SSE connection failed: ${response.status}`);
- }
-
- const reader = response.body?.getReader();
- if (!reader) throw new Error('Streaming not supported');
-
- const decoder = new TextDecoder();
- let buffer = '';
- let currentEvent = '';
-
- while (!abortController.signal.aborted) {
- const { done, value } = await reader.read();
- if (done) break;
-
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split('\n');
- buffer = lines.pop() || '';
-
- for (const line of lines) {
- if (line.startsWith('event: ')) {
- currentEvent = line.slice(7);
- } else if (line.startsWith('data: ')) {
- if (currentEvent) {
- onEvent(currentEvent, line.slice(6));
- currentEvent = '';
- }
- }
- }
- }
- } catch (err) {
- if (!abortController.signal.aborted) {
- onError(err instanceof Error ? err : new Error(String(err)));
- }
- }
- };
-
- connect();
-
- return () => abortController.abort();
- }
-
- async get(endpoint: string): Promise {
- return this.request(endpoint);
- }
-
- async post(endpoint: string, body?: unknown): Promise {
- return this.request(endpoint, {
- method: 'POST',
- body: body ? JSON.stringify(body) : undefined,
- });
- }
-
- async delete(endpoint: string): Promise {
- return this.request(endpoint, { method: 'DELETE' });
- }
-}
-
-export const apiClient = new ApiClient(API_BASE_URL);
diff --git a/mobile/api/secureStorage.ts b/mobile/api/secureStorage.ts
deleted file mode 100644
index 0d25275..0000000
--- a/mobile/api/secureStorage.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import * as SecureStore from 'expo-secure-store';
-
-const ACCESS_KEY = 'vaultdrop_access_token';
-const REFRESH_KEY = 'vaultdrop_refresh_token';
-const USER_KEY = 'vaultdrop_user';
-
-export const tokenStorage = {
- async getAccessToken(): Promise {
- return SecureStore.getItemAsync(ACCESS_KEY);
- },
-
- async setAccessToken(token: string): Promise {
- await SecureStore.setItemAsync(ACCESS_KEY, token);
- },
-
- async deleteAccessToken(): Promise {
- await SecureStore.deleteItemAsync(ACCESS_KEY);
- },
-
- async getRefreshToken(): Promise {
- return SecureStore.getItemAsync(REFRESH_KEY);
- },
-
- async setRefreshToken(token: string): Promise {
- await SecureStore.setItemAsync(REFRESH_KEY, token);
- },
-
- async deleteRefreshToken(): Promise {
- await SecureStore.deleteItemAsync(REFRESH_KEY);
- },
-
- async getUser(): Promise {
- return SecureStore.getItemAsync(USER_KEY);
- },
-
- async setUser(user: string): Promise {
- await SecureStore.setItemAsync(USER_KEY, user);
- },
-
- async deleteUser(): Promise {
- await SecureStore.deleteItemAsync(USER_KEY);
- },
-};
diff --git a/mobile/app.json b/mobile/app.json
index defb730..046312a 100644
--- a/mobile/app.json
+++ b/mobile/app.json
@@ -2,42 +2,25 @@
"expo": {
"name": "webui",
"slug": "webui",
- "version": "1.0.0",
+ "version": "2.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"ios": {
"supportsTablet": true,
- "infoPlist": {
- "NSCameraUsageDescription": "VaultDrop a besoin d'accéder à votre caméra pour scanner des documents."
- },
"bundleIdentifier": "com.anonymous.webui"
},
"android": {
- "permissions": [
- "CAMERA",
- "READ_EXTERNAL_STORAGE",
- "WRITE_EXTERNAL_STORAGE",
- "RECEIVE_BOOT_COMPLETED"
- ],
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/android-icon-foreground.png",
"backgroundImage": "./assets/android-icon-background.png",
"monochromeImage": "./assets/android-icon-monochrome.png"
},
- "predictiveBackGestureEnabled": false,
"package": "com.anonymous.webui"
},
"web": {
"favicon": "./assets/favicon.png"
- },
- "plugins": [
- "expo-secure-store",
- "expo-image",
- "expo-sqlite",
- "expo-background-task",
- "expo-status-bar"
- ]
+ }
}
-}
+}
\ No newline at end of file
diff --git a/mobile/app/batch-review.tsx b/mobile/app/batch-review.tsx
deleted file mode 100644
index fb3ba7f..0000000
--- a/mobile/app/batch-review.tsx
+++ /dev/null
@@ -1,234 +0,0 @@
-import React, { useState, useCallback } from 'react';
-import { View, Text, StyleSheet, FlatList, TouchableOpacity, Image, ActivityIndicator } from 'react-native';
-import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
-import { NativeStackNavigationProp } from '@react-navigation/native-stack';
-import { useBatchStore } from '../hooks/useBatchStore';
-import { usePdfGeneration } from '../hooks/usePdfGeneration';
-import { ConfirmModal } from '../components/ConfirmModal';
-
-type RootStackParamList = {
- Home: undefined;
- BatchReview: { batchId: string };
- PendingReview: { batchId: string; photoIds: string[] };
-};
-
-type NavigationProp = NativeStackNavigationProp;
-type BatchReviewRouteParams = { BatchReview: { batchId: string } };
-
-export function BatchReviewScreen() {
- const route = useRoute>();
- const navigation = useNavigation();
- const { getBatch, removePhotoFromBatch } = useBatchStore();
-
- const batch = getBatch(route.params.batchId);
-
- const [selectedIds, setSelectedIds] = useState>(new Set());
- const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false);
-
- if (!batch) {
- return (
-
- Lot introuvable
-
- );
- }
-
- const toggleSelection = (id: string) => {
- setSelectedIds((prev) => {
- const next = new Set(prev);
- if (next.has(id)) next.delete(id);
- else next.add(id);
- return next;
- });
- };
-
- const selectedCount = selectedIds.size;
-
- const handleDelete = () => {
- if (selectedCount === 0) return;
- setConfirmDeleteVisible(true);
- };
-
- const handleDeleteConfirm = () => {
- selectedIds.forEach((id) => removePhotoFromBatch(batch.id, id));
- setSelectedIds(new Set());
- };
-
- const handleGroup = () => {
- if (selectedCount === 0) return;
- const ids = Array.from(selectedIds);
- navigation.navigate('PendingReview', { batchId: batch.id, photoIds: ids });
- };
-
- const formatDate = (iso: string) => {
- const d = new Date(iso);
- return d.toLocaleDateString('fr-FR', {
- day: '2-digit', month: '2-digit', year: 'numeric',
- hour: '2-digit', minute: '2-digit',
- });
- };
-
- return (
-
-
- {batch.name}
-
- {batch.photos.length} photo{batch.photos.length > 1 ? 's' : ''} • {formatDate(batch.createdAt)}
-
-
-
- item.id}
- contentContainerStyle={styles.grid}
- renderItem={({ item }) => {
- const isSelected = selectedIds.has(item.id);
- return (
- toggleSelection(item.id)}
- >
-
- {isSelected && (
-
- ✓
-
- )}
-
- );
- }}
- />
-
-
-
- {selectedCount > 0 ? `${selectedCount} sélectionnée${selectedCount > 1 ? 's' : ''}` : 'Touchez une photo pour sélectionner'}
-
-
-
-
- 🗑 Supprimer
-
-
-
- 📦 Regrouper
-
-
-
-
- setConfirmDeleteVisible(false)}
- />
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#fff',
- },
- center: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- errorText: {
- fontSize: 16,
- color: '#666',
- },
- header: {
- padding: 16,
- borderBottomWidth: 1,
- borderBottomColor: '#e0e0e0',
- },
- title: {
- fontSize: 20,
- fontWeight: '700',
- marginBottom: 4,
- },
- subtitle: {
- fontSize: 14,
- color: '#666',
- },
- grid: {
- padding: 4,
- },
- gridItem: {
- flex: 1 / 3,
- aspectRatio: 1,
- margin: 4,
- borderRadius: 6,
- overflow: 'hidden',
- backgroundColor: '#f0f0f0',
- },
- gridItemSelected: {
- borderWidth: 3,
- borderColor: '#1976D2',
- },
- thumb: {
- width: '100%',
- height: '100%',
- },
- selectedOverlay: {
- ...StyleSheet.absoluteFill,
- backgroundColor: 'rgba(25,118,210,0.3)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- selectedCheck: {
- color: '#fff',
- fontSize: 32,
- fontWeight: '700',
- },
- footer: {
- padding: 16,
- borderTopWidth: 1,
- borderTopColor: '#e0e0e0',
- gap: 12,
- },
- selectionInfo: {
- fontSize: 14,
- color: '#666',
- textAlign: 'center',
- },
- actions: {
- flexDirection: 'row',
- gap: 12,
- },
- actionBtn: {
- flex: 1,
- paddingVertical: 12,
- borderRadius: 8,
- alignItems: 'center',
- },
- actionBtnDisabled: {
- opacity: 0.4,
- },
- deleteBtn: {
- backgroundColor: '#F44336',
- },
- groupBtn: {
- backgroundColor: '#4CAF50',
- },
- actionText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
-});
diff --git a/mobile/app/device-setup.tsx b/mobile/app/device-setup.tsx
deleted file mode 100644
index 62207e8..0000000
--- a/mobile/app/device-setup.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import React, { useState } from 'react';
-import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator, Platform } from 'react-native';
-import { useNavigation, CommonActions } from '@react-navigation/native';
-import { useDevice } from '../contexts/DeviceContext';
-
-export function DeviceSetupScreen() {
- const navigation = useNavigation();
- const { register, deviceName } = useDevice();
- const [name, setName] = useState(deviceName || `${Platform.OS.charAt(0).toUpperCase() + Platform.OS.slice(1)} de ${Platform.OS === 'ios' ? 'l\'utilisateur' : 'utilisateur'}`);
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const handleRegister = async () => {
- if (!name.trim()) return;
- setIsLoading(true);
- setError(null);
- try {
- await register(name.trim());
- navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'Home' }] }));
- } catch (err: any) {
- setError(err?.message || "Échec de l'enregistrement");
- } finally {
- setIsLoading(false);
- }
- };
-
- return (
-
-
- 📱
- Enregistrement du device
-
- Ce device doit être enregistré comme espace de stockage pour utiliser VaultDrop.
-
-
-
-
- {error && {error}}
-
-
- {isLoading ? (
-
- ) : (
- Enregistrer
- )}
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#f5f5f5',
- justifyContent: 'center',
- padding: 24,
- },
- content: {
- backgroundColor: '#fff',
- borderRadius: 16,
- padding: 24,
- alignItems: 'center',
- gap: 16,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.1,
- shadowRadius: 8,
- elevation: 4,
- },
- icon: {
- fontSize: 48,
- },
- title: {
- fontSize: 22,
- fontWeight: '700',
- color: '#333',
- textAlign: 'center',
- },
- subtitle: {
- fontSize: 15,
- color: '#666',
- textAlign: 'center',
- lineHeight: 22,
- },
- input: {
- width: '100%',
- borderWidth: 1,
- borderColor: '#ddd',
- borderRadius: 8,
- paddingHorizontal: 14,
- paddingVertical: 12,
- fontSize: 16,
- color: '#333',
- },
- error: {
- color: '#E53935',
- fontSize: 14,
- textAlign: 'center',
- },
- button: {
- width: '100%',
- backgroundColor: '#1976D2',
- paddingVertical: 14,
- borderRadius: 8,
- alignItems: 'center',
- },
- buttonDisabled: {
- backgroundColor: '#ccc',
- },
- buttonText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
-});
diff --git a/mobile/app/file-detail.tsx b/mobile/app/file-detail.tsx
deleted file mode 100644
index 632ab25..0000000
--- a/mobile/app/file-detail.tsx
+++ /dev/null
@@ -1,697 +0,0 @@
-import React, { useRef, useState, useCallback } from 'react';
-import {
- View,
- Text,
- StyleSheet,
- FlatList,
- Dimensions,
- ActivityIndicator,
- Modal,
- Pressable,
- TouchableOpacity,
- Share,
-} from 'react-native';
-import { Image } from 'expo-image';
-import { MaterialIcons } from '@expo/vector-icons';
-import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { useFile, useDownloadFile } from '../hooks/useFiles';
-import { fileStore } from '../services/fileStore';
-import { TagChip } from '../components/TagChip';
-import { FileThumbnail } from '../components/FileThumbnail';
-import { ConfirmModal } from '../components/ConfirmModal';
-import { SyncStatusBadge } from '../components/SyncStatusBadge';
-import { GestureHandlerRootView, Gesture, GestureDetector } from 'react-native-gesture-handler';
-import Animated, { useSharedValue, useAnimatedStyle, withSpring, runOnJS } from 'react-native-reanimated';
-import { ZoomableImage } from '../components/ZoomableImage';
-import { apiClient } from '../api/client';
-import { ENDPOINTS } from '../constants/api';
-import { downloadRegistry } from '../services/downloadRegistry';
-import { deleteAsync } from 'expo-file-system/legacy';
-import type { Variant, SyncStatus } from '../types';
-
-const SCREEN_WIDTH = Dimensions.get('window').width;
-
-type SelectedImage = { uri: string; width: number; height: number };
-
-type ModalState = { images: SelectedImage[]; index: number } | null;
-
-export type DeviceFileParam = {
- localUri: string;
- name: string;
- mimeType: string;
- createdAt: string;
-};
-
-type RootStackParamList = {
- FileDetail: { fileIds: string[]; initialIndex: number; deviceFiles?: Record };
-};
-
-type FileDetailRouteProp = RouteProp;
-
-const PANEL_HEIGHT = 410;
-const PANEL_HEADER_VISIBLE = 100;
-
-function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; deviceFile?: DeviceFileParam; onSelectImage?: (state: ModalState) => void }) {
- const isDevice = !!deviceFile;
- const navigation = useNavigation();
- const insets = useSafeAreaInsets();
-
- const localEntry = fileStore.getById(fileId);
- const apiId = isDevice ? '' : (localEntry?.backendId ?? fileId);
- const { data: fileData } = useFile(apiId);
- const downloadFile = useDownloadFile();
- const [downloading, setDownloading] = useState(false);
-
- const file = fileData as any;
- const uri = isDevice ? deviceFile.localUri : (localEntry?.localUri ?? file?.url);
-
- const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null);
-
- const handleImageLoad = useCallback((event: { source: { width: number; height: number } }) => {
- setImageSize({ width: event.source.width, height: event.source.height });
- }, []);
-
- const syncStatus: SyncStatus = isDevice
- ? 'local'
- : localEntry
- ? (localEntry.syncStatus as SyncStatus)
- : 'cloud';
-
- const fullVariants: Variant[] = (file?.variants ?? [])
- .filter((v: Variant) => v.variantType === 'thumbnail_full')
- .sort((a: Variant, b: Variant) => a.pageNumber - b.pageNumber);
- const fullThumbnails = fullVariants;
-
- const hasPages = fullThumbnails.length > 0;
- const fileName = deviceFile?.name ?? file?.name ?? fileId;
-
- const createdAt = deviceFile?.createdAt ?? file?.createdAt;
- const formattedDate = createdAt
- ? new Date(createdAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })
- : '';
-
- const fileSize = file?.size ?? 0;
- const formattedSize = fileSize > 0
- ? fileSize > 1024 * 1024
- ? `${(fileSize / (1024 * 1024)).toFixed(1)} Mo`
- : `${(fileSize / 1024).toFixed(1)} Ko`
- : '';
-
- const [optionsVisible, setOptionsVisible] = useState(false);
- const [deleting, setDeleting] = useState(false);
- const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false);
- const panelOffset = useSharedValue(PANEL_HEIGHT - PANEL_HEADER_VISIBLE);
- const panelStartY = useSharedValue(0);
- const isPanelExpanded = useSharedValue(false);
- const [panelExpanded, setPanelExpanded] = useState(false);
-
- const handleDownload = useCallback(async () => {
- if (!file) return;
- const bid = localEntry?.backendId ?? fileId;
- setDownloading(true);
- try {
- await downloadFile.mutateAsync({
- id: bid,
- backendResourceId: bid,
- name: fileName,
- mimeType: file?.mimeType ?? localEntry?.mimeType ?? 'application/octet-stream',
- size: file?.size ?? localEntry?.size ?? 0,
- createdAt: file?.createdAt ?? localEntry?.createdAt ?? new Date().toISOString(),
- source: 'cloud',
- syncStatus: 'cloud',
- tags: file?.tags ?? [],
- isFolder: false,
- });
- } catch {} finally {
- setDownloading(false);
- }
- }, [file, fileName, fileId, localEntry, downloadFile]);
-
- const handleShare = useCallback(async () => {
- setOptionsVisible(false);
- if (uri) {
- await Share.share({ url: uri, title: fileName });
- } else if (file?.url) {
- await Share.share({ url: file.url, title: fileName });
- }
- }, [uri, fileName, file?.url]);
-
- const handleDeleteConfirm = useCallback(async () => {
- setDeleting(true);
- try {
- const bid = localEntry?.backendId ?? (file as any)?.backendResourceId;
- if (bid) {
- await apiClient.delete(`${ENDPOINTS.RESOURCES}/${bid}`);
- fileStore.deleteByBackendId(bid);
- } else {
- fileStore.deleteById(fileId);
- }
- if (localEntry?.localUri) {
- await deleteAsync(localEntry.localUri, { idempotent: true });
- }
- downloadRegistry.remove(fileId);
- navigation.goBack();
- } catch {} finally {
- setDeleting(false);
- }
- }, [fileName, fileId, localEntry, file, navigation]);
-
- const handleDelete = useCallback(() => {
- setOptionsVisible(false);
- setConfirmDeleteVisible(true);
- }, []);
-
- const togglePanelJS = useCallback(() => {
- if (isPanelExpanded.value) {
- panelOffset.value = withSpring(PANEL_HEIGHT - PANEL_HEADER_VISIBLE);
- isPanelExpanded.value = false;
- setPanelExpanded(false);
- } else {
- panelOffset.value = withSpring(0);
- isPanelExpanded.value = true;
- setPanelExpanded(true);
- }
- }, []);
-
- const panGesture = Gesture.Pan()
- .onStart(() => {
- panelStartY.value = panelOffset.value;
- })
- .onUpdate((event) => {
- const offset = Math.max(0, Math.min(PANEL_HEIGHT - PANEL_HEADER_VISIBLE, panelStartY.value + event.translationY));
- panelOffset.value = offset;
- })
- .onEnd(() => {
- if (panelOffset.value > (PANEL_HEIGHT - PANEL_HEADER_VISIBLE) / 2) {
- panelOffset.value = withSpring(PANEL_HEIGHT - PANEL_HEADER_VISIBLE);
- isPanelExpanded.value = false;
- runOnJS(setPanelExpanded)(false);
- } else {
- panelOffset.value = withSpring(0);
- isPanelExpanded.value = true;
- runOnJS(setPanelExpanded)(true);
- }
- });
-
- const panelAnimatedStyle = useAnimatedStyle(() => ({
- transform: [{ translateY: panelOffset.value }],
- }));
-
- const renderImageContent = () => {
- if (hasPages) {
- return fullThumbnails.map((thumb, thumbIndex) => {
- const allImages: SelectedImage[] = fullThumbnails.map((t) => ({
- uri: t.url,
- width: SCREEN_WIDTH,
- height: t.height * (SCREEN_WIDTH / t.width),
- }));
- return (
- onSelectImage?.({ images: allImages, index: thumbIndex })}
- >
-
-
- );
- });
- }
- if (uri) {
- return (
-
- );
- }
- if (file) {
- return (
-
-
- {syncStatus === 'cloud' && (
-
- {downloading ? (
-
- ) : (
-
- )}
-
- {downloading ? 'Téléchargement...' : 'Télécharger'}
-
-
- )}
-
- );
- }
- return ;
- };
-
- if (deleting) {
- return (
-
-
-
- );
- }
-
- return (
-
- {
- if (!hasPages && uri) {
- const height = imageSize
- ? imageSize.height * SCREEN_WIDTH / imageSize.width
- : SCREEN_WIDTH;
- onSelectImage?.({
- images: [{ uri, width: SCREEN_WIDTH, height }],
- index: 0,
- });
- }
- }}
- >
-
- {renderImageContent()}
-
-
-
-
- {formattedDate}
-
- setOptionsVisible(true)} style={styles.headerBtn}>
-
-
-
-
-
-
-
-
-
- {fileName}
-
-
-
-
-
- {formattedDate ? (
-
-
- {formattedDate}
-
- ) : null}
-
- {formattedSize ? (
-
-
- {formattedSize}
-
- ) : null}
-
- {file?.tags && file.tags.length > 0 && (
-
- Tags
-
- {file.tags.map((tag: any) => (
-
- ))}
-
-
- )}
-
- {file?.ocrText && (
-
- Texte OCR
- {file.ocrText}
-
- )}
-
-
-
-
- Partager
-
-
-
- Supprimer
-
-
-
-
-
-
- setOptionsVisible(false)}>
- setOptionsVisible(false)}>
-
-
-
- Partager
-
-
-
-
- Supprimer
-
-
-
-
-
- setConfirmDeleteVisible(false)}
- />
-
- );
-}
-
-export function FileDetailScreen() {
- const route = useRoute();
- const { fileIds, initialIndex, deviceFiles } = route.params;
-
- const flatListRef = useRef(null);
- const [currentIndex, setCurrentIndex] = useState(initialIndex);
- const [modalState, setModalState] = useState(null);
-
- const handleSwipeVertical = useCallback((direction: 'up' | 'down') => {
- setModalState((prev) => {
- if (!prev) return prev;
- const next = direction === 'up'
- ? Math.min(prev.index + 1, prev.images.length - 1)
- : Math.max(prev.index - 1, 0);
- if (next === prev.index) return prev;
- return { ...prev, index: next };
- });
- }, []);
-
- return (
-
- item}
- horizontal
- snapToInterval={SCREEN_WIDTH}
- decelerationRate="fast"
- disableIntervalMomentum
- showsHorizontalScrollIndicator={false}
- initialScrollIndex={initialIndex}
- windowSize={5}
- initialNumToRender={3}
- maxToRenderPerBatch={3}
- getItemLayout={(_, index) => ({
- length: SCREEN_WIDTH,
- offset: SCREEN_WIDTH * index,
- index,
- })}
- onMomentumScrollEnd={(e) => {
- const index = Math.round(e.nativeEvent.contentOffset.x / SCREEN_WIDTH);
- setCurrentIndex(index);
- }}
- renderItem={({ item }) => (
-
-
-
- )}
- />
-
-
-
- {currentIndex + 1} / {fileIds.length}
-
-
-
- setModalState(null)}
- >
- {modalState && (
-
- setModalState(null)}
- onSwipeVertical={handleSwipeVertical}
- />
- {modalState.images.length > 1 && (
-
-
- {modalState.index + 1} / {modalState.images.length}
-
-
- )}
-
- )}
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#000',
- },
- center: {
- justifyContent: 'center',
- alignItems: 'center',
- },
- pageWrapper: {
- width: SCREEN_WIDTH,
- },
- detailContainer: {
- flex: 1,
- backgroundColor: '#000',
- },
- imageCentered: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- image: {
- width: SCREEN_WIDTH,
- },
- headerOverlay: {
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- paddingHorizontal: 8,
- paddingBottom: 8,
- backgroundColor: 'rgba(0,0,0,0.3)',
- },
- headerBtn: {
- width: 40,
- height: 40,
- borderRadius: 20,
- backgroundColor: 'rgba(0,0,0,0.4)',
- alignItems: 'center',
- justifyContent: 'center',
- },
- headerDate: {
- fontSize: 14,
- fontWeight: '600',
- color: '#fff',
- flex: 1,
- textAlign: 'center',
- marginHorizontal: 8,
- },
- panel: {
- position: 'absolute',
- bottom: 0,
- left: 0,
- right: 0,
- backgroundColor: '#fff',
- borderTopLeftRadius: 20,
- borderTopRightRadius: 20,
- paddingHorizontal: 20,
- paddingTop: 12,
- height: PANEL_HEIGHT,
- },
- panelHandle: {
- width: 36,
- height: 4,
- borderRadius: 2,
- backgroundColor: '#ddd',
- alignSelf: 'center',
- marginBottom: 12,
- },
- panelHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- marginBottom: 12,
- },
- panelFileName: {
- fontSize: 17,
- fontWeight: '700',
- color: '#333',
- flex: 1,
- marginRight: 8,
- },
- panelBody: {
- flex: 1,
- },
- metaRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- marginBottom: 8,
- },
- metaText: {
- fontSize: 14,
- color: '#666',
- },
- tagsRow: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 6,
- marginTop: 8,
- },
- tagsSection: {
- marginTop: 8,
- },
- sectionLabel: {
- fontSize: 13,
- fontWeight: '600',
- color: '#333',
- },
- ocrSection: {
- marginTop: 12,
- },
- ocrText: {
- fontSize: 13,
- color: '#555',
- lineHeight: 18,
- marginTop: 4,
- },
- actionsRow: {
- flexDirection: 'row',
- gap: 12,
- marginTop: 16,
- },
- actionBtn: {
- flex: 1,
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: 8,
- paddingVertical: 12,
- borderRadius: 10,
- backgroundColor: '#f5f5f5',
- },
- actionBtnText: {
- fontSize: 15,
- fontWeight: '600',
- color: '#1976D2',
- },
- cloudOnlyContainer: {
- alignItems: 'center',
- gap: 16,
- },
- downloadBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: '#1976D2',
- paddingHorizontal: 20,
- paddingVertical: 12,
- borderRadius: 8,
- gap: 8,
- },
- downloadBtnText: {
- color: '#fff',
- fontSize: 15,
- fontWeight: '600',
- },
- optionsOverlay: {
- flex: 1,
- backgroundColor: 'rgba(0,0,0,0.5)',
- justifyContent: 'flex-end',
- paddingBottom: 40,
- },
- optionsMenu: {
- backgroundColor: '#fff',
- borderRadius: 14,
- marginHorizontal: 20,
- overflow: 'hidden',
- },
- optionItem: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 14,
- paddingVertical: 16,
- paddingHorizontal: 20,
- },
- optionText: {
- fontSize: 16,
- color: '#333',
- },
- optionDivider: {
- height: 1,
- backgroundColor: '#eee',
- marginHorizontal: 20,
- },
- pagination: {
- position: 'absolute',
- bottom: 16,
- alignSelf: 'center',
- backgroundColor: 'rgba(0,0,0,0.5)',
- borderRadius: 12,
- paddingHorizontal: 12,
- paddingVertical: 4,
- },
- paginationText: {
- color: '#fff',
- fontSize: 13,
- },
- modalPagination: {
- position: 'absolute',
- bottom: 40,
- alignSelf: 'center',
- backgroundColor: 'rgba(0,0,0,0.5)',
- borderRadius: 12,
- paddingHorizontal: 12,
- paddingVertical: 4,
- },
- modalPaginationText: {
- color: '#fff',
- fontSize: 13,
- },
-});
diff --git a/mobile/app/file-edit.tsx b/mobile/app/file-edit.tsx
deleted file mode 100644
index daeb9d3..0000000
--- a/mobile/app/file-edit.tsx
+++ /dev/null
@@ -1,568 +0,0 @@
-import React, { useState, useCallback, useMemo, useRef } from 'react';
-import {
- View,
- Text,
- StyleSheet,
- FlatList,
- TouchableOpacity,
- TextInput,
- Alert,
- ActivityIndicator,
- Dimensions,
- Modal,
-} from 'react-native';
-import { Image } from 'expo-image';
-import { MaterialIcons } from '@expo/vector-icons';
-import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
-import { GestureHandlerRootView } from 'react-native-gesture-handler';
-import { useAddTags } from '../hooks/useFiles';
-import { usePdfGeneration } from '../hooks/usePdfGeneration';
-import { useUpload } from '../hooks/useUpload';
-import { TagChip } from '../components/TagChip';
-import { FileThumbnail } from '../components/FileThumbnail';
-import { ZoomableImage } from '../components/ZoomableImage';
-import { fileStore } from '../services/fileStore';
-import { apiClient } from '../api/client';
-import { ENDPOINTS } from '../constants/api';
-import type { SyncStatus, Variant } from '../types';
-
-const NUM_COLUMNS = 3;
-const SCREEN_WIDTH = Dimensions.get('window').width;
-const PADDING = 16;
-const ITEM_GAP = 6;
-const ITEM_SIZE = (SCREEN_WIDTH - PADDING * 2 - (NUM_COLUMNS - 1) * ITEM_GAP) / NUM_COLUMNS;
-
-type FileEditRouteParams = {
- FileEdit: { fileIds: string[] };
-};
-
-type PreviewFile = { uri: string; width: number; height: number };
-
-interface FileEditItemProps {
- fileId: string;
- selected: boolean;
- onSelect: (id: string) => void;
- onPreview: (id: string) => void;
- size: number;
-}
-
-const FileEditItem = React.memo(function FileEditItem({ fileId, selected, onSelect, onPreview, size }: FileEditItemProps) {
- const record = fileStore.getByBackendId(fileId) ?? fileStore.getById(fileId);
-
- const uri = record?.localUri ?? undefined;
- const thumbnailUrl = record?.thumbnailUrl ?? undefined;
- const mimeType = record?.mimeType ?? 'application/octet-stream';
- const fileName = record?.name ?? fileId;
- const syncStatus = (record?.syncStatus ?? 'cloud') as SyncStatus;
- const isViewable = mimeType.startsWith('image/') || mimeType === 'application/pdf';
-
- return (
-
- { if (isViewable) onPreview(fileId); else onSelect(fileId); }}
- >
-
-
-
- {isViewable && (
- onPreview(fileId)}
- hitSlop={6}
- >
-
-
- )}
-
- onSelect(fileId)}
- hitSlop={8}
- >
-
- {selected && }
-
-
-
- );
-});
-
-export function FileEditScreen() {
- const route = useRoute>();
- const navigation = useNavigation();
- const { fileIds } = route.params;
-
- const addTags = useAddTags();
- const { generatePdf, generating, progress } = usePdfGeneration();
- const upload = useUpload();
-
- const [tagInput, setTagInput] = useState('');
- const [pendingTags, setPendingTags] = useState([]);
- const [uploading, setUploading] = useState(false);
- const [selectedIds, setSelectedIds] = useState>(new Set());
- const [previewFiles, setPreviewFiles] = useState(null);
- const [previewIndex, setPreviewIndex] = useState(0);
- const [previewLoading, setPreviewLoading] = useState(false);
-
- const hasSelection = selectedIds.size > 0;
- const targetIds = hasSelection ? Array.from(selectedIds) : fileIds;
-
- const toggleSelection = useCallback((id: string) => {
- setSelectedIds((prev) => {
- const next = new Set(prev);
- if (next.has(id)) next.delete(id);
- else next.add(id);
- return next;
- });
- }, []);
-
- const toggleSelectAll = useCallback(() => {
- setSelectedIds((prev) => {
- if (prev.size === fileIds.length) return new Set();
- return new Set(fileIds);
- });
- }, [fileIds]);
-
- const openPreview = useCallback((files: PreviewFile[], index: number) => {
- setPreviewFiles(files);
- setPreviewIndex(index);
- }, []);
-
- const handlePreview = useCallback(async (fileId: string) => {
- const record = fileStore.getByBackendId(fileId) ?? fileStore.getById(fileId);
-
- if (record?.localUri) {
- openPreview([{ uri: record.localUri, width: SCREEN_WIDTH, height: SCREEN_WIDTH }], 0);
- return;
- }
-
- setPreviewLoading(true);
- try {
- const data = await apiClient.get(`${ENDPOINTS.RESOURCES}/${fileId}`);
- const url = data?.url;
- if (url) {
- const fullVariants: Variant[] = (data?.variants ?? [])
- .filter((v: Variant) => v.variantType === 'thumbnail_full')
- .sort((a: Variant, b: Variant) => a.pageNumber - b.pageNumber);
-
- if (fullVariants.length > 0) {
- const pages: PreviewFile[] = fullVariants.map((v) => ({
- uri: v.url,
- width: v.width,
- height: v.height,
- }));
- openPreview(pages, 0);
- } else {
- openPreview([{ uri: url, width: SCREEN_WIDTH, height: SCREEN_WIDTH }], 0);
- }
- } else {
- Alert.alert('Erreur', 'Impossible de charger l\'aperçu');
- }
- } catch {
- Alert.alert('Erreur', 'Impossible de charger l\'aperçu');
- } finally {
- setPreviewLoading(false);
- }
- }, [openPreview]);
-
- const handleSwipeVertical = useCallback((direction: 'up' | 'down') => {
- setPreviewIndex((prev) => {
- if (!previewFiles || previewFiles.length <= 1) return prev;
- const next = direction === 'up'
- ? Math.min(prev + 1, previewFiles.length - 1)
- : Math.max(prev - 1, 0);
- return next;
- });
- }, [previewFiles]);
-
- const handleAddTag = () => {
- const tag = tagInput.trim().toLowerCase();
- if (!tag || pendingTags.includes(tag)) return;
- setPendingTags((prev) => [...prev, tag]);
- setTagInput('');
- };
-
- const handleRemoveTag = (tag: string) => {
- setPendingTags((prev) => prev.filter((t) => t !== tag));
- };
-
- const handleApplyTags = useCallback(async () => {
- if (pendingTags.length === 0) return;
- for (const fileId of targetIds) {
- await addTags.mutateAsync({ fileId, tags: pendingTags });
- }
- Alert.alert('Succès', `${pendingTags.length} tag${pendingTags.length > 1 ? 's' : ''} ajouté${pendingTags.length > 1 ? 's' : ''}`);
- setPendingTags([]);
- }, [pendingTags, targetIds, addTags]);
-
- const handleGeneratePdf = useCallback(async () => {
- if (targetIds.length === 0) return;
-
- setUploading(true);
- try {
- const results = await Promise.all(
- targetIds.map(async (fileId) => {
- const record = fileStore.getByBackendId(fileId) ?? fileStore.getById(fileId);
- if (record?.localUri) return { uri: record.localUri };
- const data = await apiClient.get<{ url: string }>(`${ENDPOINTS.RESOURCES}/${fileId}`);
- return { uri: data?.url || '' };
- })
- );
- const imageUris = results.filter((r) => r.uri);
-
- if (imageUris.length === 0) {
- Alert.alert('Erreur', 'Aucune image trouvée pour la génération du PDF');
- setUploading(false);
- return;
- }
-
- const pdfUri = await generatePdf(imageUris);
- if (!pdfUri) {
- Alert.alert('Erreur', 'Échec de la génération du PDF');
- setUploading(false);
- return;
- }
-
- Alert.alert(
- 'PDF généré',
- 'Voulez-vous uploader le fichier ?',
- [
- { text: 'Annuler', style: 'cancel' },
- {
- text: 'Uploader',
- onPress: async () => {
- try {
- const pdfName = `document_${Date.now()}.pdf`;
- const pdfUriClean = pdfUri.startsWith('file://') ? pdfUri : 'file://' + pdfUri;
- await upload.mutateAsync([
- { uri: pdfUriClean, type: 'application/pdf', name: pdfName },
- ]);
- Alert.alert('Succès', 'PDF uploadé avec succès', [
- { text: 'OK', onPress: () => navigation.goBack() },
- ]);
- } catch (e: any) {
- const msg = e?.message || e?.toString() || 'Erreur inconnue';
- Alert.alert('Erreur', `Échec de l'upload du PDF: ${msg}`);
- }
- },
- },
- ]
- );
- } finally {
- setUploading(false);
- }
- }, [targetIds, generatePdf, upload, navigation]);
-
- const isLoading = generating || uploading || previewLoading;
-
- const renderItem = useCallback(({ item }: { item: string }) => (
-
- ), [hasSelection, selectedIds, toggleSelection, handlePreview]);
-
- return (
-
-
-
-
- Édition
-
- {hasSelection
- ? `${selectedIds.size} sélectionné${selectedIds.size > 1 ? 's' : ''} / ${fileIds.length}`
- : `${fileIds.length} fichier${fileIds.length > 1 ? 's' : ''}`}
-
-
-
-
- {hasSelection && selectedIds.size === fileIds.length ? 'Tout' : 'Tout'}
-
-
-
-
-
- item}
- contentContainerStyle={styles.grid}
- columnWrapperStyle={styles.gridRow}
- renderItem={renderItem}
- />
-
-
- Tags
-
- {pendingTags.map((tag) => (
- handleRemoveTag(tag)}>
- handleRemoveTag(tag)} />
-
- ))}
-
-
-
-
-
-
-
- {pendingTags.length > 0 && (
-
- {addTags.isPending ? (
-
- ) : (
- Appliquer les tags
- )}
-
- )}
-
-
-
- {(generating || uploading) && (
-
-
-
- {generating ? `Génération du PDF... ${progress}%` : 'Upload en cours...'}
-
-
- )}
- {previewLoading && (
-
-
- Chargement de l'aperçu...
-
- )}
-
- {generating ? (
-
- ) : (
-
- )}
- Créer un PDF
-
-
-
- setPreviewFiles(null)}
- >
- {previewFiles && (
-
- setPreviewFiles(null)}
- onSwipeVertical={handleSwipeVertical}
- />
- {previewFiles.length > 1 && (
-
-
- {previewIndex + 1} / {previewFiles.length}
-
-
- )}
-
- )}
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#fff',
- },
- header: {
- padding: PADDING,
- borderBottomWidth: 1,
- borderBottomColor: '#e0e0e0',
- },
- headerRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- },
- title: {
- fontSize: 20,
- fontWeight: '700',
- marginBottom: 4,
- },
- subtitle: {
- fontSize: 14,
- color: '#666',
- },
- selectAllBtn: {
- paddingHorizontal: 12,
- paddingVertical: 6,
- borderRadius: 16,
- backgroundColor: '#E3F2FD',
- },
- selectAllText: {
- fontSize: 13,
- fontWeight: '600',
- color: '#1976D2',
- },
- grid: {
- padding: PADDING,
- },
- gridRow: {
- gap: ITEM_GAP,
- },
- tagSection: {
- padding: PADDING,
- borderTopWidth: 1,
- borderTopColor: '#e0e0e0',
- },
- sectionTitle: {
- fontSize: 16,
- fontWeight: '600',
- marginBottom: 8,
- },
- tagRow: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 6,
- marginBottom: 8,
- },
- tagChip: {
- marginRight: 2,
- },
- tagInputRow: {
- flexDirection: 'row',
- gap: 8,
- },
- tagInput: {
- flex: 1,
- borderWidth: 1,
- borderColor: '#e0e0e0',
- borderRadius: 8,
- padding: 10,
- fontSize: 14,
- },
- tagAddBtn: {
- width: 40,
- height: 40,
- borderRadius: 8,
- backgroundColor: '#1976D2',
- justifyContent: 'center',
- alignItems: 'center',
- },
- applyTagsBtn: {
- marginTop: 10,
- backgroundColor: '#4CAF50',
- paddingVertical: 10,
- borderRadius: 8,
- alignItems: 'center',
- },
- applyTagsText: {
- color: '#fff',
- fontSize: 14,
- fontWeight: '600',
- },
- footer: {
- padding: PADDING,
- borderTopWidth: 1,
- borderTopColor: '#e0e0e0',
- gap: 8,
- },
- progressRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
- progressText: {
- fontSize: 14,
- color: '#666',
- },
- pdfBtn: {
- flexDirection: 'row',
- backgroundColor: '#1976D2',
- paddingHorizontal: 24,
- paddingVertical: 14,
- borderRadius: 8,
- alignItems: 'center',
- justifyContent: 'center',
- gap: 8,
- },
- pdfBtnDisabled: {
- opacity: 0.6,
- },
- pdfBtnText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
- previewBtn: {
- position: 'absolute',
- top: 4,
- right: 28,
- width: 24,
- height: 24,
- borderRadius: 12,
- backgroundColor: 'rgba(0,0,0,0.5)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- selectBtn: {
- position: 'absolute',
- bottom: 4,
- right: 4,
- padding: 4,
- },
- checkCircle: {
- width: 22,
- height: 22,
- borderRadius: 11,
- borderWidth: 2,
- borderColor: '#fff',
- backgroundColor: 'rgba(0,0,0,0.3)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- checkCircleSelected: {
- backgroundColor: '#1976D2',
- borderColor: '#1976D2',
- },
- modalPagination: {
- position: 'absolute',
- bottom: 40,
- alignSelf: 'center',
- backgroundColor: 'rgba(0,0,0,0.5)',
- borderRadius: 12,
- paddingHorizontal: 12,
- paddingVertical: 4,
- },
- modalPaginationText: {
- color: '#fff',
- fontSize: 13,
- },
-});
diff --git a/mobile/app/folder.tsx b/mobile/app/folder.tsx
deleted file mode 100644
index 50761e7..0000000
--- a/mobile/app/folder.tsx
+++ /dev/null
@@ -1,476 +0,0 @@
-import React, { useMemo, useState, useCallback, useEffect } from 'react';
-import { View, FlatList, StyleSheet, TouchableOpacity, Text, Modal, TextInput } from 'react-native';
-import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
-import { NativeStackNavigationProp } from '@react-navigation/native-stack';
-import { MaterialIcons } from '@expo/vector-icons';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import {
- useDeleteFile, useFiles, useFreeLocalSpace,
- useAddTags, useMoveResources, useFolders, useCreateFolder,
-} from '../hooks/useFiles';
-import { SelectionPanel } from '../components/SelectionPanel';
-import { ConfirmModal, type ConfirmOption } from '../components/ConfirmModal';
-import { UnifiedFileItem } from '../types';
-import { isFolder } from '../types';
-import { FileCard } from '../components/FileCard';
-import { fileStore } from '../services/fileStore';
-import { downloadRegistry } from '../services/downloadRegistry';
-import { apiClient } from '../api/client';
-import { ENDPOINTS } from '../constants/api';
-import { useQueryClient } from '@tanstack/react-query';
-import { deleteAsync } from 'expo-file-system/legacy';
-
-const PAGE_SIZE = 100;
-
-type RootStackParamList = {
- Folder: { folderId: string; folderName: string };
- FileDetail: { fileIds: string[]; initialIndex: number };
- FileEdit: { fileIds: string[] };
-};
-
-type NavigationProp = NativeStackNavigationProp;
-
-type FolderRouteProp = RouteProp;
-
-export function FolderScreen() {
- const route = useRoute();
- const navigation = useNavigation();
- const { folderId, folderName } = route.params;
- const [page, setPage] = useState(1);
- const { data, isLoading, isFetching } = useFiles(folderId, page, PAGE_SIZE);
- const deleteFile = useDeleteFile();
- const freeLocalSpace = useFreeLocalSpace();
- const queryClient = useQueryClient();
- const [selectedIds, setSelectedIds] = useState>(new Set());
-
- const addTags = useAddTags();
- const moveFiles = useMoveResources();
- const createFolder = useCreateFolder();
- 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('');
- const [moveModalVisible, setMoveModalVisible] = useState(false);
- const [confirmDeleteState, setConfirmDeleteState] = useState<{ message: string; options: ConfirmOption[] } | null>(null);
-
- const selectionMode = selectedIds.size > 0;
-
- useEffect(() => {
- navigation.setOptions({ title: folderName });
- }, [navigation, folderName]);
-
- const files = useMemo(() => {
- return data?.data ?? [];
- }, [data]);
-
- const fileIdToIndex = useMemo(() => {
- const map = new Map();
- files.forEach((f, i) => map.set(f.id, i));
- return map;
- }, [files]);
-
- const toggleSelection = useCallback((id: string) => {
- setSelectedIds((prev) => {
- const next = new Set(prev);
- if (next.has(id)) next.delete(id);
- else next.add(id);
- return next;
- });
- }, []);
-
- const clearSelection = useCallback(() => {
- setSelectedIds(new Set());
- }, []);
-
- const handleDelete = useCallback(() => {
- const ids = Array.from(selectedIds);
- if (ids.length === 0) return;
- const hasSynced = ids.some((id) => {
- const f = files.find((fi) => fi.id === id);
- return f?.syncStatus === 'synced';
- });
- const label = ids.length === 1 ? 'ce fichier' : `ces ${ids.length} fichiers`;
- const options: ConfirmOption[] = [];
- if (hasSynced) {
- options.push({
- label: 'Du device uniquement',
- onPress: async () => {
- const syncedIds = ids.filter((id) => {
- const f = files.find((fi) => fi.id === id);
- return f?.syncStatus === 'synced';
- });
- if (syncedIds.length > 0) {
- await freeLocalSpace.mutateAsync(syncedIds);
- }
- setSelectedIds(new Set());
- },
- });
- }
- options.push({
- label: 'Du device + serveur',
- destructive: true,
- onPress: async () => {
- for (const id of ids) {
- const f = files.find((fi) => fi.id === id);
- if (f?.backendResourceId) {
- await apiClient.delete(`${ENDPOINTS.RESOURCES}/${f.backendResourceId}`);
- fileStore.deleteByBackendId(f.backendResourceId);
- } else {
- fileStore.deleteById(id);
- }
- if (f?.localUri) {
- try { await deleteAsync(f.localUri, { idempotent: true }); } catch {}
- }
- downloadRegistry.remove(id);
- }
- await queryClient.invalidateQueries({ queryKey: ['resources'] });
- setSelectedIds(new Set());
- },
- });
- options.push({ label: 'Annuler' });
- setConfirmDeleteState({ message: `Supprimer ${label} ?`, options });
- }, [selectedIds, files, deleteFile, freeLocalSpace, queryClient]);
-
- const handleEdit = useCallback(() => {
- const ids = Array.from(selectedIds);
- if (ids.length === 0) return;
- navigation.navigate('FileEdit', { fileIds: ids });
- setSelectedIds(new Set());
- }, [selectedIds, navigation]);
-
- const openTagModal = useCallback((mode: 'tag' | 'folder') => {
- setTagModalMode(mode);
- setTagInput('');
- setTagModalVisible(true);
- }, []);
-
- const handleAddTag = useCallback(async () => {
- const name = tagInput.trim();
- if (!name) return;
- const ids = Array.from(selectedIds);
- for (const id of ids) {
- await addTags.mutateAsync({ fileId: id, tags: [name] });
- }
- setTagModalVisible(false);
- setSelectedIds(new Set());
- }, [tagInput, selectedIds, addTags]);
-
- const handleCreateFolderFromModal = useCallback(async () => {
- const name = tagInput.trim();
- if (!name) return;
- const ids = Array.from(selectedIds);
- try {
- const newFolder = await createFolder.mutateAsync({ name, parentResourceId: folderId });
- await moveFiles.mutateAsync({ resourceIds: ids, parentResourceId: newFolder.id });
- setTagModalVisible(false);
- setSelectedIds(new Set());
- navigation.navigate('Folder', { folderId: newFolder.id, folderName: newFolder.name });
- } catch {}
- }, [tagInput, selectedIds, createFolder, moveFiles, navigation, folderId]);
-
- const handleMove = useCallback(async (folderId: string | null) => {
- const ids = Array.from(selectedIds);
- await moveFiles.mutateAsync({ resourceIds: ids, parentResourceId: folderId });
- setMoveModalVisible(false);
- setSelectedIds(new Set());
- }, [selectedIds, moveFiles]);
-
- const handleItemPress = useCallback((file: UnifiedFileItem) => {
- if (selectionMode) {
- toggleSelection(file.id);
- } else if (isFolder(file)) {
- navigation.push('Folder', { folderId: file.id, folderName: file.name });
- } else {
- navigation.navigate('FileDetail', {
- fileIds: files.map((f) => f.id),
- initialIndex: fileIdToIndex.get(file.id) ?? 0,
- });
- }
- }, [selectionMode, toggleSelection, navigation, files, fileIdToIndex]);
-
- const handleItemLongPress = useCallback((file: UnifiedFileItem) => {
- if (!selectionMode) toggleSelection(file.id);
- }, [selectionMode, toggleSelection]);
-
- if (isLoading) {
- return (
-
- Chargement...
-
- );
- }
-
- return (
-
- item.id}
- contentContainerStyle={styles.list}
- onEndReached={loadMore}
- onEndReachedThreshold={0.5}
- ListEmptyComponent={
-
-
- Dossier vide
-
- }
- ListFooterComponent={
- isFetching ? (
-
- Chargement...
-
- ) : hasMore ? (
-
- Charger plus
-
- ) : null
- }
- renderItem={({ item: file }) => (
-
- )}
- />
-
- {selectionMode && (
- openTagModal('tag')}
- onFolder={() => openTagModal('folder')}
- onMove={() => setMoveModalVisible(true)}
- insetsBottom={insets.bottom}
- />
- )}
-
- setTagModalVisible(false)}>
- setTagModalVisible(false)}>
- {}}>
-
- {tagModalMode === 'folder' ? 'Créer un dossier' : 'Ajouter un tag'}
-
-
-
- setTagModalVisible(false)}>
- Annuler
-
-
- {tagModalMode === 'folder' ? 'Créer' : 'Ajouter'}
-
-
-
-
-
-
- setMoveModalVisible(false)}>
- setMoveModalVisible(false)}>
- {}}>
- Déplacer vers...
- handleMove(null)}
- >
-
- Racine
-
- {(foldersData ?? []).map((folder) => (
- handleMove(folder.id)}
- >
-
- {folder.name}
-
- ))}
-
-
-
-
- setConfirmDeleteState(null)}
- />
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#f5f5f5',
- },
- center: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- infoText: {
- fontSize: 16,
- color: '#666',
- },
- list: {
- padding: 15,
- paddingBottom: 80,
- },
- empty: {
- paddingVertical: 60,
- alignItems: 'center',
- gap: 12,
- },
- emptyText: {
- fontSize: 16,
- color: '#999',
- },
- selectionBar: {
- backgroundColor: '#fff',
- borderTopWidth: 1,
- borderTopColor: '#e0e0e0',
- paddingHorizontal: 16,
- paddingVertical: 12,
- },
- selectionHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- marginBottom: 12,
- },
- cancelBtn: {
- padding: 4,
- },
- selectionCount: {
- fontSize: 16,
- fontWeight: '600',
- color: '#333',
- },
- deleteBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- paddingVertical: 10,
- borderRadius: 8,
- borderWidth: 1.5,
- borderColor: '#F44336',
- gap: 6,
- },
- deleteText: {
- fontSize: 15,
- fontWeight: '600',
- color: '#F44336',
- },
- modalOverlay: {
- flex: 1,
- backgroundColor: 'rgba(0,0,0,0.4)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- modalContent: {
- backgroundColor: '#fff',
- borderRadius: 12,
- padding: 20,
- width: '80%',
- },
- modalTitle: {
- fontSize: 18,
- fontWeight: '700',
- color: '#333',
- marginBottom: 16,
- },
- modalInput: {
- borderWidth: 1,
- borderColor: '#ddd',
- borderRadius: 8,
- paddingHorizontal: 12,
- paddingVertical: 10,
- fontSize: 16,
- color: '#333',
- marginBottom: 16,
- },
- modalActions: {
- flexDirection: 'row',
- justifyContent: 'flex-end',
- gap: 12,
- },
- modalCancelBtn: {
- paddingHorizontal: 16,
- paddingVertical: 8,
- },
- modalCancelText: {
- fontSize: 15,
- color: '#666',
- },
- modalConfirmBtn: {
- paddingHorizontal: 16,
- paddingVertical: 8,
- backgroundColor: '#1976D2',
- borderRadius: 8,
- },
- modalConfirmDisabled: {
- backgroundColor: '#ccc',
- },
- modalConfirmText: {
- fontSize: 15,
- color: '#fff',
- fontWeight: '600',
- },
- folderOption: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 12,
- paddingHorizontal: 8,
- gap: 10,
- borderBottomWidth: 1,
- borderBottomColor: '#f0f0f0',
- },
- folderOptionText: {
- fontSize: 16,
- color: '#333',
- },
- footer: {
- paddingVertical: 20,
- alignItems: 'center',
- },
- footerText: {
- fontSize: 14,
- color: '#999',
- },
- footerLink: {
- fontSize: 14,
- color: '#1976D2',
- fontWeight: '600',
- },
-});
diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx
deleted file mode 100644
index 3765b4b..0000000
--- a/mobile/app/index.tsx
+++ /dev/null
@@ -1,902 +0,0 @@
-import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
-import { View, FlatList, StyleSheet, TouchableOpacity, Text, KeyboardAvoidingView, Platform, Keyboard, Modal, TextInput, ActivityIndicator } from 'react-native';
-import { useNavigation } from '@react-navigation/native';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { NativeStackNavigationProp } from '@react-navigation/native-stack';
-import { MaterialIcons } from '@expo/vector-icons';
-import { useAddTags, useMoveResources, useFolders, useFiles, useFreeLocalSpace, useCreateFolder } from '../hooks/useFiles';
-import { SelectionPanel } from '../components/SelectionPanel';
-import { UnifiedFileItem, isFolder } from '../types';
-import { SearchBar, SearchFilters, SortState } from '../components/SearchBar';
-import { SortChips } from '../components/SortChips';
-import { FileCard } from '../components/FileCard';
-import { SettingsModal } from '../components/SettingsModal';
-import { ConfirmModal, type ConfirmOption } from '../components/ConfirmModal';
-import { UploadModal } from '../components/UploadModal';
-import { SyncStatusIcon } from '../components/SyncStatusIcon';
-import { NetworkStatusBar } from '../components/NetworkStatusBar';
-import { useSyncQueue } from '../hooks/useSyncQueue';
-import { useUploadQueue } from '../hooks/useUploadQueue';
-import { useAutoSync } from '../hooks/useAutoSync';
-import { safDirectory, SyncMode, SyncGlobalMode } from '../services/safDirectory';
-import { fileStore } from '../services/fileStore';
-import { downloadRegistry } from '../services/downloadRegistry';
-import { useQueryClient } from '@tanstack/react-query';
-import { apiClient } from '../api/client';
-import { ENDPOINTS } from '../constants/api';
-import { useLocalFiles } from '../hooks/useLocalFiles';
-import { deleteAsync } from 'expo-file-system/legacy';
-import { useDebounce } from '../hooks/useDebounce';
-
-const PAGE_SIZE = 100;
-
-type RootStackParamList = {
- Home: undefined;
- Upload: undefined;
- Scan: undefined;
- FileDetail: { fileIds: string[]; initialIndex: number; deviceFiles?: Record };
- FileEdit: { fileIds: string[] };
- Folder: { folderId: string; folderName: string };
- SyncDetail: undefined;
-};
-
-type NavigationProp = NativeStackNavigationProp;
-
-function parseBackendDate(dateStr: string): Date | null {
- if (!dateStr) return null;
- const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/);
- if (!match) return new Date(dateStr);
- return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]), Number(match[6]));
-}
-
-function matchesQuery(file: UnifiedFileItem, query: string, filters: SearchFilters): boolean {
- if (!query) return true;
- const q = query.toLowerCase();
- if (filters.name && file.name.toLowerCase().includes(q)) return true;
- if (filters.ocrText && file.ocrText?.toLowerCase().includes(q)) return true;
- if (!filters.name && !filters.ocrText) {
- if (file.name.toLowerCase().includes(q)) return true;
- if (file.ocrText?.toLowerCase().includes(q)) return true;
- }
- return false;
-}
-
-function compareBySort(a: UnifiedFileItem, b: UnifiedFileItem, sort: SortState): number {
- let cmp = 0;
- if (sort.key === 'name') {
- cmp = a.name.toLowerCase().localeCompare(b.name.toLowerCase());
- } else if (sort.key === 'size') {
- cmp = (a.size ?? 0) - (b.size ?? 0);
- } else {
- const da = parseBackendDate(a.createdAt);
- const db = parseBackendDate(b.createdAt);
- cmp = (da?.getTime() ?? 0) - (db?.getTime() ?? 0);
- }
- return sort.direction === 'asc' ? cmp : -cmp;
-}
-
-export function HomeScreen() {
- const navigation = useNavigation();
- const insets = useSafeAreaInsets();
- const [page, setPage] = useState(1);
- const { data, isLoading, error, isFetching, refetch } = useFiles(null, page, PAGE_SIZE);
- const { pickAndScanRecursive, folders, refreshFolders, discovered } = useLocalFiles();
- const freeLocalSpace = useFreeLocalSpace();
- const queryClient = useQueryClient();
- const [searchQuery, setSearchQuery] = useState('');
- const debouncedSearch = useDebounce(searchQuery, 250);
- const [filters, setFilters] = useState({ name: true, ocrText: true });
- const toggleFilter = useCallback((key: keyof SearchFilters) => {
- setFilters((prev) => ({ ...prev, [key]: !prev[key] }));
- }, []);
- const [sort, setSort] = useState({ key: 'date', direction: 'desc' });
- const listRef = useRef>(null);
- const [keyboardOpen, setKeyboardOpen] = useState(false);
- const [selectedIds, setSelectedIds] = useState>(new Set());
- const [tagModalVisible, setTagModalVisible] = useState(false);
- const [tagModalMode, setTagModalMode] = useState<'tag' | 'folder'>('tag');
- const [tagInput, setTagInput] = useState('');
- const addTags = useAddTags();
- const moveFiles = useMoveResources();
- const createFolder = useCreateFolder();
- const { data: foldersData } = useFolders();
- const [moveModalVisible, setMoveModalVisible] = useState(false);
- const [settingsModalVisible, setSettingsModalVisible] = useState(false);
- const [filterModalVisible, setFilterModalVisible] = useState(false);
- const [uploadModalVisible, setUploadModalVisible] = useState(false);
- const [globalSyncMode, setGlobalSyncMode] = useState(() => safDirectory.getGlobalSyncMode());
- const [globalSyncCellular, setGlobalSyncCellular] = useState(() => safDirectory.getGlobalSyncCellular());
- const [confirmDeleteState, setConfirmDeleteState] = useState<{ message: string; options: ConfirmOption[] } | null>(null);
- const [removeFolderConfirmId, setRemoveFolderConfirmId] = useState(null);
- const { pendingCount, isSyncing } = useSyncQueue();
- const { tasks: uploadTasks } = useUploadQueue();
- useAutoSync();
-
- const loadingMoreRef = useRef(false);
- const listLayoutHeightRef = useRef(0);
- const listContentHeightRef = useRef(0);
-
- const loadMore = useCallback(() => {
- 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);
- }
- }, [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;
- const hasMore = loadedFiles > 0 && loadedFiles < totalFiles;
- const isFiltering = !!debouncedSearch.trim();
-
- useEffect(() => {
- const show = Keyboard.addListener('keyboardDidShow', () => setKeyboardOpen(true));
- const hide = Keyboard.addListener('keyboardDidHide', () => setKeyboardOpen(false));
- return () => { show.remove(); hide.remove(); };
- }, []);
-
- useEffect(() => {
- navigation.setOptions({
- headerRight: () => (
-
-
- t.status === 'uploading')}
- uploadPendingCount={uploadTasks.filter(t => t.status === 'pending' || t.status === 'uploading').length}
- onPress={() => navigation.navigate('SyncDetail')}
- />
- setUploadModalVisible(true)} style={{ padding: 8 }}>
-
-
- setSettingsModalVisible(true)} style={{ marginRight: 4, padding: 8 }}>
-
-
-
- ),
- });
- }, [navigation, pendingCount, isSyncing, uploadTasks]);
-
-
- const selectionMode = selectedIds.size > 0;
-
- const files = data?.data ?? [];
-
- const filteredFiles = useMemo(
- () => {
-
- const search = debouncedSearch.trim();
-
- if ( search ) {
- return files.filter((f) => matchesQuery(f, debouncedSearch, filters));
- }
-
- return files;
-
- },
- [files, debouncedSearch, filters]
- );
-
- const uploadGhostItems = useMemo(() => {
- return uploadTasks
- .filter((t) => t.status === 'pending' || t.status === 'uploading')
- .map((t) => ({
- id: t.id,
- name: t.file.name,
- mimeType: t.file.type,
- size: 0,
- createdAt: new Date(t.createdAt).toISOString(),
- source: 'local' as const,
- syncStatus: 'local' as const,
- localUri: t.file.uri,
- tags: [],
- isFolder: false,
- isDeviceFile: false,
- isUploading: true,
- uploadProgress: t.progress,
- uploadStatus: t.status,
- }));
- }, [uploadTasks]);
-
- const sortedFiles = useMemo(() => {
- const uploadedExistingIds = new Set(
- filteredFiles.map((f) => f.localUri).filter(Boolean)
- );
- const ghosts = uploadGhostItems.filter(
- (g) => g.localUri && !uploadedExistingIds.has(g.localUri)
- );
- const sorted = [...filteredFiles].sort((a, b) => compareBySort(a, b, sort));
- return [...ghosts, ...sorted];
- }, [filteredFiles, uploadGhostItems, sort]);
-
- const displayedCount = sortedFiles.length;
-
- const fileIdToIndex = useMemo(() => {
- const map = new Map();
- sortedFiles.forEach((f, i) => map.set(f.id, i));
- return map;
- }, [sortedFiles]);
-
- const toggleSelection = useCallback((id: string) => {
- setSelectedIds((prev) => {
- const next = new Set(prev);
- if (next.has(id)) next.delete(id);
- else next.add(id);
- return next;
- });
- }, []);
-
- const clearSelection = useCallback(() => {
- setSelectedIds(new Set());
- }, []);
-
- const handleDelete = useCallback(() => {
- const ids = Array.from(selectedIds);
- if (ids.length === 0) return;
- const hasSynced = ids.some((id) => {
- const f = files.find((fi) => fi.id === id);
- return f?.syncStatus === 'synced';
- });
- const label = ids.length === 1 ? 'ce fichier' : `ces ${ids.length} fichiers`;
- const options: ConfirmOption[] = [];
- if (hasSynced) {
- options.push({
- label: 'Du device uniquement',
- onPress: async () => {
- const syncedIds = ids.filter((id) => {
- const f = files.find((fi) => fi.id === id);
- return f?.syncStatus === 'synced';
- });
- if (syncedIds.length > 0) {
- await freeLocalSpace.mutateAsync(syncedIds);
- }
- setSelectedIds(new Set());
- },
- });
- }
- options.push({
- label: 'Du device + serveur',
- destructive: true,
- onPress: async () => {
- for (const id of ids) {
- const f = files.find((fi) => fi.id === id);
- if (f?.backendResourceId) {
- await apiClient.delete(`${ENDPOINTS.RESOURCES}/${f.backendResourceId}`);
- fileStore.deleteByBackendId(f.backendResourceId);
- } else {
- fileStore.deleteById(id);
- }
- if (f?.localUri) {
- try { await deleteAsync(f.localUri, { idempotent: true }); } catch {}
- }
- downloadRegistry.remove(id);
- }
- await queryClient.invalidateQueries({ queryKey: ['resources'] });
- setSelectedIds(new Set());
- },
- });
- options.push({ label: 'Annuler' });
- setConfirmDeleteState({ message: `Supprimer ${label} ?`, options });
- }, [selectedIds, files, freeLocalSpace, queryClient]);
-
- const handleEdit = useCallback(() => {
- const ids = Array.from(selectedIds);
- if (ids.length === 0) return;
- navigation.navigate('FileEdit', { fileIds: ids });
- setSelectedIds(new Set());
- }, [selectedIds, navigation]);
-
- const openTagModal = useCallback((mode: 'tag' | 'folder') => {
- setTagModalMode(mode);
- setTagInput('');
- setTagModalVisible(true);
- }, []);
-
- const handleAddTag = useCallback(async () => {
- const name = tagInput.trim();
- if (!name) return;
- const ids = Array.from(selectedIds);
- for (const id of ids) {
- await addTags.mutateAsync({ fileId: id, tags: [name] });
- }
- setTagModalVisible(false);
- setSelectedIds(new Set());
- }, [tagInput, selectedIds, addTags]);
-
- const handleCreateFolderFromModal = useCallback(async () => {
- const name = tagInput.trim();
- if (!name) return;
- const ids = Array.from(selectedIds);
- try {
- const newFolder = await createFolder.mutateAsync({ name });
- await moveFiles.mutateAsync({ resourceIds: ids, parentResourceId: newFolder.id });
- setTagModalVisible(false);
- setSelectedIds(new Set());
- navigation.navigate('Folder', { folderId: newFolder.id, folderName: newFolder.name });
- } catch {}
- }, [tagInput, selectedIds, createFolder, moveFiles, navigation]);
-
- const handleMove = useCallback(async (folderId: string | null) => {
- const ids = Array.from(selectedIds);
- await moveFiles.mutateAsync({ resourceIds: ids, parentResourceId: folderId });
- setMoveModalVisible(false);
- setSelectedIds(new Set());
- }, [selectedIds, moveFiles]);
-
- const handleToggleFolderVisibility = useCallback((folderId: string) => {
- safDirectory.toggleVisibility(folderId);
- refreshFolders();
- }, [refreshFolders]);
-
- const handleRemoveFolder = useCallback((folderId: string) => {
- setRemoveFolderConfirmId(folderId);
- }, []);
-
- const handleRemoveFolderConfirm = useCallback(() => {
- if (removeFolderConfirmId) {
- safDirectory.removeFolder(removeFolderConfirmId);
- refreshFolders();
- }
- setRemoveFolderConfirmId(null);
- }, [removeFolderConfirmId, refreshFolders]);
-
- const handleAddFolderRecursive = useCallback(async () => {
- setSettingsModalVisible(false);
- await pickAndScanRecursive();
- }, [pickAndScanRecursive]);
-
- const handleUpdateSyncMode = useCallback((folderId: string, mode: SyncMode) => {
- safDirectory.updateSyncMode(folderId, mode);
- refreshFolders();
- }, [refreshFolders]);
-
- const handleUpdateSyncCellular = useCallback((folderId: string, enabled: boolean) => {
- safDirectory.updateSyncCellular(folderId, enabled);
- refreshFolders();
- }, [refreshFolders]);
-
- const handleSetGlobalSyncMode = useCallback((mode: SyncGlobalMode) => {
- safDirectory.setGlobalSyncMode(mode);
- setGlobalSyncMode(mode);
- }, []);
-
- const handleSetGlobalSyncCellular = useCallback((enabled: boolean) => {
- safDirectory.setGlobalSyncCellular(enabled);
- setGlobalSyncCellular(enabled);
- }, []);
-
- const handleItemPress = useCallback((file: UnifiedFileItem) => {
- if (selectionMode) {
- toggleSelection(file.id);
- } else if (isFolder(file)) {
- navigation.navigate('Folder', { folderId: file.id, folderName: file.name });
- } else {
- const deviceFilesMap: Record = {};
- for (const f of sortedFiles) {
- if (f.isDeviceFile && f.localUri) {
- deviceFilesMap[f.id] = { localUri: f.localUri, name: f.name, mimeType: f.mimeType, createdAt: f.createdAt };
- }
- }
- navigation.navigate('FileDetail', {
- fileIds: sortedFiles.map((f) => f.id),
- initialIndex: fileIdToIndex.get(file.id) ?? 0,
- deviceFiles: Object.keys(deviceFilesMap).length > 0 ? deviceFilesMap : undefined,
- });
- }
- }, [selectionMode, toggleSelection, navigation, sortedFiles, fileIdToIndex]);
-
- const handleItemLongPress = useCallback((file: UnifiedFileItem) => {
- if (!selectionMode) {
- toggleSelection(file.id);
- }
- }, [selectionMode, toggleSelection]);
-
- const renderItem = useCallback(({ item }: { item: UnifiedFileItem }) => (
-
- ), [selectedIds, handleItemPress, handleItemLongPress]);
-
- if (isLoading) {
- return (
-
- Chargement...
-
- );
- }
-
- if (error) {
- return (
-
- Erreur de chargement
-
- );
- }
-
- return (
-
- {files.length === 0 && !searchQuery && (
-
- {folders.length > 0 ? (
- <>
-
-
- {folders.length} dossier{folders.length > 1 ? 's' : ''} scanné{folders.length > 1 ? 's' : ''}
-
-
- Tout scanner
-
- >
- ) : (
- <>
-
-
- Scanner un dossier de votre appareil
-
-
- Choisir
-
- >
- )}
-
- )}
-
-
- item.id}
- contentContainerStyle={styles.list}
- keyboardShouldPersistTaps="handled"
- keyboardDismissMode="on-drag"
- 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 ? (
-
- {isFetching ? (
-
- ) : (
-
- Charger plus ({displayedCount}{!isFiltering && `/${totalFiles}`})
-
- )}
-
- ) : displayedCount > 0 ? (
- {displayedCount} fichier{displayedCount > 1 ? 's' : ''}
- ) : null
- }
- ListEmptyComponent={
-
-
- {debouncedSearch ? 'Aucun résultat' : 'Aucun fichier'}
-
-
- }
- renderItem={renderItem}
- />
-
-
- {!selectionMode && (
- setSearchQuery('')}
- filters={filters}
- onFiltersChange={setFilters}
- sort={sort}
- onSettingsPress={() => setFilterModalVisible(true)}
- bottomPadding={keyboardOpen ? insets.bottom+8 : 0}
- />
- )}
-
- {selectionMode ? (
- openTagModal('tag')}
- onFolder={() => openTagModal('folder')}
- onMove={() => setMoveModalVisible(true)}
- insetsBottom={insets.bottom}
- />
- ) : (
-
- {}}>
-
- Accueil
-
-
- navigation.navigate('Scan')}
- >
-
- Scan
-
-
- )}
-
- setTagModalVisible(false)}>
- setTagModalVisible(false)}>
- {}}>
-
- {tagModalMode === 'folder' ? 'Créer un dossier' : 'Ajouter un tag'}
-
-
-
- setTagModalVisible(false)}>
- Annuler
-
-
- {tagModalMode === 'folder' ? 'Créer' : 'Ajouter'}
-
-
-
-
-
-
- setMoveModalVisible(false)}>
- setMoveModalVisible(false)}>
- {}}>
- Déplacer vers...
- handleMove(null)}
- >
-
- Racine
-
- {(foldersData ?? []).map((folder) => (
- handleMove(folder.id)}
- >
-
- {folder.name}
-
- ))}
-
-
-
-
- setFilterModalVisible(false)}>
- setFilterModalVisible(false)}>
- {}}>
- Rechercher dans
- toggleFilter('name')}
- >
-
- Nom
-
- {filters.name && }
-
-
- toggleFilter('ocrText')}
- >
-
- Texte OCR
-
- {filters.ocrText && }
-
-
-
-
-
-
- setUploadModalVisible(false)}
- />
-
- setSettingsModalVisible(false)}
- folders={folders}
- onToggleVisibility={handleToggleFolderVisibility}
- onRemoveFolder={handleRemoveFolder}
- onAddFolderRecursive={handleAddFolderRecursive}
- onUpdateSyncMode={handleUpdateSyncMode}
- onUpdateSyncCellular={handleUpdateSyncCellular}
- globalSyncMode={globalSyncMode}
- onSetGlobalSyncMode={handleSetGlobalSyncMode}
- globalSyncCellular={globalSyncCellular}
- onSetGlobalSyncCellular={handleSetGlobalSyncCellular}
- />
-
- setConfirmDeleteState(null)}
- />
-
- setRemoveFolderConfirmId(null)}
- />
-
- );
-}
-
-const styles = StyleSheet.create({
- loadMoreBtn: {
- alignItems: 'center',
- paddingVertical: 16,
- },
- loadMoreText: {
- fontSize: 14,
- color: '#1976D2',
- fontWeight: '600',
- },
- loadedAllText: {
- textAlign: 'center',
- fontSize: 13,
- color: '#999',
- paddingVertical: 12,
- },
- container: {
- flex: 1,
- backgroundColor: '#f5f5f5',
- },
- permissionBtn: {
- backgroundColor: '#1976D2',
- borderRadius: 8,
- paddingHorizontal: 14,
- paddingVertical: 8,
- },
- permissionBtnText: {
- fontSize: 14,
- color: '#fff',
- fontWeight: '600',
- },
- folderScanBanner: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: '#FFF3E0',
- paddingHorizontal: 16,
- paddingVertical: 14,
- gap: 10,
- },
- folderScanText: {
- flex: 1,
- fontSize: 14,
- color: '#333',
- },
- center: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- infoText: {
- fontSize: 16,
- color: '#666',
- },
- listWrapper: {
- flex: 1,
- },
- list: {
- paddingHorizontal: 15,
- paddingTop: 10,
- paddingBottom: 80,
- },
- empty: {
- paddingVertical: 60,
- alignItems: 'center',
- },
- emptyText: {
- fontSize: 16,
- color: '#999',
- },
- bottomNav: {
- flexDirection: 'row',
- justifyContent: 'space-around',
- paddingVertical: 12,
- backgroundColor: '#fff',
- borderTopWidth: 1,
- borderTopColor: '#e0e0e0',
- },
- navButton: {
- alignItems: 'center',
- padding: 8,
- gap: 4,
- },
- navText: {
- fontSize: 16,
- color: '#1976D2',
- },
- selectionBar: {
- backgroundColor: '#fff',
- borderTopWidth: 1,
- borderTopColor: '#e0e0e0',
- paddingHorizontal: 16,
- paddingTop: 12,
- },
- selectionHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- marginBottom: 12,
- },
- cancelBtn: {
- padding: 4,
- },
- selectionCount: {
- fontSize: 16,
- fontWeight: '600',
- color: '#333',
- },
- selectAllBtn: {
- paddingHorizontal: 8,
- paddingVertical: 4,
- },
- selectAllText: {
- fontSize: 14,
- color: '#1976D2',
- fontWeight: '600',
- },
- selectionActions: {
- flexDirection: 'row',
- gap: 8,
- paddingVertical: 4,
- },
- selectionChip: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingHorizontal: 14,
- paddingVertical: 8,
- borderRadius: 20,
- gap: 6,
- },
- chipText: {
- fontSize: 13,
- fontWeight: '600',
- },
- deleteChip: { backgroundColor: '#FFEBEE' },
- editChip: { backgroundColor: '#E3F2FD' },
- tagChip: { backgroundColor: '#F3E5F5' },
- folderChip: { backgroundColor: '#FFF3E0' },
- moveChip: { backgroundColor: '#E0F2F1' },
- folderOption: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 12,
- paddingHorizontal: 8,
- gap: 10,
- borderBottomWidth: 1,
- borderBottomColor: '#f0f0f0',
- },
- folderOptionText: {
- fontSize: 16,
- color: '#333',
- flex: 1,
- },
- filterCheck: {
- width: 24,
- alignItems: 'flex-end',
- },
- modalOverlay: {
- flex: 1,
- backgroundColor: 'rgba(0,0,0,0.4)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- modalContent: {
- backgroundColor: '#fff',
- borderRadius: 12,
- padding: 20,
- width: '80%',
- },
- modalTitle: {
- fontSize: 18,
- fontWeight: '700',
- color: '#333',
- marginBottom: 16,
- },
- modalInput: {
- borderWidth: 1,
- borderColor: '#ddd',
- borderRadius: 8,
- paddingHorizontal: 12,
- paddingVertical: 10,
- fontSize: 16,
- color: '#333',
- marginBottom: 16,
- },
- modalActions: {
- flexDirection: 'row',
- justifyContent: 'flex-end',
- gap: 12,
- },
- modalCancelBtn: {
- paddingHorizontal: 16,
- paddingVertical: 8,
- },
- modalCancelText: {
- fontSize: 15,
- color: '#666',
- },
- modalConfirmBtn: {
- paddingHorizontal: 16,
- paddingVertical: 8,
- backgroundColor: '#1976D2',
- borderRadius: 8,
- },
- modalConfirmDisabled: {
- backgroundColor: '#ccc',
- },
- modalConfirmText: {
- fontSize: 15,
- color: '#fff',
- fontWeight: '600',
- },
-});
diff --git a/mobile/app/login.tsx b/mobile/app/login.tsx
deleted file mode 100644
index 232a509..0000000
--- a/mobile/app/login.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import React, { useState } from 'react';
-import {
- View,
- Text,
- TextInput,
- TouchableOpacity,
- StyleSheet,
- Alert,
- KeyboardAvoidingView,
- Platform,
- ActivityIndicator,
-} from 'react-native';
-import { useAuth } from '../contexts/AuthContext';
-
-export function LoginScreen({ navigation }: any) {
- const { login } = useAuth();
- const [username, setUsername] = useState('');
- const [password, setPassword] = useState('');
- const [loading, setLoading] = useState(false);
-
- async function handleLogin() {
- if (!username.trim() || !password) {
- Alert.alert('Erreur', 'Veuillez remplir tous les champs');
- return;
- }
-
- setLoading(true);
- try {
- await login(username.trim(), password);
- } catch (error: any) {
- Alert.alert('Erreur', error.message || 'Connexion échouée');
- } finally {
- setLoading(false);
- }
- }
-
- return (
-
-
- Dot.
- Connectez-vous à votre compte
-
-
-
-
-
-
- {loading ? (
-
- ) : (
- Se connecter
- )}
-
-
- navigation.navigate('Register')}
- disabled={loading}
- >
- Pas de compte ? S'inscrire
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#fff',
- },
- inner: {
- flex: 1,
- justifyContent: 'center',
- paddingHorizontal: 32,
- },
- title: {
- fontSize: 32,
- fontWeight: 'bold',
- textAlign: 'center',
- marginBottom: 8,
- },
- subtitle: {
- fontSize: 16,
- color: '#666',
- textAlign: 'center',
- marginBottom: 32,
- },
- input: {
- borderWidth: 1,
- borderColor: '#ddd',
- borderRadius: 8,
- padding: 16,
- fontSize: 16,
- marginBottom: 16,
- },
- button: {
- backgroundColor: '#000',
- borderRadius: 8,
- padding: 16,
- alignItems: 'center',
- marginBottom: 16,
- },
- buttonDisabled: {
- opacity: 0.6,
- },
- buttonText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
- linkButton: {
- alignItems: 'center',
- },
- linkText: {
- color: '#000',
- fontSize: 14,
- },
-});
diff --git a/mobile/app/onboarding.tsx b/mobile/app/onboarding.tsx
deleted file mode 100644
index a68bdb3..0000000
--- a/mobile/app/onboarding.tsx
+++ /dev/null
@@ -1,297 +0,0 @@
-import React, { useState, useCallback, useEffect } from 'react';
-import {
- View,
- Text,
- StyleSheet,
- TouchableOpacity,
- ScrollView,
-} from 'react-native';
-import { useNavigation, CommonActions } from '@react-navigation/native';
-import { MaterialIcons } from '@expo/vector-icons';
-import { ONBOARDING_STEPS, CURRENT_ONBOARDING_VERSION } from '../config/onboarding';
-import { onboardingStorage } from '../services/onboardingStorage';
-import { safDirectory, StoredFolder } from '../services/safDirectory';
-import * as FileSystem from 'expo-file-system/legacy';
-
-function SelectFoldersStep({
- selectedFolders,
- onAddFolder,
- onRemoveFolder,
-}: {
- selectedFolders: StoredFolder[];
- onAddFolder: () => void;
- onRemoveFolder: (folder: StoredFolder) => void;
-}) {
- return (
-
-
-
-
- Ajoutez vos dossiers
-
- Sélectionnez les dossiers que vous souhaitez synchroniser avec Dot.
-
-
- {selectedFolders.length > 0 && (
-
- {selectedFolders.map((f) => (
-
-
-
- {f.name}
-
- onRemoveFolder(f)}
- style={styles.removeBtn}
- >
-
-
-
- ))}
-
- )}
-
-
-
- Ajouter un dossier
-
-
- );
-}
-
-export function OnboardingScreen() {
- const navigation = useNavigation();
- const [currentIndex, setCurrentIndex] = useState(0);
- const [selectedFolders, setSelectedFolders] = useState([]);
- const pendingSteps = onboardingStorage.getPendingSteps();
-
- const step = pendingSteps[currentIndex];
-
- const complete = useCallback(() => {
- onboardingStorage.setCompletedVersion(CURRENT_ONBOARDING_VERSION);
- navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'Home' }] }));
- }, [navigation]);
-
- const handlePickDirectory = useCallback(async () => {
- try {
- const result = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
- if (!result.granted) return;
- const dirUri = result.directoryUri;
- const parts = dirUri.split('/');
- const dirName = decodeURIComponent(parts[parts.length - 1] ?? 'Dossier');
-
- const folder = safDirectory.addFolder(dirUri, dirName);
- setSelectedFolders((prev) => [...prev, folder]);
- } catch (err) {
- console.error('[Onboarding] pickDirectory error:', err);
- }
- }, []);
-
- const handleRemoveFolder = useCallback((folder: StoredFolder) => {
- safDirectory.removeFolder(folder.id);
- setSelectedFolders((prev) => prev.filter((f) => f.id !== folder.id));
- }, []);
-
- const handleNext = useCallback(async () => {
- if (!step) return;
- onboardingStorage.markStepSeen(step.id);
-
- if (currentIndex < pendingSteps.length - 1) {
- setCurrentIndex(currentIndex + 1);
- setSelectedFolders([]);
- } else {
- complete();
- }
- }, [step, currentIndex, pendingSteps.length, complete]);
-
- const handleSkip = useCallback(() => {
- complete();
- }, [complete]);
-
- useEffect(() => {
- if (!step) {
- complete();
- }
- }, [step, complete]);
-
- if (!step) {
- return null;
- }
-
- const isFolderStep = step.action?.type === 'pick_directory';
-
- return (
-
-
-
- Passer
-
-
-
-
- {isFolderStep ? (
-
- ) : (
-
-
-
-
- {step.title}
- {step.description}
-
- )}
-
-
-
-
- {pendingSteps.map((_, i) => (
-
- ))}
-
-
-
-
- {currentIndex < pendingSteps.length - 1 ? 'Suivant' : 'Commencer'}
-
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#fff',
- },
- skipContainer: {
- alignItems: 'flex-end',
- paddingHorizontal: 20,
- paddingTop: 60,
- },
- skipText: {
- fontSize: 16,
- color: '#999',
- },
- scrollContent: {
- flex: 1,
- },
- scrollInner: {
- flexGrow: 1,
- },
- welcomeContent: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- paddingHorizontal: 40,
- },
- folderStepContent: {
- flex: 1,
- alignItems: 'center',
- paddingHorizontal: 40,
- paddingTop: 40,
- },
- iconContainer: {
- width: 120,
- height: 120,
- borderRadius: 60,
- backgroundColor: '#E3F2FD',
- justifyContent: 'center',
- alignItems: 'center',
- marginBottom: 40,
- },
- title: {
- fontSize: 26,
- fontWeight: '700',
- color: '#333',
- textAlign: 'center',
- marginBottom: 16,
- },
- description: {
- fontSize: 16,
- color: '#666',
- textAlign: 'center',
- lineHeight: 24,
- marginBottom: 20,
- },
- folderList: {
- width: '100%',
- marginBottom: 16,
- },
- selectedFolderRow: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: '#fafafa',
- borderRadius: 10,
- paddingHorizontal: 14,
- paddingVertical: 12,
- marginBottom: 8,
- gap: 10,
- },
- selectedFolderName: {
- flex: 1,
- fontSize: 15,
- color: '#333',
- },
- removeBtn: {
- padding: 4,
- },
- actionBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: '#F57C00',
- borderRadius: 12,
- paddingHorizontal: 24,
- paddingVertical: 14,
- gap: 10,
- width: '100%',
- },
- actionBtnText: {
- fontSize: 16,
- color: '#fff',
- fontWeight: '600',
- },
- footer: {
- paddingHorizontal: 40,
- paddingBottom: 60,
- alignItems: 'center',
- gap: 24,
- },
- dots: {
- flexDirection: 'row',
- gap: 8,
- },
- dot: {
- width: 8,
- height: 8,
- borderRadius: 4,
- backgroundColor: '#ddd',
- },
- dotActive: {
- backgroundColor: '#1976D2',
- width: 24,
- },
- nextBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: '#1976D2',
- borderRadius: 12,
- paddingHorizontal: 32,
- paddingVertical: 14,
- gap: 8,
- },
- nextBtnText: {
- fontSize: 16,
- color: '#fff',
- fontWeight: '600',
- },
-});
diff --git a/mobile/app/pending-review.tsx b/mobile/app/pending-review.tsx
deleted file mode 100644
index cc4ceb6..0000000
--- a/mobile/app/pending-review.tsx
+++ /dev/null
@@ -1,350 +0,0 @@
-import React, { useState, useCallback } from 'react';
-import { View, Text, StyleSheet, FlatList, TouchableOpacity, Image, TextInput, Alert, ActivityIndicator, Modal, Dimensions } from 'react-native';
-import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
-import { useBatchStore } from '../hooks/useBatchStore';
-import { usePdfGeneration } from '../hooks/usePdfGeneration';
-import { useUpload } from '../hooks/useUpload';
-import { CapturedPhoto } from '../types';
-
-const NUM_COLUMNS = 3;
-const SCREEN_WIDTH = Dimensions.get('window').width;
-const ITEM_SIZE = (SCREEN_WIDTH - 16 * 2 - (NUM_COLUMNS - 1) * 6) / NUM_COLUMNS;
-
-type PendingReviewRouteParams = {
- PendingReview: { batchId: string; photoIds: string[] };
-};
-
-export function PendingReviewScreen() {
- const route = useRoute>();
- const navigation = useNavigation();
- const { getBatch, addTagToBatch, removeTagFromBatch } = useBatchStore();
- const { generatePdf, generating, progress } = usePdfGeneration();
- const upload = useUpload();
-
- const batch = getBatch(route.params.batchId);
-
- const initialPhotos = (batch?.photos ?? []).filter((p) =>
- route.params.photoIds.includes(p.id)
- );
-
- const [photos, setPhotos] = useState(initialPhotos);
- const [selectedSet, setSelectedSet] = useState>(new Set(initialPhotos.map((p) => p.id)));
- const [batchTags, setBatchTags] = useState(batch?.tags ?? []);
- const [tagInput, setTagInput] = useState('');
- const [previewUri, setPreviewUri] = useState(null);
-
- const orderedPhotos = photos.filter((p) => selectedSet.has(p.id));
-
- const togglePhoto = (id: string) => {
- setSelectedSet((prev) => {
- const next = new Set(prev);
- if (next.has(id)) next.delete(id);
- else next.add(id);
- return next;
- });
- };
-
- const handleAddTag = () => {
- const tag = tagInput.trim().toLowerCase();
- if (!tag || batchTags.includes(tag)) return;
- setBatchTags((prev) => [...prev, tag]);
- addTagToBatch(route.params.batchId, tag);
- setTagInput('');
- };
-
- const handleRemoveTag = (tag: string) => {
- setBatchTags((prev) => prev.filter((t) => t !== tag));
- removeTagFromBatch(route.params.batchId, tag);
- };
-
- const handleFinalize = async () => {
- if (orderedPhotos.length === 0) {
- Alert.alert('Aucune photo', 'Sélectionnez au moins une photo');
- return;
- }
-
- const pdfUri = await generatePdf(orderedPhotos.map((p) => ({ uri: p.uri })));
- if (!pdfUri) {
- Alert.alert('Erreur', 'Échec de la génération du PDF');
- return;
- }
-
- Alert.alert(
- 'PDF généré',
- 'Voulez-vous uploader le fichier ?',
- [
- { text: 'Annuler', style: 'cancel', onPress: () => navigation.goBack() },
- {
- text: 'Uploader',
- onPress: async () => {
- try {
- const pdfName = `${batch?.name ?? 'document'}.pdf`;
- const pdfUriClean = pdfUri.startsWith('file://') ? pdfUri : 'file://' + pdfUri;
- await upload.mutateAsync([
- { uri: pdfUriClean, type: 'application/pdf', name: pdfName },
- ]);
- Alert.alert('Succès', 'PDF uploadé avec succès', [
- { text: 'OK', onPress: () => navigation.navigate('Home' as never) },
- ]);
- } catch (e: any) {
- const msg = e?.message || e?.toString() || "Erreur inconnue";
- Alert.alert('Erreur', `Échec de l'upload du PDF: ${msg}`);
- }
- },
- },
- ]
- );
- };
-
- if (!batch) {
- return (
-
- Lot introuvable
-
- );
- }
-
- const renderPhoto = ({ item, index }: { item: CapturedPhoto; index: number }) => {
- const isSelected = selectedSet.has(item.id);
- const pageNumber = isSelected
- ? orderedPhotos.findIndex((p) => p.id === item.id) + 1
- : null;
-
- return (
- togglePhoto(item.id)}
- onLongPress={() => setPreviewUri(item.uri)}
- >
-
- {isSelected && (
-
- {pageNumber}
-
- )}
-
- );
- };
-
- return (
-
-
- Réorganiser les photos
-
- {orderedPhotos.length}/{photos.length} sélectionnée{orderedPhotos.length > 1 ? 's' : ''}
-
-
-
- item.id}
- contentContainerStyle={styles.grid}
- columnWrapperStyle={styles.gridRow}
- renderItem={renderPhoto}
- />
-
-
- Tags
-
- {batchTags.map((tag) => (
- handleRemoveTag(tag)}>
- {tag} ✕
-
- ))}
-
-
-
-
- +
-
-
-
-
-
- {generating && (
-
-
- Génération du PDF... {progress}%
-
- )}
-
- {generating ? (
-
- ) : (
- 📄 Finaliser et uploader le PDF
- )}
-
-
-
- setPreviewUri(null)}>
- setPreviewUri(null)}>
- {previewUri && (
-
- )}
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#fff',
- },
- center: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- errorText: {
- fontSize: 16,
- color: '#666',
- },
- header: {
- padding: 16,
- borderBottomWidth: 1,
- borderBottomColor: '#e0e0e0',
- },
- title: {
- fontSize: 20,
- fontWeight: '700',
- marginBottom: 4,
- },
- subtitle: {
- fontSize: 14,
- color: '#666',
- },
- grid: {
- padding: 16,
- },
- gridRow: {
- gap: 6,
- },
- gridItem: {
- width: ITEM_SIZE,
- height: ITEM_SIZE,
- borderRadius: 6,
- overflow: 'hidden',
- backgroundColor: '#f0f0f0',
- marginBottom: 6,
- },
- thumb: {
- width: '100%',
- height: '100%',
- },
- selectedOverlay: {
- ...StyleSheet.absoluteFill,
- backgroundColor: 'rgba(25,118,210,0.35)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- pageNumber: {
- color: '#fff',
- fontSize: 28,
- fontWeight: '800',
- },
- tagSection: {
- padding: 16,
- borderTopWidth: 1,
- borderTopColor: '#e0e0e0',
- },
- sectionTitle: {
- fontSize: 16,
- fontWeight: '600',
- marginBottom: 8,
- },
- tagRow: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 6,
- marginBottom: 8,
- },
- tagChip: {
- backgroundColor: '#E3F2FD',
- paddingHorizontal: 10,
- paddingVertical: 4,
- borderRadius: 12,
- },
- tagText: {
- fontSize: 13,
- color: '#1976D2',
- },
- tagInputRow: {
- flexDirection: 'row',
- gap: 8,
- },
- tagInput: {
- flex: 1,
- borderWidth: 1,
- borderColor: '#e0e0e0',
- borderRadius: 8,
- padding: 10,
- fontSize: 14,
- },
- tagAddBtn: {
- width: 40,
- height: 40,
- borderRadius: 8,
- backgroundColor: '#1976D2',
- justifyContent: 'center',
- alignItems: 'center',
- },
- tagAddText: {
- color: '#fff',
- fontSize: 20,
- fontWeight: '700',
- },
- footer: {
- padding: 16,
- borderTopWidth: 1,
- borderTopColor: '#e0e0e0',
- gap: 8,
- },
- progressRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
- progressText: {
- fontSize: 14,
- color: '#666',
- },
- finalizeBtn: {
- backgroundColor: '#1976D2',
- paddingHorizontal: 24,
- paddingVertical: 14,
- borderRadius: 8,
- alignItems: 'center',
- },
- finalizeBtnDisabled: {
- opacity: 0.6,
- },
- finalizeText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
- modalOverlay: {
- flex: 1,
- backgroundColor: 'rgba(0,0,0,0.9)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- previewImage: {
- width: SCREEN_WIDTH * 0.95,
- height: '80%',
- },
-});
diff --git a/mobile/app/register.tsx b/mobile/app/register.tsx
deleted file mode 100644
index 58e55a9..0000000
--- a/mobile/app/register.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import React, { useState } from 'react';
-import {
- View,
- Text,
- TextInput,
- TouchableOpacity,
- StyleSheet,
- Alert,
- KeyboardAvoidingView,
- Platform,
- ActivityIndicator,
-} from 'react-native';
-import { useAuth } from '../contexts/AuthContext';
-
-export function RegisterScreen({ navigation }: any) {
- const { register } = useAuth();
- const [username, setUsername] = useState('');
- const [password, setPassword] = useState('');
- const [confirmPassword, setConfirmPassword] = useState('');
- const [loading, setLoading] = useState(false);
-
- async function handleRegister() {
- if (!username.trim() || !password || !confirmPassword) {
- Alert.alert('Erreur', 'Veuillez remplir tous les champs');
- return;
- }
-
- if (password !== confirmPassword) {
- Alert.alert('Erreur', 'Les mots de passe ne correspondent pas');
- return;
- }
-
- if (password.length < 8) {
- Alert.alert('Erreur', 'Le mot de passe doit contenir au moins 8 caractères');
- return;
- }
-
- setLoading(true);
- try {
- await register(username.trim(), password);
- } catch (error: any) {
- Alert.alert('Erreur', error.message || "Inscription échouée");
- } finally {
- setLoading(false);
- }
- }
-
- return (
-
-
- Dot.
- Créez votre compte
-
-
-
-
-
-
-
-
- {loading ? (
-
- ) : (
- S'inscrire
- )}
-
-
- navigation.navigate('Login')}
- disabled={loading}
- >
- Déjà un compte ? Se connecter
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#fff',
- },
- inner: {
- flex: 1,
- justifyContent: 'center',
- paddingHorizontal: 32,
- },
- title: {
- fontSize: 32,
- fontWeight: 'bold',
- textAlign: 'center',
- marginBottom: 8,
- },
- subtitle: {
- fontSize: 16,
- color: '#666',
- textAlign: 'center',
- marginBottom: 32,
- },
- input: {
- borderWidth: 1,
- borderColor: '#ddd',
- borderRadius: 8,
- padding: 16,
- fontSize: 16,
- marginBottom: 16,
- },
- button: {
- backgroundColor: '#000',
- borderRadius: 8,
- padding: 16,
- alignItems: 'center',
- marginBottom: 16,
- },
- buttonDisabled: {
- opacity: 0.6,
- },
- buttonText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
- linkButton: {
- alignItems: 'center',
- },
- linkText: {
- color: '#000',
- fontSize: 14,
- },
-});
diff --git a/mobile/app/scan.tsx b/mobile/app/scan.tsx
deleted file mode 100644
index 94ee216..0000000
--- a/mobile/app/scan.tsx
+++ /dev/null
@@ -1,273 +0,0 @@
-import React, { useEffect } from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
-import { useNavigation } from '@react-navigation/native';
-import { NativeStackNavigationProp } from '@react-navigation/native-stack';
-import { Camera } from 'react-native-vision-camera';
-import { useCameraCapture } from '../hooks/useCameraCapture';
-import { UploadProgress } from '../components/UploadProgress';
-import { PhotoThumbnailStrip } from '../components/PhotoThumbnailStrip';
-import { useBatchStore } from '../hooks/useBatchStore';
-
-type RootStackParamList = {
- Home: undefined;
- Upload: undefined;
- Scan: undefined;
- Search: undefined;
- BatchReview: { batchId: string };
-};
-
-type NavigationProp = NativeStackNavigationProp;
-
-export function ScanScreen() {
- const navigation = useNavigation();
- const {
- cameraRef,
- hasPermission,
- requestPermission,
- device,
- photoOutput,
- capturePhoto,
- captureStatus,
- captureError,
- isActive,
- torchMode,
- toggleTorch,
- onStarted,
- onStopped,
- capturedPhotos,
- removeCapturedPhoto,
- clearCapturedPhotos,
- capturedCount,
- } = useCameraCapture();
-
- const { saveBatch } = useBatchStore();
-
- useEffect(() => {
- if (!hasPermission) {
- requestPermission();
- }
- }, [hasPermission, requestPermission]);
-
- const statusToUploadProgress: Record = {
- idle: 'idle',
- capturing: 'uploading',
- uploading: 'uploading',
- success: 'success',
- error: 'error',
- };
-
- const finishBatch = () => {
- if (capturedPhotos.length === 0) return;
-
- const batchId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
- const now = new Date();
- const name = `Lot du ${now.toLocaleDateString('fr-FR')} ${now.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`;
-
- saveBatch({
- id: batchId,
- name,
- createdAt: now.toISOString(),
- photos: capturedPhotos,
- tags: [],
- });
-
- clearCapturedPhotos();
- navigation.navigate('BatchReview', { batchId });
- };
-
- if (!hasPermission) {
- return (
-
- Permission caméra requise
-
- Accorder l'accès
-
-
- );
- }
-
- if (!device) {
- return (
-
-
- Caméra initialisation...
-
- );
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
- {capturedCount > 0 && (
-
- {capturedCount} photo{capturedCount > 1 ? 's' : ''} prise{capturedCount > 1 ? 's' : ''}
-
- )}
-
-
-
-
-
-
- {torchMode === 'on' ? '🔦' : '💡'}
-
-
-
-
-
-
- {capturedCount > 0 ? (
-
- ✓
-
- ) : (
-
- )}
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#000',
- },
- center: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: '#000',
- padding: 24,
- },
- permissionText: {
- fontSize: 18,
- color: '#fff',
- textAlign: 'center',
- marginBottom: 24,
- },
- permissionButton: {
- backgroundColor: '#1976D2',
- paddingHorizontal: 32,
- paddingVertical: 14,
- borderRadius: 8,
- },
- permissionButtonText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
- loadingText: {
- fontSize: 16,
- color: '#fff',
- marginTop: 16,
- },
- viewfinderOverlay: {
- ...StyleSheet.absoluteFill,
- justifyContent: 'center',
- alignItems: 'center',
- },
- viewfinderFrame: {
- width: '85%',
- maxWidth: 400,
- aspectRatio: 3 / 4,
- borderWidth: 2,
- borderColor: 'rgba(255,255,255,0.6)',
- borderRadius: 12,
- },
- topOverlay: {
- position: 'absolute',
- top: 60,
- left: 16,
- right: 16,
- },
- batchInfo: {
- backgroundColor: 'rgba(0,0,0,0.5)',
- borderRadius: 8,
- padding: 8,
- marginTop: 8,
- alignItems: 'center',
- },
- batchInfoText: {
- color: '#fff',
- fontSize: 14,
- fontWeight: '600',
- },
- bottomBar: {
- position: 'absolute',
- bottom: 50,
- left: 0,
- right: 0,
- flexDirection: 'row',
- justifyContent: 'space-around',
- alignItems: 'center',
- paddingHorizontal: 32,
- },
- torchButton: {
- width: 48,
- height: 48,
- borderRadius: 24,
- backgroundColor: 'rgba(255,255,255,0.2)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- torchIcon: {
- fontSize: 24,
- },
- captureButton: {
- width: 76,
- height: 76,
- borderRadius: 38,
- borderWidth: 4,
- borderColor: '#fff',
- justifyContent: 'center',
- alignItems: 'center',
- },
- captureButtonDisabled: {
- opacity: 0.5,
- },
- captureInner: {
- width: 60,
- height: 60,
- borderRadius: 30,
- backgroundColor: '#fff',
- },
- finishButton: {
- width: 48,
- height: 48,
- borderRadius: 24,
- backgroundColor: '#4CAF50',
- justifyContent: 'center',
- alignItems: 'center',
- },
- finishText: {
- color: '#fff',
- fontSize: 22,
- fontWeight: '700',
- },
-});
diff --git a/mobile/app/search.tsx b/mobile/app/search.tsx
deleted file mode 100644
index 1c39f09..0000000
--- a/mobile/app/search.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React, { useState } from 'react';
-import { View, TextInput, FlatList, StyleSheet, Text } from 'react-native';
-import { useSearch } from '../hooks/useSearch';
-import { FileCard } from '../components/FileCard';
-import { FileItem } from '../types';
-
-export function SearchScreen() {
- const [query, setQuery] = useState('');
- const { data, isLoading } = useSearch(query);
-
- const renderItem = ({ item }: { item: FileItem }) => (
- console.log('File pressed:', file.id)}
- />
- );
-
- return (
-
-
-
- {isLoading && Recherche en cours...}
-
- item.id}
- contentContainerStyle={styles.list}
- />
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#f5f5f5',
- },
- input: {
- backgroundColor: '#fff',
- padding: 12,
- margin: 16,
- borderRadius: 8,
- fontSize: 16,
- borderWidth: 1,
- borderColor: '#e0e0e0',
- },
- loading: {
- textAlign: 'center',
- color: '#666',
- marginBottom: 8,
- },
- list: {
- padding: 16,
- },
-});
diff --git a/mobile/app/sync-detail.tsx b/mobile/app/sync-detail.tsx
deleted file mode 100644
index 5d98678..0000000
--- a/mobile/app/sync-detail.tsx
+++ /dev/null
@@ -1,490 +0,0 @@
-import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react';
-import {
- View,
- Text,
- StyleSheet,
- FlatList,
- TouchableOpacity,
- ActivityIndicator,
- Alert,
- Animated,
- Easing,
-} from 'react-native';
-import { MaterialIcons } from '@expo/vector-icons';
-import { fileStore, FileRecord } from '../services/fileStore';
-import { useSyncQueue, useSyncProgress } from '../hooks/useSyncQueue';
-import { useAutoSync } from '../hooks/useAutoSync';
-import { useSyncPush } from '../hooks/useSyncPush';
-import { useUploadQueue } from '../hooks/useUploadQueue';
-import { UploadTask } from '../services/uploadQueue';
-
-function uploadStatusIcon(task: UploadTask) {
- switch (task.status) {
- case 'pending':
- return ;
- case 'uploading':
- return ;
- case 'done':
- return ;
- case 'error':
- return ;
- }
-}
-
-function SpinningSyncIcon({ size, color }: { size: number; color: string }) {
- const spin = useRef(new Animated.Value(0)).current;
-
- useEffect(() => {
- const animation = Animated.loop(
- Animated.timing(spin, {
- toValue: 1,
- duration: 1200,
- easing: Easing.linear,
- useNativeDriver: true,
- })
- );
- animation.start();
- return () => animation.stop();
- }, [spin]);
-
- const rotate = spin.interpolate({
- inputRange: [0, 1],
- outputRange: ['0deg', '360deg'],
- });
-
- return (
-
-
-
- );
-}
-
-type PendingFile = { id: string; name: string };
-
-const PendingSyncCard = React.memo(function PendingSyncCard({
- syncing,
- pendingCount,
- shortList,
- onSyncPress,
- onCancel,
-}: {
- syncing: boolean;
- pendingCount: number;
- shortList: PendingFile[] | null;
- onSyncPress: () => void;
- onCancel: () => void;
-}) {
- return (
-
-
-
- {syncing ? (
-
- ) : (
-
- )}
-
-
-
- {syncing
- ? 'Synchronisation en cours...'
- : pendingCount > 1
- ? `Vous avez ${pendingCount} fichiers locaux pouvant être synchronisés`
- : 'Vous avez 1 fichier local pouvant être synchronisé'}
-
-
- {syncing
- ? shortList && shortList[0]
- ? `En cours : ${shortList[0].name}`
- : 'Synchronisation en cours...'
- : 'Appuyer maintenant pour les synchroniser'}
-
-
- {!syncing && }
-
-
- {syncing && shortList && shortList.length > 0 && (
-
- {shortList.map((f, i) => (
-
- {i === 0 ? (
-
- ) : (
-
- )}
-
- {f.name}
-
-
- ))}
-
- )}
-
- {syncing && (
-
-
- Arrêter la synchronisation
-
- )}
-
- );
-});
-
-export function SyncDetailScreen() {
- const { pendingCount, isSyncing, refresh } = useSyncQueue();
- const { syncProgress } = useSyncProgress();
- const { syncManually, cancelSync } = useAutoSync();
- const { push } = useSyncPush();
- const { tasks: uploadTasks, retry, retryAll } = useUploadQueue();
-
- const [listVersion, setListVersion] = useState(0);
- const bumpList = useCallback(() => setListVersion((v) => v + 1), []);
-
- useEffect(() => {
- const interval = setInterval(bumpList, 5000);
- return () => clearInterval(interval);
- }, [bumpList]);
-
- const errorFiles = useMemo(() => {
- return fileStore.getErrorFiles();
- }, [listVersion]);
-
- const uploadErrors = uploadTasks.filter((t) => t.status === 'error');
-
- const handleSyncAll = useCallback(async () => {
- for (const f of fileStore.getErrorFiles()) {
- fileStore.resetSyncError(f.id);
- }
- await syncManually();
- try {
- const { getStoredDeviceServerId } = await import('../hooks/useDeviceRegistration');
- const serverId = await getStoredDeviceServerId();
- if (serverId) {
- await push(serverId);
- }
- } catch { }
- refresh();
- bumpList();
- }, [syncManually, push, refresh, bumpList]);
-
- const handleSyncButtonPress = useCallback(async () => {
- if (uploadErrors.length > 0) {
- retryAll();
- }
- await handleSyncAll();
- }, [uploadErrors.length, retryAll, handleSyncAll]);
-
- const handleErrorFilePress = useCallback((file: FileRecord) => {
- Alert.alert(
- 'Fichier en erreur',
- 'Réessayer la synchronisation de ce fichier ?',
- [
- { text: 'Annuler', style: 'cancel' },
- {
- text: 'Réessayer',
- onPress: () => {
- fileStore.resetSyncError(file.id);
- syncManually().finally(() => {
- refresh();
- bumpList();
- });
- },
- },
- ],
- );
- }, [syncManually, refresh, bumpList]);
-
- const handleTaskPress = useCallback((task: UploadTask) => {
- if (task.status !== 'error') return;
- Alert.alert(
- 'Erreur d\'upload',
- task.error || 'Erreur inconnue',
- [
- { text: 'Annuler', style: 'cancel' },
- { text: 'Réessayer', onPress: () => retry(task.id) },
- ],
- );
- }, [retry]);
-
- const syncing = !!syncProgress;
- const hasUploads = uploadTasks.length > 0;
- const hasPending = pendingCount > 0 || syncing;
- const hasErrors = errorFiles.length > 0;
- const hasContent = hasUploads || hasPending || hasErrors;
-
- const shortList = useMemo(() => {
- if (!syncProgress) return null;
- const { files, currentIndex } = syncProgress;
- const start = Math.max(0, currentIndex);
- return files.slice(start, start + 5);
- }, [syncProgress]);
-
- return (
-
- {!hasContent ? (
-
-
- Tout est synchronisé
-
- Aucun fichier en attente
-
-
- ) : (
- ({ type: 'upload' as const, data: t })),
- ...(hasErrors ? [{ type: 'section', label: 'Fichiers en erreur' } as const] : []),
- ...errorFiles.map((f) => ({ type: 'error' as const, data: f })),
- ...(hasPending ? [{ type: 'pendingCard' } as const] : []),
- ]}
- keyExtractor={(item) =>
- item.type === 'section' || item.type === 'pendingCard'
- ? item.type === 'pendingCard'
- ? 'pending-card'
- : item.label
- : item.data.id
- }
- extraData={[isSyncing, syncProgress]}
- renderItem={({ item }) => {
- if (item.type === 'section') {
- return {item.label};
- }
- if (item.type === 'pendingCard') {
- return (
-
- );
- }
- if (item.type === 'upload') {
- const task = item.data;
- return (
- handleTaskPress(task)}
- activeOpacity={task.status === 'error' ? 0.6 : 1}
- >
- {uploadStatusIcon(task)}
-
- {task.file.name}
- {task.status === 'uploading' && (
-
-
-
- )}
- {task.status === 'error' && task.error && (
- {task.error}
- )}
- {task.status === 'done' && (
- Upload terminé
- )}
- {task.status === 'pending' && (
- En attente
- )}
-
-
- );
- }
- if (item.type === 'error') {
- const file = item.data;
- return (
- handleErrorFilePress(file)}
- activeOpacity={0.6}
- >
-
-
- {file.name}
-
- Échec de synchronisation · toucher pour réessayer
-
-
-
-
- );
- }
- return null;
- }}
- contentContainerStyle={styles.list}
- />
- )}
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#f5f5f5',
- },
- list: {
- padding: 16,
- },
- headerText: {
- fontSize: 14,
- color: '#666',
- marginBottom: 12,
- },
- fileRow: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: '#fff',
- borderRadius: 10,
- padding: 12,
- marginBottom: 8,
- gap: 12,
- },
- pendingCard: {
- backgroundColor: '#fff',
- borderRadius: 14,
- padding: 16,
- marginBottom: 8,
- gap: 14,
- shadowColor: '#000',
- shadowOpacity: 0.06,
- shadowRadius: 8,
- shadowOffset: { width: 0, height: 2 },
- elevation: 2,
- },
- pendingCardTop: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 14,
- },
- pendingCardIcon: {
- width: 48,
- height: 48,
- borderRadius: 24,
- backgroundColor: '#E3F2FD',
- justifyContent: 'center',
- alignItems: 'center',
- },
- pendingCardInfo: {
- flex: 1,
- },
- pendingCardTitle: {
- fontSize: 15,
- color: '#333',
- fontWeight: '600',
- },
- pendingCardSubtitle: {
- fontSize: 13,
- color: '#1976D2',
- marginTop: 4,
- },
- pendingList: {
- borderTopWidth: 1,
- borderTopColor: '#f0f0f0',
- paddingTop: 12,
- gap: 8,
- },
- pendingRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 10,
- },
- pendingRowText: {
- flex: 1,
- fontSize: 13,
- color: '#999',
- },
- pendingRowActive: {
- color: '#1976D2',
- fontWeight: '500',
- },
- stopBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: 8,
- backgroundColor: '#E53935',
- borderRadius: 8,
- paddingVertical: 10,
- marginTop: 4,
- },
- stopBtnText: {
- fontSize: 14,
- color: '#fff',
- fontWeight: '600',
- },
- fileInfo: {
- flex: 1,
- },
- fileName: {
- fontSize: 15,
- color: '#333',
- fontWeight: '500',
- },
- fileMeta: {
- fontSize: 12,
- color: '#999',
- marginTop: 2,
- },
- localBadge: {
- width: 28,
- height: 28,
- borderRadius: 14,
- backgroundColor: '#f5f5f5',
- justifyContent: 'center',
- alignItems: 'center',
- },
- fileRowError: {
- backgroundColor: '#FFF0F0',
- },
- progressBar: {
- height: 4,
- backgroundColor: '#E0E0E0',
- borderRadius: 2,
- marginTop: 6,
- overflow: 'hidden',
- },
- progressFill: {
- height: '100%',
- backgroundColor: '#1976D2',
- borderRadius: 2,
- },
- errorText: {
- fontSize: 12,
- color: '#E53935',
- marginTop: 2,
- },
- doneText: {
- fontSize: 12,
- color: '#4CAF50',
- marginTop: 2,
- },
- pendingText: {
- fontSize: 12,
- color: '#FFA000',
- marginTop: 2,
- },
- empty: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- gap: 12,
- },
- emptyTitle: {
- fontSize: 18,
- fontWeight: '600',
- color: '#333',
- },
- emptySubtitle: {
- fontSize: 14,
- color: '#999',
- },
-});
diff --git a/mobile/babel.config.js b/mobile/babel.config.js
index d872de3..5f462d8 100644
--- a/mobile/babel.config.js
+++ b/mobile/babel.config.js
@@ -2,6 +2,5 @@ module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
- plugins: ['react-native-reanimated/plugin'],
};
-};
+};
\ No newline at end of file
diff --git a/mobile/components/ConfirmModal.tsx b/mobile/components/ConfirmModal.tsx
deleted file mode 100644
index 5a2c964..0000000
--- a/mobile/components/ConfirmModal.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import React from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, Modal } from 'react-native';
-
-export interface ConfirmOption {
- label: string;
- onPress?: () => void;
- destructive?: boolean;
-}
-
-interface ConfirmModalProps {
- visible: boolean;
- title: string;
- message?: string;
- options: ConfirmOption[];
- onClose: () => void;
-}
-
-export function ConfirmModal({ visible, title, message, options, onClose }: ConfirmModalProps) {
- const stacked = options.length > 2;
-
- return (
-
-
- {}}>
-
- {title}
- {message ? {message} : null}
-
- {options.map((opt, i) => (
- {
- onClose();
- opt.onPress?.();
- }}
- >
-
- {opt.label}
-
-
- ))}
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- overlay: {
- flex: 1,
- backgroundColor: 'rgba(0,0,0,0.4)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- container: {
- backgroundColor: '#fff',
- borderRadius: 16,
- paddingHorizontal: 20,
- paddingTop: 12,
- paddingBottom: 20,
- width: '85%',
- },
- handle: {
- width: 36,
- height: 4,
- borderRadius: 2,
- backgroundColor: '#ddd',
- alignSelf: 'center',
- marginBottom: 16,
- },
- title: {
- fontSize: 18,
- fontWeight: '700',
- color: '#333',
- marginBottom: 8,
- },
- message: {
- fontSize: 14,
- color: '#666',
- lineHeight: 20,
- marginBottom: 20,
- },
- optionsContainer: {
- flexDirection: 'row',
- gap: 10,
- },
- optionsStacked: {
- flexDirection: 'column',
- gap: 0,
- },
- sideBtn: {
- flex: 1,
- paddingVertical: 12,
- borderRadius: 10,
- alignItems: 'center',
- },
- sideDestructive: {
- backgroundColor: '#E53935',
- },
- sideBtnSecondary: {
- backgroundColor: '#f5f5f5',
- },
- sideBtnText: {
- fontSize: 15,
- fontWeight: '600',
- },
- sideDestructiveText: {
- color: '#fff',
- },
- sideBtnSecondaryText: {
- color: '#666',
- },
- stackedBtn: {
- paddingVertical: 14,
- alignItems: 'center',
- },
- stackedBtnBorder: {
- borderBottomWidth: 1,
- borderBottomColor: '#f0f0f0',
- },
- stackedDestructive: {},
- stackedSecondary: {},
- stackedBtnText: {
- fontSize: 16,
- fontWeight: '600',
- },
- stackedDestructiveText: {
- color: '#E53935',
- },
- stackedSecondaryText: {
- color: '#333',
- },
-});
diff --git a/mobile/components/FileCard.tsx b/mobile/components/FileCard.tsx
deleted file mode 100644
index eef9f3d..0000000
--- a/mobile/components/FileCard.tsx
+++ /dev/null
@@ -1,251 +0,0 @@
-import React from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
-import { Image } from 'expo-image';
-import { MaterialIcons } from '@expo/vector-icons';
-import { FileItem } from '../types';
-import { TagChip } from './TagChip';
-import { SyncStatusBadge } from './SyncStatusBadge';
-import type { ComponentProps } from 'react';
-
-type IconName = ComponentProps['name'];
-
-interface FileCardProps {
- file: FileItem;
- onPress?: (file: FileItem) => void;
- onLongPress?: (file: FileItem) => void;
- selected?: boolean;
-}
-
-function formatSize(bytes: number) {
- if (bytes < 1024) return `${bytes} B`;
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
-}
-
-function getFileInfo(mimeType: string, fileName: string): { icon: IconName; color: string; bg: string } {
- 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' };
- if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('rar') || mimeType.includes('tar')) return { icon: 'folder-zip', color: '#6D4C41', bg: '#EFEBE9' };
- if (mimeType.includes('json') || mimeType.includes('xml')) return { icon: 'code', color: '#37474F', bg: '#ECEFF1' };
- return { icon: 'insert-drive-file', color: '#757575', bg: '#F5F5F5' };
-}
-
-function getExtension(fileName: string): string {
- const ext = fileName.split('.').pop();
- return ext ? ext.toUpperCase() : '';
-}
-
-export const FileCard = React.memo(function FileCard({ file, onPress, onLongPress, selected }: FileCardProps) {
- const info = getFileInfo(file.mimeType, file.name);
- const ext = getExtension(file.name);
-
- const imageUri = file.thumbnailUrl || file.thumbnailLocal || (file.url && file.mimeType?.startsWith('image/') ? file.url : undefined) || (file.localUri && file.mimeType?.startsWith('image/') ? file.localUri : undefined);
- const isFolder = file.isFolder;
- const isUploading = file.isUploading;
-
- return (
- onPress?.(file)}
- onLongPress={() => onLongPress?.(file)}
- delayLongPress={400}
- activeOpacity={0.7}
- >
-
- {isFolder ? (
-
-
-
- ) : imageUri ? (
-
- ) : (
-
- {isUploading ? (
-
- ) : (
- <>
-
- {ext.length <= 4 && {ext}}
- >
- )}
-
- )}
-
- {selected && (
-
-
-
-
-
- )}
-
- {file.syncStatus && !isUploading && (
-
-
-
- )}
-
-
-
-
-
- {file.name}
-
- {!isFolder && file.size > 0 && (
- {formatSize(file.size)}
- )}
-
-
- {file.syncStatus && !isUploading && (
-
-
-
- )}
-
- {isUploading ? (
-
-
-
- Upload {(file.uploadProgress ?? 0)}%
-
-
- ) : file.ocrText ? (
-
- {file.ocrText}
-
- ) : null}
-
- {file.tags && file.tags.length > 0 && (
-
- {file.tags.slice(0, 3).map((tag) => (
-
- ))}
-
- )}
-
-
- );
-});
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- backgroundColor: '#fff',
- borderRadius: 10,
- padding: 10,
- marginBottom: 8,
- gap: 12,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 1 },
- shadowOpacity: 0.05,
- shadowRadius: 4,
- elevation: 1,
- },
- containerSelected: {
- opacity: 0.85,
- borderWidth: 1.5,
- borderColor: '#1976D2',
- },
- thumbnailWrap: {
- width: 64,
- height: 64,
- borderRadius: 8,
- overflow: 'hidden',
- },
- thumbnail: {
- width: 64,
- height: 64,
- borderRadius: 8,
- },
- placeholder: {
- justifyContent: 'center',
- alignItems: 'center',
- gap: 1,
- },
- ext: {
- fontSize: 9,
- fontWeight: '700',
- },
- selectedOverlay: {
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- justifyContent: 'flex-start',
- alignItems: 'flex-end',
- padding: 4,
- },
- checkCircle: {
- width: 22,
- height: 22,
- borderRadius: 11,
- backgroundColor: '#1976D2',
- justifyContent: 'center',
- alignItems: 'center',
- },
- badge: {
- position: 'absolute',
- bottom: 4,
- left: 4,
- },
- body: {
- flex: 1,
- justifyContent: 'center',
- },
- header: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 4,
- gap: 8,
- },
- name: {
- fontSize: 15,
- fontWeight: '600',
- color: '#333',
- flex: 1,
- },
- size: {
- fontSize: 12,
- color: '#999',
- },
- statusRow: {
- flexDirection: 'row',
- marginBottom: 6,
- },
- preview: {
- fontSize: 13,
- color: '#666',
- marginBottom: 6,
- lineHeight: 18,
- },
- uploadRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 6,
- marginBottom: 6,
- },
- uploadText: {
- fontSize: 13,
- color: '#1976D2',
- fontWeight: '500',
- },
- tags: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 4,
- },
-});
diff --git a/mobile/components/FileThumbnail.tsx b/mobile/components/FileThumbnail.tsx
deleted file mode 100644
index 3f72cd8..0000000
--- a/mobile/components/FileThumbnail.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import React from 'react';
-import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
-import { Image } from 'expo-image';
-import { MaterialIcons } from '@expo/vector-icons';
-import type { ComponentProps } from 'react';
-import { SyncStatusBadge } from './SyncStatusBadge';
-import type { SyncStatus } from '../types';
-
-type IconName = ComponentProps['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' };
- if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('rar') || mimeType.includes('tar')) return { icon: 'folder-zip', color: '#6D4C41', bg: '#EFEBE9' };
- if (mimeType.includes('json') || mimeType.includes('xml')) return { icon: 'code', color: '#37474F', bg: '#ECEFF1' };
- 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;
- thumbnailUrl?: string;
- thumbnailLocal?: string;
- mimeType: string;
- fileName: string;
- size: number;
- isLoading?: boolean;
- syncStatus?: SyncStatus;
- isFolder?: boolean;
- isUploading?: boolean;
- uploadProgress?: number;
-}
-
-export const FileThumbnail = React.memo(function FileThumbnail({ uri, thumbnailUrl, thumbnailLocal, mimeType, fileName, size, isLoading, syncStatus, isFolder, isUploading, uploadProgress }: FileThumbnailProps) {
- const info = getFileInfo(mimeType, fileName);
- const ext = getExtension(fileName);
-
- if (isFolder) {
- return (
-
-
-
- );
- }
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- const imageUri = thumbnailUrl || thumbnailLocal || (uri && mimeType.startsWith('image/') ? uri : undefined);
-
- if (imageUri) {
- return (
-
-
- {syncStatus && }
- {isUploading && (
-
-
- {uploadProgress ?? 0}%
-
-
- )}
-
- );
- }
-
- return (
-
- {isUploading ? (
-
-
-
- {uploadProgress ?? 0}%
-
- ) : (
- <>
-
- {ext.length <= 4 && (
- {ext}
- )}
- >
- )}
- {syncStatus && }
-
- );
-});
-
-const styles = StyleSheet.create({
- container: {
- borderRadius: 6,
- justifyContent: 'center',
- alignItems: 'center',
- gap: 2,
- },
- image: {
- borderRadius: 6,
- },
- ext: {
- fontSize: 11,
- fontWeight: '700',
- },
- uploadGhost: {
- alignItems: 'center',
- gap: 4,
- },
- uploadPercent: {
- fontSize: 11,
- fontWeight: '700',
- color: '#1976D2',
- },
- uploadOverlay: {
- ...StyleSheet.absoluteFill,
- backgroundColor: 'rgba(0,0,0,0.45)',
- borderRadius: 6,
- justifyContent: 'center',
- alignItems: 'center',
- gap: 4,
- },
- uploadProgressText: {
- fontSize: 12,
- fontWeight: '700',
- color: '#fff',
- },
- uploadProgressBar: {
- position: 'absolute',
- bottom: 0,
- left: 0,
- height: 3,
- backgroundColor: '#1976D2',
- borderBottomLeftRadius: 6,
- },
-});
diff --git a/mobile/components/NetworkStatusBar.tsx b/mobile/components/NetworkStatusBar.tsx
deleted file mode 100644
index fd3561b..0000000
--- a/mobile/components/NetworkStatusBar.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from 'react';
-import { View, Text, StyleSheet } from 'react-native';
-import { MaterialIcons } from '@expo/vector-icons';
-import { useNetworkStatus } from '../hooks/useNetworkStatus';
-
-export function NetworkStatusBar() {
- const { isOnline } = useNetworkStatus();
-
- if (isOnline) {
- return (
-
-
-
- );
- }
-
- return (
-
-
-
- Offline
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- marginRight: 4,
- padding: 4,
- alignItems: 'center',
- justifyContent: 'center',
- },
- dotOnline: {
- width: 8,
- height: 8,
- borderRadius: 4,
- backgroundColor: '#4CAF50',
- },
- offlineBadge: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: '#E53935',
- borderRadius: 12,
- paddingHorizontal: 8,
- paddingVertical: 4,
- gap: 4,
- },
- offlineText: {
- fontSize: 11,
- fontWeight: '700',
- color: '#fff',
- },
-});
diff --git a/mobile/components/PhotoThumbnailStrip.tsx b/mobile/components/PhotoThumbnailStrip.tsx
deleted file mode 100644
index 7061215..0000000
--- a/mobile/components/PhotoThumbnailStrip.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from 'react';
-import { View, Image, FlatList, StyleSheet, TouchableOpacity, Text } from 'react-native';
-import { CapturedPhoto } from '../types';
-
-interface PhotoThumbnailStripProps {
- photos: CapturedPhoto[];
- onRemove: (id: string) => void;
-}
-
-export function PhotoThumbnailStrip({ photos, onRemove }: PhotoThumbnailStripProps) {
- if (photos.length === 0) return null;
-
- return (
-
- item.id}
- renderItem={({ item }) => (
-
-
- onRemove(item.id)}
- >
- ✕
-
-
- )}
- />
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- position: 'absolute',
- bottom: 120,
- left: 0,
- right: 0,
- height: 80,
- },
- list: {
- paddingHorizontal: 16,
- gap: 8,
- },
- thumb: {
- width: 64,
- height: 64,
- borderRadius: 8,
- overflow: 'hidden',
- backgroundColor: '#333',
- },
- image: {
- width: '100%',
- height: '100%',
- },
- removeButton: {
- position: 'absolute',
- top: 2,
- right: 2,
- width: 20,
- height: 20,
- borderRadius: 10,
- backgroundColor: 'rgba(0,0,0,0.6)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- removeText: {
- color: '#fff',
- fontSize: 11,
- fontWeight: '700',
- },
-});
diff --git a/mobile/components/SearchBar.tsx b/mobile/components/SearchBar.tsx
deleted file mode 100644
index fb9f0c5..0000000
--- a/mobile/components/SearchBar.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import React from 'react';
-import { View, TextInput, TouchableOpacity, Text, StyleSheet } from 'react-native';
-import { MaterialIcons } from '@expo/vector-icons';
-import type { ComponentProps } from 'react';
-
-type IconName = ComponentProps['name'];
-
-export interface SearchFilters {
- name: boolean;
- ocrText: boolean;
-}
-
-export type SortKey = 'date' | 'name' | 'size';
-export type SortDirection = 'asc' | 'desc';
-
-export interface SortState {
- key: SortKey;
- direction: SortDirection;
-}
-
-interface SearchBarProps {
- query: string;
- onQueryChange: (q: string) => void;
- onClear: () => void;
- filters: SearchFilters;
- onFiltersChange: (f: SearchFilters) => void;
- onSettingsPress: () => void;
- sort: SortState;
- bottomPadding?: number;
-}
-
-export function SearchBar({ query, onQueryChange, onClear, filters, onSettingsPress, sort, bottomPadding = 0 }: SearchBarProps) {
- const hasActiveFilter = filters.name || filters.ocrText || sort.key !== 'date';
-
- return (
-
-
-
-
-
- {query.length > 0 && (
-
-
-
- )}
-
-
-
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- wrapper: {
- backgroundColor: '#fff',
- borderTopWidth: 1,
- borderTopColor: '#e0e0e0',
- },
- inputRow: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingHorizontal: 12,
- paddingVertical: 10,
- gap: 8,
- },
- inputContainer: {
- flex: 1,
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: '#f5f5f5',
- borderRadius: 10,
- paddingHorizontal: 10,
- },
- searchIcon: {
- marginRight: 6,
- },
- input: {
- flex: 1,
- paddingVertical: 8,
- fontSize: 14,
- color: '#333',
- },
- clearBtn: {
- padding: 4,
- },
- iconBtn: {
- width: 40,
- height: 40,
- borderRadius: 10,
- borderWidth: 1,
- borderColor: '#1976D2',
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: '#fff',
- },
- iconBtnActive: {
- backgroundColor: '#1976D2',
- },
-});
diff --git a/mobile/components/SelectionPanel.tsx b/mobile/components/SelectionPanel.tsx
deleted file mode 100644
index 9955559..0000000
--- a/mobile/components/SelectionPanel.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import React, { useCallback } from 'react';
-import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
-import { MaterialIcons } from '@expo/vector-icons';
-import { Gesture, GestureDetector } from 'react-native-gesture-handler';
-import Animated, { useSharedValue, useAnimatedStyle, withSpring, runOnJS } from 'react-native-reanimated';
-
-const PANEL_HEIGHT = 300;
-const PANEL_HEADER_VISIBLE = 80;
-
-interface SelectionPanelProps {
- selectedCount: number;
- onClose: () => void;
- onDelete?: () => void;
- onEdit?: () => void;
- onTags?: () => void;
- onFolder?: () => void;
- onMove?: () => void;
- insetsBottom: number;
-}
-
-export function SelectionPanel({
- selectedCount,
- onClose,
- onDelete,
- onEdit,
- onTags,
- onFolder,
- onMove,
- insetsBottom,
-}: SelectionPanelProps) {
- const panelOffset = useSharedValue(PANEL_HEIGHT - PANEL_HEADER_VISIBLE);
- const panelStartY = useSharedValue(0);
- const isPanelExpanded = useSharedValue(false);
- const [panelExpanded, setPanelExpanded] = React.useState(false);
-
- const togglePanelJS = useCallback(() => {
- if (isPanelExpanded.value) {
- panelOffset.value = withSpring(PANEL_HEIGHT - PANEL_HEADER_VISIBLE);
- isPanelExpanded.value = false;
- setPanelExpanded(false);
- } else {
- panelOffset.value = withSpring(0);
- isPanelExpanded.value = true;
- setPanelExpanded(true);
- }
- }, []);
-
- const panGesture = Gesture.Pan()
- .onStart(() => {
- panelStartY.value = panelOffset.value;
- })
- .onUpdate((event) => {
- const offset = Math.max(0, Math.min(PANEL_HEIGHT - PANEL_HEADER_VISIBLE, panelStartY.value + event.translationY));
- panelOffset.value = offset;
- })
- .onEnd(() => {
- if (panelOffset.value > (PANEL_HEIGHT - PANEL_HEADER_VISIBLE) / 2) {
- panelOffset.value = withSpring(PANEL_HEIGHT - PANEL_HEADER_VISIBLE);
- isPanelExpanded.value = false;
- runOnJS(setPanelExpanded)(false);
- } else {
- panelOffset.value = withSpring(0);
- isPanelExpanded.value = true;
- runOnJS(setPanelExpanded)(true);
- }
- });
-
- const panelAnimatedStyle = useAnimatedStyle(() => ({
- transform: [{ translateY: panelOffset.value }],
- }));
-
- return (
-
-
-
-
-
-
-
-
-
- {selectedCount} sélectionnée{selectedCount > 1 ? 's' : ''}
-
-
- Tout
-
-
-
-
-
-
- {onDelete && (
-
-
- Supprimer
-
- )}
- {onEdit && (
-
-
- Éditer
-
- )}
- {onTags && (
-
-
- Tags
-
- )}
- {onFolder && (
-
-
- Dossier
-
- )}
- {onMove && (
-
-
- Déplacer
-
- )}
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- panel: {
- position: 'absolute',
- bottom: 0,
- left: 0,
- right: 0,
- backgroundColor: '#fff',
- borderTopLeftRadius: 20,
- borderTopRightRadius: 20,
- paddingHorizontal: 20,
- paddingTop: 12,
- height: PANEL_HEIGHT,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: -2 },
- shadowOpacity: 0.1,
- shadowRadius: 8,
- elevation: 10,
- },
- panelHandle: {
- width: 36,
- height: 4,
- borderRadius: 2,
- backgroundColor: '#ddd',
- alignSelf: 'center',
- marginBottom: 12,
- },
- panelHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- marginBottom: 16,
- },
- closeBtn: {
- padding: 4,
- },
- selectionCount: {
- fontSize: 16,
- fontWeight: '700',
- color: '#333',
- },
- selectAllBtn: {
- paddingHorizontal: 8,
- paddingVertical: 4,
- },
- selectAllText: {
- fontSize: 14,
- color: '#1976D2',
- fontWeight: '600',
- },
- panelBody: {
- flex: 1,
- },
- actionsGrid: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 10,
- },
- actionBtn: {
- width: '47%',
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: 10,
- paddingVertical: 14,
- borderRadius: 12,
- backgroundColor: '#f5f5f5',
- },
- actionLabel: {
- fontSize: 15,
- fontWeight: '600',
- },
- deleteBtn: {},
- editBtn: {},
- tagBtn: {},
- folderBtn: {},
- moveBtn: {},
-});
diff --git a/mobile/components/SettingsModal.tsx b/mobile/components/SettingsModal.tsx
deleted file mode 100644
index cf9011b..0000000
--- a/mobile/components/SettingsModal.tsx
+++ /dev/null
@@ -1,670 +0,0 @@
-import React, { useState, useCallback } from 'react';
-import {
- View,
- Text,
- StyleSheet,
- TouchableOpacity,
- Modal,
- ScrollView,
-} from 'react-native';
-import { MaterialIcons } from '@expo/vector-icons';
-import { StoredFolder, SyncMode, SyncGlobalMode, type FolderSource } from '../services/safDirectory';
-import { ConfirmModal } from './ConfirmModal';
-
-type SettingsView = 'menu' | 'folders' | 'sync';
-
-interface SettingsModalProps {
- visible: boolean;
- onClose: () => void;
- folders: StoredFolder[];
- onToggleVisibility: (folderId: string) => void;
- onRemoveFolder: (folderId: string) => void;
- onAddFolderRecursive: () => void;
- onUpdateSyncMode: (folderId: string, mode: SyncMode) => void;
- onUpdateSyncCellular: (folderId: string, enabled: boolean) => void;
- globalSyncMode: SyncGlobalMode;
- onSetGlobalSyncMode: (mode: SyncGlobalMode) => void;
- globalSyncCellular: boolean;
- onSetGlobalSyncCellular: (enabled: boolean) => void;
-}
-
-const SYNC_MODES: { value: SyncMode; label: string; icon: string; color: string }[] = [
- { value: 'none', label: 'Aucun', icon: 'sync-disabled', color: '#999' },
- { value: 'manual', label: 'Manuel', icon: 'sync', color: '#1976D2' },
- { value: 'auto', label: 'Auto', icon: 'sync-problem', color: '#43A047' },
-];
-
-const GLOBAL_MODES: { value: SyncGlobalMode; label: string; description: string; icon: string; color: string }[] = [
- { value: 'off', label: 'Désactivé', description: 'Aucun upload automatique', icon: 'sync-disabled', color: '#999' },
- { value: 'auto', label: 'Automatique', description: 'Tout synchroniser automatiquement', icon: 'sync-problem', color: '#43A047' },
- { value: 'manual', label: 'Par dossier', description: 'Choisir dossier par dossier', icon: 'tune', color: '#1976D2' },
-];
-
-export function SettingsModal({
- visible,
- onClose,
- folders,
- onToggleVisibility,
- onRemoveFolder,
- onAddFolderRecursive,
- onUpdateSyncMode,
- onUpdateSyncCellular,
- globalSyncMode,
- onSetGlobalSyncMode,
- globalSyncCellular,
- onSetGlobalSyncCellular,
-}: SettingsModalProps) {
- const [view, setView] = useState('menu');
- const [confirmRemoveFolder, setConfirmRemoveFolder] = useState(null);
-
- const handleClose = useCallback(() => {
- setView('menu');
- onClose();
- }, [onClose]);
-
- const handleRemoveFolder = useCallback(
- (folder: StoredFolder) => {
- setConfirmRemoveFolder(folder);
- },
- []
- );
-
- const handleRemoveFolderConfirm = useCallback(() => {
- if (confirmRemoveFolder) {
- onRemoveFolder(confirmRemoveFolder.id);
- }
- setConfirmRemoveFolder(null);
- }, [confirmRemoveFolder, onRemoveFolder]);
-
- return (
-
-
- {}}>
-
-
- {view === 'menu' && (
- setView(v)}
- onClose={handleClose}
- />
- )}
-
- {view === 'folders' && (
- setView('menu')}
- onToggleVisibility={onToggleVisibility}
- onRemoveFolder={handleRemoveFolder}
- onAddFolderRecursive={onAddFolderRecursive}
- />
- )}
-
- {view === 'sync' && (
- setView('menu')}
- onUpdateSyncMode={onUpdateSyncMode}
- onUpdateSyncCellular={onUpdateSyncCellular}
- globalSyncMode={globalSyncMode}
- onSetGlobalSyncMode={onSetGlobalSyncMode}
- globalSyncCellular={globalSyncCellular}
- onSetGlobalSyncCellular={onSetGlobalSyncCellular}
- />
- )}
-
-
-
- setConfirmRemoveFolder(null)}
- />
-
- );
-}
-
-function MenuView({ onSelect, onClose }: { onSelect: (v: SettingsView) => void; onClose: () => void }) {
- return (
-
- Paramètres
-
- onSelect('folders')}>
-
-
-
-
- Dossiers
- Gérer les dossiers affichés
-
-
-
-
- onSelect('sync')}>
-
-
-
-
- Synchronisation
- Configurer l'upload automatique
-
-
-
-
-
- Fermer
-
-
- );
-}
-
-function folderIcon(folder: StoredFolder): { name: keyof typeof MaterialIcons.glyphMap; color: string } {
- switch (folder.source) {
- case 'media-library':
- return { name: 'photo-library', color: '#43A047' };
- case 'recursive':
- return { name: 'subdirectory-arrow-right', color: '#8E24AA' };
- default:
- return { name: 'folder', color: '#F57C00' };
- }
-}
-
-function FoldersView({
- folders,
- onBack,
- onToggleVisibility,
- onRemoveFolder,
- onAddFolderRecursive,
-}: {
- folders: StoredFolder[];
- onBack: () => void;
- onToggleVisibility: (id: string) => void;
- onRemoveFolder: (folder: StoredFolder) => void;
- onAddFolderRecursive: () => void;
-}) {
- return (
-
-
-
-
-
- Dossiers
-
-
- {folders.length === 0 ? (
- Aucun dossier configuré
- ) : (
- folders.map((folder) => {
- const icon = folderIcon(folder);
- return (
-
-
-
- {folder.name}
-
- onToggleVisibility(folder.id)}
- style={styles.actionBtn}
- >
-
-
- onRemoveFolder(folder)}
- style={styles.actionBtn}
- >
-
-
-
- );
- })
- )}
-
-
-
- Ajouter un dossier
-
-
- );
-}
-
-function SyncView({
- folders,
- onBack,
- onUpdateSyncMode,
- onUpdateSyncCellular,
- globalSyncMode,
- onSetGlobalSyncMode,
- globalSyncCellular,
- onSetGlobalSyncCellular,
-}: {
- folders: StoredFolder[];
- onBack: () => void;
- onUpdateSyncMode: (id: string, mode: SyncMode) => void;
- onUpdateSyncCellular: (id: string, enabled: boolean) => void;
- globalSyncMode: SyncGlobalMode;
- onSetGlobalSyncMode: (mode: SyncGlobalMode) => void;
- globalSyncCellular: boolean;
- onSetGlobalSyncCellular: (enabled: boolean) => void;
-}) {
- return (
-
-
-
-
-
- Synchronisation
-
-
-
- Choisissez comment vos fichiers sont envoyés au serveur.
-
-
-
- Mode de synchronisation
- {GLOBAL_MODES.map((mode) => (
- onSetGlobalSyncMode(mode.value)}
- >
-
-
-
- {mode.label}
-
- {mode.description}
-
-
-
- ))}
-
-
- {globalSyncMode === 'auto' && (
-
-
-
- Autoriser le réseau cellulaire
- onSetGlobalSyncCellular(!globalSyncCellular)}
- >
-
-
-
-
- {globalSyncCellular
- ? 'Upload via WiFi et données mobiles'
- : 'Upload uniquement en WiFi'}
-
-
- )}
-
- {globalSyncMode === 'manual' && (
-
- Configuration par dossier
- {folders.length === 0 ? (
- Aucun dossier configuré
- ) : (
- folders.map((folder) => (
-
-
-
-
- {folder.name}
-
-
-
-
- {SYNC_MODES.map((mode) => (
- onUpdateSyncMode(folder.id, mode.value)}
- >
-
-
- {mode.label}
-
-
- ))}
-
-
- {folder.syncMode !== 'none' && (
-
-
- Réseau cellulaire
- onUpdateSyncCellular(folder.id, !folder.syncCellular)}
- >
-
-
-
- )}
-
- ))
- )}
-
- )}
-
- );
-}
-
-const styles = StyleSheet.create({
- overlay: {
- flex: 1,
- backgroundColor: 'rgba(0,0,0,0.4)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- container: {
- backgroundColor: '#fff',
- borderRadius: 16,
- paddingHorizontal: 20,
- paddingTop: 12,
- paddingBottom: 20,
- width: '85%',
- maxHeight: '80%',
- },
- handle: {
- width: 36,
- height: 4,
- borderRadius: 2,
- backgroundColor: '#ddd',
- alignSelf: 'center',
- marginBottom: 16,
- },
- title: {
- fontSize: 18,
- fontWeight: '700',
- color: '#333',
- marginBottom: 16,
- },
- menuIcon: {
- width: 40,
- height: 40,
- borderRadius: 10,
- justifyContent: 'center',
- alignItems: 'center',
- },
- menuItem: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 14,
- gap: 12,
- borderBottomWidth: 1,
- borderBottomColor: '#f0f0f0',
- },
- menuTextContainer: {
- flex: 1,
- },
- menuLabel: {
- fontSize: 16,
- fontWeight: '600',
- color: '#333',
- },
- menuDescription: {
- fontSize: 13,
- color: '#999',
- marginTop: 2,
- },
- closeBtn: {
- marginTop: 16,
- alignItems: 'center',
- paddingVertical: 10,
- },
- closeBtnText: {
- fontSize: 15,
- color: '#666',
- },
- viewContainer: {
- maxHeight: 500,
- },
- viewHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 10,
- marginBottom: 8,
- },
- backBtn: {
- padding: 4,
- },
- emptyText: {
- fontSize: 14,
- color: '#999',
- textAlign: 'center',
- marginVertical: 20,
- },
- folderRow: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 10,
- gap: 10,
- borderBottomWidth: 1,
- borderBottomColor: '#f0f0f0',
- },
- folderName: {
- flex: 1,
- fontSize: 15,
- color: '#333',
- },
- actionBtn: {
- padding: 6,
- },
- addBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: '#F57C00',
- borderRadius: 10,
- paddingVertical: 12,
- marginTop: 16,
- gap: 8,
- },
- addBtnText: {
- fontSize: 15,
- color: '#fff',
- fontWeight: '600',
- },
- syncInfo: {
- fontSize: 13,
- color: '#666',
- marginBottom: 16,
- lineHeight: 18,
- },
- globalModeCard: {
- backgroundColor: '#fafafa',
- borderRadius: 10,
- padding: 12,
- marginBottom: 12,
- },
- sectionLabel: {
- fontSize: 13,
- fontWeight: '600',
- color: '#666',
- marginBottom: 10,
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- },
- globalModeRow: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 10,
- paddingHorizontal: 8,
- gap: 10,
- borderRadius: 8,
- marginBottom: 4,
- },
- globalModeRowActive: {
- backgroundColor: '#fff',
- borderWidth: 1,
- borderColor: '#e0e0e0',
- },
- globalModeTextContainer: {
- flex: 1,
- },
- globalModeLabel: {
- fontSize: 15,
- fontWeight: '600',
- color: '#333',
- },
- globalModeDescription: {
- fontSize: 12,
- color: '#999',
- marginTop: 2,
- },
- cellularCard: {
- backgroundColor: '#fafafa',
- borderRadius: 10,
- padding: 12,
- marginBottom: 12,
- },
- cellularRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
- cellularLabel: {
- flex: 1,
- fontSize: 14,
- color: '#333',
- },
- cellularHint: {
- fontSize: 12,
- color: '#999',
- marginTop: 6,
- marginLeft: 28,
- },
- perFolderSection: {
- marginTop: 4,
- },
- syncFolderCard: {
- backgroundColor: '#fafafa',
- borderRadius: 10,
- padding: 12,
- marginBottom: 10,
- },
- syncFolderHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- marginBottom: 10,
- },
- syncFolderName: {
- fontSize: 15,
- fontWeight: '600',
- color: '#333',
- flex: 1,
- },
- radioGroup: {
- flexDirection: 'row',
- gap: 6,
- marginBottom: 10,
- },
- radioBtn: {
- flex: 1,
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- paddingVertical: 8,
- paddingHorizontal: 6,
- borderRadius: 8,
- backgroundColor: '#fff',
- borderWidth: 1,
- borderColor: '#e0e0e0',
- gap: 4,
- },
- radioBtnActive: {
- borderWidth: 1.5,
- },
- radioLabel: {
- fontSize: 12,
- fontWeight: '600',
- color: '#999',
- },
- toggleBtn: {
- width: 44,
- height: 24,
- borderRadius: 12,
- backgroundColor: '#ddd',
- justifyContent: 'center',
- paddingHorizontal: 2,
- },
- toggleBtnActive: {
- backgroundColor: '#1976D2',
- },
- toggleDot: {
- width: 20,
- height: 20,
- borderRadius: 10,
- backgroundColor: '#fff',
- },
- toggleDotActive: {
- alignSelf: 'flex-end',
- },
-});
diff --git a/mobile/components/SortChips.tsx b/mobile/components/SortChips.tsx
deleted file mode 100644
index 297bf89..0000000
--- a/mobile/components/SortChips.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import React from 'react';
-import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
-import { MaterialIcons } from '@expo/vector-icons';
-import type { ComponentProps } from 'react';
-import type { SortState } from './SearchBar';
-
-type IconName = ComponentProps['name'];
-
-interface SortChipsProps {
- sort: SortState;
- onSortChange: (s: SortState) => void;
-}
-
-const OPTIONS: { key: SortState['key']; label: string; icon: IconName }[] = [
- { key: 'name', label: 'A-Z', icon: 'sort-by-alpha' },
- { key: 'date', label: 'Date', icon: 'schedule' },
-];
-
-export function SortChips({ sort, onSortChange }: SortChipsProps) {
- const select = (key: SortState['key']) => {
- if (sort.key === key) {
- onSortChange({ key, direction: sort.direction === 'asc' ? 'desc' : 'asc' });
- } else {
- onSortChange({ key, direction: key === 'name' ? 'asc' : 'desc' });
- }
- };
-
- return (
-
- {OPTIONS.map((opt) => {
- const active = sort.key === opt.key;
- return (
- select(opt.key)}
- >
-
- {opt.label}
-
- );
- })}
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- paddingHorizontal: 15,
- paddingVertical: 8,
- backgroundColor: '#f5f5f5',
- },
- chip: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 4,
- paddingHorizontal: 12,
- paddingVertical: 6,
- borderRadius: 16,
- borderWidth: 1,
- borderColor: '#1976D2',
- backgroundColor: '#fff',
- },
- chipActive: {
- backgroundColor: '#1976D2',
- },
- chipText: {
- fontSize: 13,
- fontWeight: '600',
- color: '#1976D2',
- },
- chipTextActive: {
- color: '#fff',
- },
-});
diff --git a/mobile/components/SyncStatusBadge.tsx b/mobile/components/SyncStatusBadge.tsx
deleted file mode 100644
index 74e2797..0000000
--- a/mobile/components/SyncStatusBadge.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import React from 'react';
-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 = {
- 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, showLabel }: SyncStatusBadgeProps) {
- const config = STATUS_CONFIG[status];
- const iconSize = Math.round(size * 0.7);
-
- if (showLabel) {
- return (
-
-
- {config.label}
-
- );
- }
-
- return (
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- badge: {
- position: 'absolute',
- top: 2,
- right: 2,
- justifyContent: 'center',
- alignItems: 'center',
- 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',
- },
-});
diff --git a/mobile/components/SyncStatusIcon.tsx b/mobile/components/SyncStatusIcon.tsx
deleted file mode 100644
index 7e62f75..0000000
--- a/mobile/components/SyncStatusIcon.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import React, { useEffect } from 'react';
-import { TouchableOpacity, Text, StyleSheet, View } from 'react-native';
-import { MaterialIcons } from '@expo/vector-icons';
-import Animated, {
- useSharedValue,
- useAnimatedStyle,
- withTiming,
- withRepeat,
- withSequence,
- Easing,
- cancelAnimation,
-} from 'react-native-reanimated';
-
-interface SyncStatusIconProps {
- isSyncing: boolean;
- pendingCount: number;
- isUploading: boolean;
- uploadPendingCount: number;
- onPress: () => void;
-}
-
-export function SyncStatusIcon({ isSyncing, pendingCount, isUploading, uploadPendingCount, onPress }: SyncStatusIconProps) {
- const rotation = useSharedValue(0);
- const isActive = isSyncing || isUploading;
- const totalPending = pendingCount + uploadPendingCount;
-
- useEffect(() => {
- if (isActive) {
- rotation.value = withRepeat(
- withSequence(
- withTiming(360, { duration: 1000, easing: Easing.linear }),
- withTiming(0, { duration: 0 })
- ),
- -1
- );
- } else {
- cancelAnimation(rotation);
- rotation.value = withTiming(0, { duration: 200 });
- }
- }, [isActive, rotation]);
-
- const animatedStyle = useAnimatedStyle(() => ({
- transform: [{ rotate: `${rotation.value}deg` }],
- }));
-
- return (
-
-
- 0 ? '#F57C00' : '#666'}
- />
-
- {totalPending > 0 && !isActive && (
-
-
- {totalPending > 99 ? '99+' : totalPending}
-
-
- )}
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- position: 'relative',
- marginRight: 4,
- padding: 8,
- },
- badge: {
- position: 'absolute',
- top: 2,
- right: 0,
- backgroundColor: '#E53935',
- borderRadius: 8,
- minWidth: 16,
- height: 16,
- justifyContent: 'center',
- alignItems: 'center',
- paddingHorizontal: 4,
- },
- badgeText: {
- fontSize: 9,
- fontWeight: '700',
- color: '#fff',
- },
-});
diff --git a/mobile/components/TagChip.tsx b/mobile/components/TagChip.tsx
deleted file mode 100644
index 44920b8..0000000
--- a/mobile/components/TagChip.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React from 'react';
-import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
-
-interface TagChipProps {
- name: string;
- onRemove?: () => void;
-}
-
-export function TagChip({ name, onRemove }: TagChipProps) {
- return (
-
- {name}
- {onRemove && (
-
- ×
-
- )}
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: '#E3F2FD',
- borderRadius: 12,
- paddingHorizontal: 10,
- paddingVertical: 4,
- gap: 4,
- },
- text: {
- fontSize: 12,
- color: '#1976D2',
- fontWeight: '500',
- },
- removeButton: {
- width: 16,
- height: 16,
- borderRadius: 8,
- backgroundColor: '#BBDEFB',
- justifyContent: 'center',
- alignItems: 'center',
- },
- removeText: {
- fontSize: 12,
- color: '#1976D2',
- fontWeight: '700',
- },
-});
diff --git a/mobile/components/UploadModal.tsx b/mobile/components/UploadModal.tsx
deleted file mode 100644
index ffbfbe7..0000000
--- a/mobile/components/UploadModal.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import React from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, Modal, Alert } from 'react-native';
-import { MaterialIcons } from '@expo/vector-icons';
-import * as DocumentPicker from 'expo-document-picker';
-import { UploadFile } from '../services/uploadQueue';
-import { useUploadQueue } from '../hooks/useUploadQueue';
-
-interface UploadModalProps {
- visible: boolean;
- onClose: () => void;
-}
-
-export function UploadModal({ visible, onClose }: UploadModalProps) {
- const { enqueue } = useUploadQueue();
-
- const pickDocuments = async () => {
- try {
- const result = await DocumentPicker.getDocumentAsync({
- multiple: true,
- copyToCacheDirectory: true,
- });
-
- if (result.canceled || result.assets.length === 0) return;
-
- const files = result.assets.map((asset) => ({
- uri: asset.uri,
- type: asset.mimeType || 'application/octet-stream',
- name: asset.name,
- }));
-
- enqueue(files);
- onClose();
- } catch {
- Alert.alert('Erreur', "Impossible de sélectionner des documents");
- }
- };
-
- return (
-
-
- {}}>
-
-
-
- Ajouter des fichiers
-
-
-
-
-
-
-
- Sélectionner des documents
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- overlay: {
- flex: 1,
- backgroundColor: 'rgba(0,0,0,0.4)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- container: {
- backgroundColor: '#fff',
- borderRadius: 16,
- paddingHorizontal: 20,
- paddingTop: 12,
- paddingBottom: 20,
- width: '85%',
- },
- handle: {
- width: 36,
- height: 4,
- borderRadius: 2,
- backgroundColor: '#ddd',
- alignSelf: 'center',
- marginBottom: 16,
- },
- header: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 16,
- },
- title: {
- fontSize: 18,
- fontWeight: '700',
- color: '#333',
- },
- closeBtn: {
- padding: 4,
- },
- docButton: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: '#4CAF50',
- padding: 16,
- borderRadius: 8,
- gap: 8,
- },
- buttonText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
-});
diff --git a/mobile/components/UploadProgress.tsx b/mobile/components/UploadProgress.tsx
deleted file mode 100644
index ac0fc10..0000000
--- a/mobile/components/UploadProgress.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from 'react';
-import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
-
-interface UploadProgressProps {
- progress?: number;
- status: 'idle' | 'uploading' | 'processing' | 'success' | 'error';
- error?: string;
- uploadedCount?: number;
- totalCount?: number;
-}
-
-export function UploadProgress({ progress, status, error, uploadedCount, totalCount }: UploadProgressProps) {
- if (status === 'idle') return null;
-
- const hasMulti = totalCount !== undefined && totalCount > 1;
-
- return (
-
- {status === 'uploading' && (
- <>
-
-
- {hasMulti
- ? `Upload ${uploadedCount || 0}/${totalCount}...`
- : `Upload en cours...${progress !== undefined ? ` ${progress}%` : ''}`}
-
- >
- )}
-
- {status === 'processing' && (
- <>
-
- Traitement OCR en cours...
- >
- )}
-
- {status === 'success' && (
-
- {hasMulti ? `${uploadedCount} fichiers uploadés !` : 'Upload terminé !'}
-
- )}
-
- {status === 'error' && (
-
- {error || "Erreur lors de l'upload"}
-
- )}
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- alignItems: 'center',
- padding: 12,
- backgroundColor: '#F5F5F5',
- borderRadius: 8,
- marginBottom: 16,
- },
- text: {
- marginLeft: 8,
- fontSize: 14,
- color: '#333',
- },
- success: {
- color: '#4CAF50',
- },
- error: {
- color: '#F44336',
- },
-});
diff --git a/mobile/components/ZoomableImage.tsx b/mobile/components/ZoomableImage.tsx
deleted file mode 100644
index ccd8fcc..0000000
--- a/mobile/components/ZoomableImage.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-import React, { useCallback } from 'react';
-import { StyleSheet, Pressable, View } from 'react-native';
-import Animated, {
- useSharedValue,
- useAnimatedStyle,
- withTiming,
- withSpring,
- runOnJS,
-} from 'react-native-reanimated';
-import {
- Gesture,
- GestureDetector,
-} from 'react-native-gesture-handler';
-
-interface ZoomableImageProps {
- uri: string;
- width: number;
- height: number;
- onClose?: () => void;
- onSwipeVertical?: (direction: 'up' | 'down') => void;
-}
-
-const MAX_SCALE = 4;
-const DOUBLE_TAP_SCALE = 2.5;
-const SPRING_CONFIG = { damping: 20, stiffness: 200, mass: 0.5 };
-
-export function ZoomableImage({ uri, width, height, onClose, onSwipeVertical }: ZoomableImageProps) {
- const scale = useSharedValue(1);
- const savedScale = useSharedValue(1);
- const translateX = useSharedValue(0);
- const translateY = useSharedValue(0);
- const savedTranslateX = useSharedValue(0);
- const savedTranslateY = useSharedValue(0);
-
- const handleClose = useCallback(() => {
- onClose?.();
- }, [onClose]);
-
- const handleSwipeVertical = useCallback((direction: 'up' | 'down') => {
- onSwipeVertical?.(direction);
- }, [onSwipeVertical]);
-
- const pinch = Gesture.Pinch()
- .onUpdate((e) => {
- scale.value = Math.min(Math.max(savedScale.value * e.scale, 1), MAX_SCALE);
- })
- .onEnd(() => {
- if (scale.value < 1) {
- scale.value = withSpring(1, SPRING_CONFIG);
- translateX.value = withSpring(0, SPRING_CONFIG);
- translateY.value = withSpring(0, SPRING_CONFIG);
- savedScale.value = 1;
- savedTranslateX.value = 0;
- savedTranslateY.value = 0;
- } else {
- savedScale.value = scale.value;
- }
- });
-
- const pan = Gesture.Pan()
- .minDistance(5)
- .onUpdate((e) => {
- if (savedScale.value > 1) {
- translateX.value = savedTranslateX.value + e.translationX;
- translateY.value = savedTranslateY.value + e.translationY;
- }
- })
- .onEnd((e) => {
- if (savedScale.value <= 1) {
- const absY = Math.abs(e.translationY);
- const absX = Math.abs(e.translationX);
- if (absY > 30 && absY > absX) {
- runOnJS(handleSwipeVertical)(e.translationY > 0 ? 'down' : 'up');
- }
- translateX.value = withSpring(0, SPRING_CONFIG);
- translateY.value = withSpring(0, SPRING_CONFIG);
- }
- savedTranslateX.value = translateX.value;
- savedTranslateY.value = translateY.value;
- });
-
- const zoomGestures = Gesture.Simultaneous(pinch, pan);
-
- const doubleTap = Gesture.Tap()
- .numberOfTaps(2)
- .maxDuration(250)
- .onEnd(() => {
- if (scale.value > 1) {
- scale.value = withTiming(1, { duration: 200 });
- translateX.value = withTiming(0, { duration: 200 });
- translateY.value = withTiming(0, { duration: 200 });
- savedScale.value = 1;
- savedTranslateX.value = 0;
- savedTranslateY.value = 0;
- } else {
- scale.value = withTiming(DOUBLE_TAP_SCALE, { duration: 200 });
- savedScale.value = DOUBLE_TAP_SCALE;
- }
- });
-
- const singleTap = Gesture.Tap()
- .maxDuration(250)
- .onEnd(() => {
- if (scale.value <= 1) {
- runOnJS(handleClose)();
- }
- });
-
- const taps = Gesture.Exclusive(doubleTap, singleTap);
-
- const composed = Gesture.Race(zoomGestures, taps);
-
- const animatedStyle = useAnimatedStyle(() => ({
- transform: [
- { scale: scale.value },
- { translateX: translateX.value },
- { translateY: translateY.value },
- ],
- }));
-
- return (
-
-
-
-
- {onClose && (
-
-
-
-
-
-
- )}
-
- );
-}
-
-const CLOSE_SIZE = 36;
-const CLOSE_LINE = 20;
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#000',
- justifyContent: 'center',
- alignItems: 'center',
- },
- image: {
- overflow: 'hidden',
- },
- closeButton: {
- position: 'absolute',
- top: 56,
- right: 16,
- width: CLOSE_SIZE,
- height: CLOSE_SIZE,
- borderRadius: CLOSE_SIZE / 2,
- backgroundColor: 'rgba(255,255,255,0.25)',
- justifyContent: 'center',
- alignItems: 'center',
- },
- closeIcon: {
- width: CLOSE_LINE,
- height: CLOSE_LINE,
- justifyContent: 'center',
- alignItems: 'center',
- },
- closeLine: {
- position: 'absolute',
- width: CLOSE_LINE,
- height: 2,
- backgroundColor: '#fff',
- borderRadius: 1,
- },
- closeLine1: {
- transform: [{ rotate: '45deg' }],
- },
- closeLine2: {
- transform: [{ rotate: '-45deg' }],
- },
-});
diff --git a/mobile/config/onboarding.ts b/mobile/config/onboarding.ts
deleted file mode 100644
index b1de0cf..0000000
--- a/mobile/config/onboarding.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-export const CURRENT_ONBOARDING_VERSION = 3;
-
-export type OnboardingAction = {
- type: 'pick_directory';
- label: string;
-};
-
-export type OnboardingStep = {
- id: string;
- version: number;
- title: string;
- description: string;
- icon: string;
- action?: OnboardingAction;
- condition?: 'has_no_folders';
-};
-
-export const ONBOARDING_STEPS: OnboardingStep[] = [
- {
- id: 'welcome',
- version: 3,
- title: 'Bienvenue sur Dot.',
- description: 'Votre espace document personnel, toujours accessible.',
- icon: 'waving-hand',
- },
- {
- id: 'select_folders',
- version: 3,
- title: 'Ajoutez vos dossiers',
- description: 'Sélectionnez les dossiers que vous souhaitez synchroniser avec Dot.\n\nAjoutez-en autant que vous voulez, vous pourrez les gérer plus tard.',
- icon: 'create-new-folder',
- action: { type: 'pick_directory', label: 'Ajouter un dossier' },
- },
-];
diff --git a/mobile/constants/api.ts b/mobile/constants/api.ts
deleted file mode 100644
index b4ecabf..0000000
--- a/mobile/constants/api.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-export const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://192.168.1.17:8080/api/v1';
-
-export const ENDPOINTS = {
- RESOURCES: '/resources',
- RESOURCE: '/resources',
- UPLOAD: '/resources/upload',
- MOVE: '/resources/move',
- FOLDERS: '/resources/folders',
- VARIANT: '/variants',
- DEDUP_CHECK: '/resources/dedup-check',
- OCR_JOBS: '/ocr/jobs',
- EVENTS: '/events',
- HEALTH: '/health',
- AUTH_LOGIN: '/auth/login',
- AUTH_REGISTER: '/auth/register',
- AUTH_REFRESH: '/auth/refresh',
- AUTH_LOGOUT: '/auth/logout',
- DEVICES: '/devices',
- SYNC_PULL: '/sync/pull',
- SYNC_PUSH: '/sync/push',
- SHARE: '/resources/:id/share',
- ACCESS: '/resources/:id/access',
-} as const;
diff --git a/mobile/contexts/AuthContext.tsx b/mobile/contexts/AuthContext.tsx
deleted file mode 100644
index a7ed630..0000000
--- a/mobile/contexts/AuthContext.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import React, { createContext, useContext, useEffect, useState } from 'react';
-import { apiClient } from '../api/client';
-import { tokenStorage } from '../api/secureStorage';
-import { ENDPOINTS } from '../constants/api';
-import { AuthResponse, User } from '../types';
-
-interface AuthContextType {
- user: User | null;
- isLoading: boolean;
- login: (username: string, password: string) => Promise;
- register: (username: string, password: string) => Promise;
- logout: () => Promise;
-}
-
-const AuthContext = createContext(undefined);
-
-export function AuthProvider({ children }: { children: React.ReactNode }) {
- const [user, setUser] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
-
- useEffect(() => {
- loadUser();
- }, []);
-
- async function loadUser() {
- try {
- const stored = await tokenStorage.getUser();
- if (stored) {
- const userData = JSON.parse(stored) as User;
- setUser(userData);
-
- const accessToken = await tokenStorage.getAccessToken();
- if (accessToken) {
- apiClient.setAccessToken(accessToken);
- }
- }
- } catch {
- await tokenStorage.deleteUser();
- } finally {
- setIsLoading(false);
- }
- }
-
- async function login(username: string, password: string) {
- const response = await apiClient.post(ENDPOINTS.AUTH_LOGIN, {
- username,
- password,
- });
-
- apiClient.setAccessToken(response.access_token);
- await tokenStorage.setAccessToken(response.access_token);
- await tokenStorage.setRefreshToken(response.refresh_token);
- await tokenStorage.setUser(JSON.stringify(response.user));
- setUser(response.user);
- }
-
- async function register(username: string, password: string) {
- const response = await apiClient.post(ENDPOINTS.AUTH_REGISTER, {
- username,
- password,
- });
-
- apiClient.setAccessToken(response.access_token);
- await tokenStorage.setAccessToken(response.access_token);
- await tokenStorage.setRefreshToken(response.refresh_token);
- await tokenStorage.setUser(JSON.stringify(response.user));
- setUser(response.user);
- }
-
- async function logout() {
- try {
- const refreshToken = await tokenStorage.getRefreshToken();
- if (refreshToken) {
- await apiClient.post(ENDPOINTS.AUTH_LOGOUT, { refresh_token: refreshToken });
- }
- } catch {}
-
- apiClient.setAccessToken(null);
- await tokenStorage.deleteAccessToken();
- await tokenStorage.deleteRefreshToken();
- await tokenStorage.deleteUser();
- setUser(null);
- }
-
- return (
-
- {children}
-
- );
-}
-
-export function useAuth() {
- const context = useContext(AuthContext);
- if (!context) {
- throw new Error('useAuth must be used within an AuthProvider');
- }
- return context;
-}
diff --git a/mobile/contexts/DeviceContext.tsx b/mobile/contexts/DeviceContext.tsx
deleted file mode 100644
index 037d234..0000000
--- a/mobile/contexts/DeviceContext.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React, { createContext, useContext, useEffect, useState } from 'react';
-import { useDeviceRegistration } from '../hooks/useDeviceRegistration';
-import { useAuth } from './AuthContext';
-
-interface DeviceContextType {
- isRegistered: boolean | null;
- isLoading: boolean;
- deviceName: string;
- register: (name: string) => Promise<{ id: string; device_name: string; role: string }>;
-}
-
-const DeviceContext = createContext(undefined);
-
-export function DeviceProvider({ children }: { children: React.ReactNode }) {
- const { user } = useAuth();
- const registration = useDeviceRegistration();
- const { isRegistered, isLoading, device, register } = registration;
-
- const value: DeviceContextType = user
- ? {
- isRegistered,
- isLoading,
- deviceName: device?.name ?? '',
- register,
- }
- : {
- isRegistered: null,
- isLoading: false,
- deviceName: '',
- register: async () => { throw new Error('Not authenticated'); },
- };
-
- return (
-
- {children}
-
- );
-}
-
-export function useDevice() {
- const context = useContext(DeviceContext);
- if (!context) {
- throw new Error('useDevice must be used within a DeviceProvider');
- }
- return context;
-}
diff --git a/mobile/contexts/SseContext.tsx b/mobile/contexts/SseContext.tsx
deleted file mode 100644
index bd02bb9..0000000
--- a/mobile/contexts/SseContext.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React, { createContext, useContext, useEffect, useRef, useCallback } from 'react';
-import { apiClient } from '../api/client';
-import { useAuth } from './AuthContext';
-
-type OcrDoneCallback = (resourceId: string) => void;
-type ResourceCreatedCallback = (resourceId: string) => void;
-
-interface SseContextValue {
- onOcrDone: (cb: OcrDoneCallback) => () => void;
- onResourceCreated: (cb: ResourceCreatedCallback) => () => void;
-}
-
-const SseContext = createContext({
- onOcrDone: () => () => {},
- onResourceCreated: () => () => {},
-});
-
-export function SseProvider({ children }: { children: React.ReactNode }) {
- const { user } = useAuth();
- const ocrListenersRef = useRef>(new Set());
- const resourceListenersRef = useRef>(new Set());
- const cancelRef = useRef<() => void>(() => {});
- const reconnectTimeoutRef = useRef | undefined>(undefined);
-
- const connect = useCallback(() => {
- if (!user) return;
-
- let cancelled = false;
-
- const start = async () => {
- try {
- const cancel = await apiClient.subscribeEvents(
- (event, data) => {
- if (event === 'ocr_done') {
- ocrListenersRef.current.forEach((cb) => cb(data));
- } else if (event === 'resource.created') {
- resourceListenersRef.current.forEach((cb) => cb(data));
- }
- },
- () => {
- reconnectTimeoutRef.current = setTimeout(connect, 3000);
- },
- );
- if (cancelled) {
- cancel();
- } else {
- cancelRef.current = cancel;
- }
- } catch {
- reconnectTimeoutRef.current = setTimeout(connect, 3000);
- }
- };
-
- start();
-
- return () => {
- cancelled = true;
- };
- }, [user]);
-
- useEffect(() => {
- const cleanup = connect();
- return () => {
- cleanup?.();
- cancelRef.current();
- clearTimeout(reconnectTimeoutRef.current);
- };
- }, [connect]);
-
- const onOcrDone = useCallback((cb: OcrDoneCallback) => {
- ocrListenersRef.current.add(cb);
- return () => {
- ocrListenersRef.current.delete(cb);
- };
- }, []);
-
- const onResourceCreated = useCallback((cb: ResourceCreatedCallback) => {
- resourceListenersRef.current.add(cb);
- return () => {
- resourceListenersRef.current.delete(cb);
- };
- }, []);
-
- return (
-
- {children}
-
- );
-}
-
-export function useOcrDone(onDone?: OcrDoneCallback) {
- const ctx = useContext(SseContext);
- useEffect(() => {
- if (onDone) return ctx.onOcrDone(onDone);
- }, [onDone, ctx]);
- return { onOcrDone: ctx.onOcrDone };
-}
-
-export function useResourceCreated(onCreated?: ResourceCreatedCallback) {
- const ctx = useContext(SseContext);
- useEffect(() => {
- if (onCreated) return ctx.onResourceCreated(onCreated);
- }, [onCreated, ctx]);
- return { onResourceCreated: ctx.onResourceCreated };
-}
diff --git a/mobile/hooks/useAutoSync.ts b/mobile/hooks/useAutoSync.ts
deleted file mode 100644
index db05e19..0000000
--- a/mobile/hooks/useAutoSync.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import { useEffect, useCallback, useRef } from 'react';
-import { createMMKV } from 'react-native-mmkv';
-import { useQueryClient } from '@tanstack/react-query';
-import { File, UploadType } from 'expo-file-system';
-import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
-import { safDirectory } from '../services/safDirectory';
-import { fileStore } from '../services/fileStore';
-import { activeUploadUris } from '../services/uploadQueue';
-import { apiClient } from '../api/client';
-import { API_BASE_URL, ENDPOINTS } from '../constants/api';
-import { ApiError } from '../types';
-import {
- setIsSyncing,
- setSyncProgress,
- isSyncLoopRunning,
- setSyncLoopRunning,
- requestSyncCancel,
- isSyncCancelRequested,
- consumeSyncCancel,
-} from './useSyncQueue';
-
-const MAX_RETRIES = 5;
-const RETRY_MMKV_ID = 'vaultdrop-sync-retries';
-
-const retryStorage = createMMKV({ id: RETRY_MMKV_ID });
-
-function getRetryCount(fileId: string): number {
- return retryStorage.getNumber(`${fileId}_retries`) ?? 0;
-}
-
-function incrementRetry(fileId: string): number {
- const count = getRetryCount(fileId) + 1;
- retryStorage.set(`${fileId}_retries`, count);
- return count;
-}
-
-function resetRetry(fileId: string) {
- retryStorage.remove(`${fileId}_retries`);
-}
-
-interface UploadResult {
- name: string;
- id: string;
-}
-
-async function uploadFile(file: { uri: string; type: string; name: string }): Promise {
- const fsFile = new File(file.uri);
- const headers: Record = {};
- const token = apiClient.getAccessToken();
- if (token) {
- headers['Authorization'] = `Bearer ${token}`;
- }
- const result = await fsFile.upload(`${API_BASE_URL}${ENDPOINTS.UPLOAD}`, {
- httpMethod: 'POST',
- uploadType: UploadType.MULTIPART,
- fieldName: 'file',
- mimeType: file.type,
- headers,
- });
-
- if (result.status >= 400) {
- let message = 'Upload failed';
- try {
- const body: ApiError = JSON.parse(result.body);
- message = body.error?.message || message;
- } catch {
- message = result.body || message;
- }
- throw new Error(message);
- }
-
- const body = JSON.parse(result.body);
- const items = body.data ?? body;
- const item = Array.isArray(items) ? items[0] : items;
- return item as UploadResult;
-}
-
-function canSyncBasedOnNetwork(netInfo: NetInfoState, cellularAllowed: boolean): boolean {
- if (!netInfo.isConnected) return false;
- if (!cellularAllowed && netInfo.type !== 'wifi') return false;
- return true;
-}
-
-let autoSyncLoopStarted = false;
-
-export function useAutoSync() {
- const queryClient = useQueryClient();
- const isRunning = useRef(false);
-
- const getPendingFiles = useCallback((): Array[number]> => {
- return fileStore.getAllLocal().filter(
- (entry) => !entry.backendId && entry.syncStatus === 'local' && entry.localUri
- );
- }, []);
-
- const cancelSync = useCallback(() => {
- requestSyncCancel();
- }, []);
-
- const runPendingSync = useCallback(async (pendingFiles: Array[number]>) => {
- if (isRunning.current) return;
- if (isSyncLoopRunning()) return;
-
- const globalCellular = safDirectory.getGlobalSyncCellular();
- const netInfo = await NetInfo.fetch();
-
- if (!canSyncBasedOnNetwork(netInfo, globalCellular)) return;
-
- isRunning.current = true;
- setSyncLoopRunning(true);
-
- try {
- setIsSyncing(true);
- consumeSyncCancel();
-
- const progressFiles = pendingFiles.map((f) => ({ id: f.id, name: f.name }));
-
- for (let index = 0; index < pendingFiles.length; index++) {
- if (isSyncCancelRequested()) break;
- const entry = pendingFiles[index];
- setSyncProgress({ files: progressFiles, currentIndex: index });
- const uri = entry.localUri;
- if (!uri || activeUploadUris.has(uri)) continue;
- activeUploadUris.add(uri);
- try {
- const uploaded = await uploadFile({
- uri,
- type: entry.mimeType,
- name: entry.name,
- });
-
- resetRetry(entry.id);
- fileStore.updatePartial(entry.id, {
- backendId: uploaded.id,
- syncStatus: 'synced',
- source: 'synced',
- });
- } catch (err) {
- const retries = incrementRetry(entry.id);
- if (retries >= MAX_RETRIES) {
- fileStore.updatePartial(entry.id, {
- syncStatus: 'error',
- });
- resetRetry(entry.id);
- }
- } finally {
- activeUploadUris.delete(uri);
- }
- }
-
- if (consumeSyncCancel()) {
- return;
- }
-
- queryClient.invalidateQueries({ queryKey: ['resources'] });
- } finally {
- setSyncProgress(null);
- setIsSyncing(false);
- isRunning.current = false;
- setSyncLoopRunning(false);
- }
- }, [queryClient]);
-
- const checkAndSync = useCallback(async () => {
- const globalMode = safDirectory.getGlobalSyncMode();
- if (globalMode === 'off') return;
-
- let pendingFiles = getPendingFiles();
-
- if (globalMode === 'manual') {
- const allFolders = safDirectory.getAll();
- const autoFolderIds = new Set(
- allFolders.filter((f) => f.syncMode === 'auto').map((f) => f.id)
- );
- pendingFiles = pendingFiles.filter(
- (entry) => entry.parentResourceId && autoFolderIds.has(entry.parentResourceId)
- );
- }
-
- if (pendingFiles.length === 0) return;
-
- await runPendingSync(pendingFiles);
- }, [getPendingFiles, runPendingSync]);
-
- const syncManually = useCallback(async () => {
- const pendingFiles = getPendingFiles();
- if (pendingFiles.length === 0) return;
- await runPendingSync(pendingFiles);
- }, [getPendingFiles, runPendingSync]);
-
- useEffect(() => {
- if (autoSyncLoopStarted) return;
- autoSyncLoopStarted = true;
-
- const timeout = setTimeout(() => {
- checkAndSync();
- }, 5_000);
- const interval = setInterval(checkAndSync, 30_000);
- return () => { clearTimeout(timeout); clearInterval(interval); autoSyncLoopStarted = false; };
- }, [checkAndSync]);
-
- return { triggerSync: checkAndSync, syncManually, cancelSync };
-}
diff --git a/mobile/hooks/useBatchStore.ts b/mobile/hooks/useBatchStore.ts
deleted file mode 100644
index 878df8a..0000000
--- a/mobile/hooks/useBatchStore.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { useCallback } from 'react';
-import { createMMKV, useMMKVObject } from 'react-native-mmkv';
-import { Batch } from '../types';
-
-const storage = createMMKV({ id: 'vaultdrop-batches' });
-
-export function useBatchStore() {
- const [batchIds, setBatchIds] = useMMKVObject('batch-ids', storage);
-
- const saveBatch = useCallback((batch: Batch) => {
- storage.set(`batch_${batch.id}`, JSON.stringify(batch));
- const current = batchIds ?? [];
- if (!current.includes(batch.id)) {
- setBatchIds([batch.id, ...current]);
- }
- }, [batchIds, setBatchIds]);
-
- const getBatch = useCallback((id: string): Batch | undefined => {
- const raw = storage.getString(`batch_${id}`);
- if (!raw) return undefined;
- return JSON.parse(raw) as Batch;
- }, []);
-
- const getAllBatches = useCallback((): Batch[] => {
- return (batchIds ?? [])
- .map((id) => getBatch(id))
- .filter((b): b is Batch => b !== undefined);
- }, [batchIds, getBatch]);
-
- const addTagToBatch = useCallback((batchId: string, tag: string) => {
- const batch = getBatch(batchId);
- if (!batch) return;
- if (batch.tags.includes(tag)) return;
- batch.tags = [...batch.tags, tag];
- storage.set(`batch_${batchId}`, JSON.stringify(batch));
- }, [getBatch]);
-
- const removeTagFromBatch = useCallback((batchId: string, tag: string) => {
- const batch = getBatch(batchId);
- if (!batch) return;
- batch.tags = batch.tags.filter((t) => t !== tag);
- storage.set(`batch_${batchId}`, JSON.stringify(batch));
- }, [getBatch]);
-
- const deleteBatch = useCallback((batchId: string) => {
- storage.set(`batch_${batchId}`, undefined as any);
- storage.remove(`batch_${batchId}` as any);
- setBatchIds((batchIds ?? []).filter((id) => id !== batchId));
- }, [batchIds, setBatchIds]);
-
- const removePhotoFromBatch = useCallback((batchId: string, photoId: string) => {
- const batch = getBatch(batchId);
- if (!batch) return;
- batch.photos = batch.photos.filter((p) => p.id !== photoId);
- if (batch.photos.length === 0) {
- deleteBatch(batchId);
- } else {
- storage.set(`batch_${batchId}`, JSON.stringify(batch));
- }
- }, [getBatch, deleteBatch]);
-
- return {
- saveBatch,
- getBatch,
- getAllBatches,
- addTagToBatch,
- removeTagFromBatch,
- deleteBatch,
- removePhotoFromBatch,
- };
-}
diff --git a/mobile/hooks/useCameraCapture.ts b/mobile/hooks/useCameraCapture.ts
deleted file mode 100644
index 808b482..0000000
--- a/mobile/hooks/useCameraCapture.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import { useEffect, useState, useCallback, useRef } from 'react';
-import { useCameraDevice, useCameraPermission, usePhotoOutput, type TorchMode, type CameraRef } from 'react-native-vision-camera';
-import { useUpload } from './useUpload';
-import { CapturedPhoto } from '../types';
-
-type CaptureStatus = 'idle' | 'capturing' | 'uploading' | 'success' | 'error';
-
-export function useCameraCapture() {
- const { hasPermission, requestPermission } = useCameraPermission();
- const device = useCameraDevice('back');
- const cameraRef = useRef(null);
-
- const [captureStatus, setCaptureStatus] = useState('idle');
- const [captureError, setCaptureError] = useState();
- const [torchMode, setTorchMode] = useState('off');
- const [cameraReady, setCameraReady] = useState(false);
- const [capturedPhotos, setCapturedPhotos] = useState([]);
-
- const upload = useUpload();
-
- const photoOutput = usePhotoOutput();
-
- useEffect(() => {
- if (!hasPermission) {
- requestPermission();
- }
- }, [hasPermission, requestPermission]);
-
- const isActive = hasPermission && !!device && captureStatus !== 'uploading';
-
- const toggleTorch = useCallback(async () => {
- const next = torchMode === 'off' ? 'on' : 'off';
- setTorchMode(next);
- if (cameraReady && cameraRef.current) {
- try {
- const controller = cameraRef.current.controller;
- if (controller?.device.hasTorch) {
- await controller.setTorchMode(next);
- }
- } catch {
- setTorchMode(torchMode);
- }
- }
- }, [torchMode, cameraReady]);
-
- const onStarted = useCallback(() => {
- setCameraReady(true);
- if (torchMode === 'on' && cameraRef.current) {
- const controller = cameraRef.current.controller;
- controller?.setTorchMode('on').catch(() => {});
- }
- }, [torchMode]);
-
- const onStopped = useCallback(() => {
- setCameraReady(false);
- }, []);
-
- const addCapturedPhoto = useCallback((photo: CapturedPhoto) => {
- setCapturedPhotos((prev) => [...prev, photo]);
- }, []);
-
- const removeCapturedPhoto = useCallback((id: string) => {
- setCapturedPhotos((prev) => prev.filter((p) => p.id !== id));
- }, []);
-
- const clearCapturedPhotos = useCallback(() => {
- setCapturedPhotos([]);
- }, []);
-
- const capturePhoto = useCallback(async () => {
- if (!photoOutput) return;
-
- const photoId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
-
- try {
- setCaptureStatus('capturing');
- setCaptureError(undefined);
-
- const { filePath } = await photoOutput.capturePhotoToFile({}, {});
-
- setCaptureStatus('uploading');
-
- const capturedPhoto: CapturedPhoto = {
- id: photoId,
- filePath,
- uri: 'file://' + filePath,
- };
-
- addCapturedPhoto(capturedPhoto);
-
- const result = await upload.mutateAsync([
- { uri: capturedPhoto.uri, type: 'image/jpeg', name: `scan_${photoId}.jpg` },
- ]);
-
- if (result.errors.length > 0) {
- setCaptureStatus('error');
- setCaptureError(result.errors[0].message);
- } else {
- capturedPhoto.uploadedId = result.uploaded[0]?.id;
- capturedPhoto.uploadedAt = new Date().toISOString();
- setCaptureStatus('success');
- }
- } catch (err) {
- setCaptureStatus('error');
- setCaptureError(err instanceof Error ? err.message : 'Erreur lors de la capture');
- }
-
- setTimeout(() => {
- setCaptureStatus('idle');
- setCaptureError(undefined);
- }, 1500);
- }, [photoOutput, upload, addCapturedPhoto]);
-
- return {
- cameraRef,
- hasPermission,
- requestPermission,
- device,
- photoOutput,
- capturePhoto,
- captureStatus,
- captureError,
- isActive,
- torchMode,
- toggleTorch,
- onStarted,
- onStopped,
- capturedPhotos,
- removeCapturedPhoto,
- clearCapturedPhotos,
- capturedCount: capturedPhotos.length,
- };
-}
diff --git a/mobile/hooks/useDebounce.ts b/mobile/hooks/useDebounce.ts
deleted file mode 100644
index 0ca7902..0000000
--- a/mobile/hooks/useDebounce.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { useState, useEffect } from 'react';
-
-export function useDebounce(value: T, delay: number): T {
- const [debounced, setDebounced] = useState(value);
-
- useEffect(() => {
- const id = setTimeout(() => setDebounced(value), delay);
- return () => clearTimeout(id);
- }, [value, delay]);
-
- return debounced;
-}
diff --git a/mobile/hooks/useDeviceFiles.ts b/mobile/hooks/useDeviceFiles.ts
deleted file mode 100644
index aaa23be..0000000
--- a/mobile/hooks/useDeviceFiles.ts
+++ /dev/null
@@ -1,244 +0,0 @@
-import { useState, useEffect, useCallback } from 'react';
-import * as FileSystem from 'expo-file-system/legacy';
-import { safDirectory, type StoredFolder } from '../services/safDirectory';
-import { downloadRegistry } from '../services/downloadRegistry';
-import { useFileWatcher } from './useFileWatcher';
-import { FileDetectedEvent } from '../modules/expo-download-detect';
-
-export interface DeviceFile {
- id: string;
- uri: string;
- name: string;
- mimeType: string;
- size: number;
- createdAt: string;
- folderId?: string;
-}
-
-function guessMimeType(name: string): string {
- const ext = name.split('.').pop()?.toLowerCase() ?? '';
- const map: Record = {
- pdf: 'application/pdf',
- doc: 'application/msword',
- docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- xls: 'application/vnd.ms-excel',
- xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
- ppt: 'application/vnd.ms-powerpoint',
- pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
- txt: 'text/plain',
- csv: 'text/csv',
- json: 'application/json',
- xml: 'application/xml',
- zip: 'application/zip',
- rar: 'application/x-rar-compressed',
- mp4: 'video/mp4',
- mp3: 'audio/mpeg',
- jpg: 'image/jpeg',
- jpeg: 'image/jpeg',
- png: 'image/png',
- gif: 'image/gif',
- webp: 'image/webp',
- };
- return map[ext] ?? 'application/octet-stream';
-}
-
-function eventToDeviceFile(event: FileDetectedEvent): DeviceFile {
- return {
- id: event.id,
- uri: event.uri,
- name: event.name,
- mimeType: event.mimeType,
- size: event.size,
- createdAt: new Date(event.createdAt).toISOString(),
- };
-}
-
-const SKIP_SUBDIRS = new Set([
- 'Android', 'android', 'data', 'obb', 'cache',
- '.thumbnails', '.Trash', 'lost+found',
- 'LOST.DIR', 'System Volume Information',
- 'com.android', '.cache', 'tmp', '.tmp',
-]);
-
-function shouldSkipDir(name: string): boolean {
- if (name.startsWith('.')) return true;
- return SKIP_SUBDIRS.has(name);
-}
-
-async function scanSafFolder(folder: StoredFolder): Promise {
- try {
- const entries = await FileSystem.StorageAccessFramework.readDirectoryAsync(folder.uri);
- const files: DeviceFile[] = [];
- for (const entryUri of entries) {
- const parts = entryUri.split('/');
- const name = decodeURIComponent(parts[parts.length - 1]);
- if (name.startsWith('.')) continue;
- files.push({
- id: `saf_${folder.id}_${entryUri}`,
- uri: entryUri,
- name,
- mimeType: guessMimeType(name),
- size: 0,
- createdAt: new Date().toISOString(),
- folderId: folder.id,
- });
- }
- return files;
- } catch (err) {
- return [];
- }
-}
-
-export async function scanSubdirectories(
- baseUri: string,
- depth: number = 0,
- maxDepth: number = 3,
- discovered: Array<{ uri: string; name: string; parentUri: string }> = []
-): Promise> {
- if (depth >= maxDepth || discovered.length >= 300) return discovered;
-
- try {
- const entries = await FileSystem.StorageAccessFramework.readDirectoryAsync(baseUri);
- for (const entryUri of entries) {
- const parts = entryUri.split('/');
- const name = decodeURIComponent(parts[parts.length - 1]);
- if (shouldSkipDir(name)) continue;
-
- try {
- await FileSystem.StorageAccessFramework.readDirectoryAsync(entryUri);
- discovered.push({ uri: entryUri, name, parentUri: baseUri });
- await scanSubdirectories(entryUri, depth + 1, maxDepth, discovered);
- } catch {}
- }
- } catch {}
-
- return discovered;
-}
-
-export function useDeviceFiles() {
- const [files, setFiles] = useState([]);
- const [isLoading, setIsLoading] = useState(false);
- const [folders, setFolders] = useState(() => safDirectory.getAll());
- const [discovered, setDiscovered] = useState(() => safDirectory.getDiscovered());
- const { newFiles, clearNewFiles } = useFileWatcher();
-
- const scanVisibleFolders = useCallback(async () => {
- const visibleFolders = safDirectory.getVisibleFolders().filter((f) => f.source !== 'media-library');
- if (visibleFolders.length === 0) return;
-
- const results = await Promise.all(visibleFolders.map((folder) => scanSafFolder(folder)));
- const safFiles = results.flat();
-
- setFiles((prev) => {
- const existing = new Set(prev.filter((f) => !f.folderId).map((f) => f.id));
- const mediaOnly = prev.filter((f) => !f.folderId);
- const merged = [...mediaOnly];
- for (const f of safFiles) {
- if (!existing.has(f.id)) merged.push(f);
- }
- return merged;
- });
- }, []);
-
- const loadRegistryFiles = useCallback(() => {
- const registryEntries = downloadRegistry.getAll();
- const registryFiles: DeviceFile[] = registryEntries.map((entry) => ({
- id: entry.id,
- uri: entry.uri,
- name: entry.name,
- mimeType: entry.mimeType,
- size: entry.size,
- createdAt: new Date(entry.createdAt).toISOString(),
- }));
-
- setFiles((prev) => {
- const existing = new Set(prev.map((f) => f.id));
- const merged = [...prev];
- for (const f of registryFiles) {
- if (!existing.has(f.id)) merged.push(f);
- }
- return merged;
- });
- }, []);
-
- const pickAndScanRecursive = useCallback(async (): Promise<{ addedFolders: number }> => {
- try {
- const result = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
- if (!result.granted) return { addedFolders: 0 };
-
- const dirUri = result.directoryUri;
- const parts = dirUri.split('/');
- const dirName = decodeURIComponent(parts[parts.length - 1] ?? 'Dossier');
-
- safDirectory.addFolder(dirUri, dirName);
-
- const subdirs = await scanSubdirectories(dirUri);
- if (subdirs.length > 0) {
- safDirectory.addBatchFolders(
- subdirs.map((d) => ({ uri: d.uri, name: d.name, source: 'recursive', parentUri: d.parentUri }))
- );
- }
-
- setFolders(safDirectory.getAll());
-
- if (!discovered) {
- safDirectory.setDiscovered();
- setDiscovered(true);
- }
-
- await scanVisibleFolders();
-
- return { addedFolders: 1 + subdirs.length };
- } catch (err) {
- return { addedFolders: 0 };
- }
- }, [discovered, scanVisibleFolders]);
-
- const refreshFolders = useCallback(() => {
- setFolders(safDirectory.getAll());
- }, []);
-
- const rescan = useCallback(async () => {
- setIsLoading(true);
- try {
- await scanVisibleFolders();
- loadRegistryFiles();
- } finally {
- setIsLoading(false);
- }
- }, [scanVisibleFolders, loadRegistryFiles]);
-
- // Handle new files detected by native module
- useEffect(() => {
- if (newFiles.length === 0) return;
-
- downloadRegistry.addBatch(newFiles);
-
- const newDeviceFiles = newFiles.map(eventToDeviceFile);
- setFiles((prev) => {
- const existing = new Set(prev.map((f) => f.id));
- const merged = [...prev];
- for (const f of newDeviceFiles) {
- if (!existing.has(f.id)) merged.push(f);
- }
- return merged;
- });
-
- clearNewFiles();
- }, [newFiles, clearNewFiles]);
-
- // Initial load
- useEffect(() => {
- scanVisibleFolders();
- loadRegistryFiles();
- }, [scanVisibleFolders, loadRegistryFiles]);
-
- return {
- files, isLoading,
- rescan,
- pickAndScanRecursive,
- folders, refreshFolders,
- discovered,
- };
-}
-
diff --git a/mobile/hooks/useDeviceRegistration.ts b/mobile/hooks/useDeviceRegistration.ts
deleted file mode 100644
index ffb0cd7..0000000
--- a/mobile/hooks/useDeviceRegistration.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { useState, useEffect, useCallback } from 'react';
-import { Platform } from 'react-native';
-import * as SecureStore from 'expo-secure-store';
-import { apiClient } from '../api/client';
-import { ENDPOINTS } from '../constants/api';
-import { Device as DeviceType } from '../types';
-
-const DEVICE_ID_KEY = 'vaultdrop_device_id';
-const DEVICE_NAME_KEY = 'vaultdrop_device_name';
-const DEVICE_SERVER_ID_KEY = 'vaultdrop_device_server_id';
-
-function generateDefaultDeviceName(): string {
- const constants = Platform.constants as Record;
- const brand = String(constants?.Manufacturer ?? '');
- const model = String(constants?.Model ?? '');
- const suffix = Math.random().toString(36).slice(2, 6);
- const base = [brand, model].filter(Boolean).join(' ') || Platform.OS;
- return `${Platform.OS === 'ios' ? 'iOS' : 'Android'} ${base} (${suffix})`;
-}
-
-async function getOrCreateDeviceId(): Promise {
- let deviceId = await SecureStore.getItemAsync(DEVICE_ID_KEY);
- if (!deviceId) {
- deviceId = `device_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
- await SecureStore.setItemAsync(DEVICE_ID_KEY, deviceId);
- }
- return deviceId;
-}
-
-export async function getStoredDeviceServerId(): Promise {
- return SecureStore.getItemAsync(DEVICE_SERVER_ID_KEY);
-}
-
-export async function getStoredDeviceName(): Promise {
- return SecureStore.getItemAsync(DEVICE_NAME_KEY);
-}
-
-export function useDeviceRegistration() {
- const [device, setDevice] = useState<{ localId: string; serverId: string | null; name: string } | null>(null);
- const [isLoading, setIsLoading] = useState(true);
- const [isRegistered, setIsRegistered] = useState(null);
-
- const register = useCallback(async (deviceName: string) => {
- const localId = await getOrCreateDeviceId();
- const result = await apiClient.post<{ id: string; device_name: string; role: string }>(ENDPOINTS.DEVICES, {
- device_name: deviceName,
- });
-
- await SecureStore.setItemAsync(DEVICE_SERVER_ID_KEY, result.id);
- await SecureStore.setItemAsync(DEVICE_NAME_KEY, result.device_name);
-
- setDevice({ localId, serverId: result.id, name: result.device_name });
- setIsRegistered(true);
- return result;
- }, []);
-
- const checkRegistration = useCallback(async () => {
- try {
- const localId = await getOrCreateDeviceId();
- const storedName = await getStoredDeviceName();
-
- try {
- const devices = await apiClient.get(ENDPOINTS.DEVICES);
- if (devices.length > 0) {
- const existing = devices[0];
- await SecureStore.setItemAsync(DEVICE_SERVER_ID_KEY, existing.id);
- const name = storedName || existing.device_name;
- setDevice({ localId, serverId: existing.id, name });
- setIsRegistered(true);
- setIsLoading(false);
- return;
- }
- } catch {
- // Not registered yet
- }
-
- // Auto-register with a generated name instead of blocking
- const autoName = storedName || generateDefaultDeviceName();
- try {
- await register(autoName);
- } catch {
- setDevice({ localId, serverId: null, name: autoName });
- setIsRegistered(false);
- }
- } catch {
- setIsRegistered(false);
- } finally {
- setIsLoading(false);
- }
- }, [register]);
-
- useEffect(() => {
- checkRegistration();
- }, [checkRegistration]);
-
- return {
- device,
- isLoading,
- isRegistered,
- register,
- checkRegistration,
- };
-}
diff --git a/mobile/hooks/useFileWatcher.ts b/mobile/hooks/useFileWatcher.ts
deleted file mode 100644
index 479d33a..0000000
--- a/mobile/hooks/useFileWatcher.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { useEffect, useState, useCallback } from 'react';
-import { Platform } from 'react-native';
-import { ExpoDownloadDetectModule, FileDetectedEvent } from '../modules/expo-download-detect';
-
-export function useFileWatcher() {
- const [newFiles, setNewFiles] = useState([]);
- const [isSupported] = useState(() => Platform.OS === 'android');
-
- const clearNewFiles = useCallback(() => {
- setNewFiles([]);
- }, []);
-
- useEffect(() => {
- if (!isSupported) return;
-
- ExpoDownloadDetectModule.startWatching();
-
- const subscription = ExpoDownloadDetectModule.addListener('onNewFile', (event: FileDetectedEvent) => {
- setNewFiles((prev) => {
- if (prev.some((f) => f.id === event.id)) return prev;
- return [...prev, event];
- });
- });
-
- return () => {
- subscription.remove();
- ExpoDownloadDetectModule.stopWatching();
- };
- }, [isSupported]);
-
- return { newFiles, clearNewFiles, isSupported };
-}
diff --git a/mobile/hooks/useFiles.ts b/mobile/hooks/useFiles.ts
deleted file mode 100644
index ff39636..0000000
--- a/mobile/hooks/useFiles.ts
+++ /dev/null
@@ -1,384 +0,0 @@
-import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query';
-import { apiClient } from '../api/client';
-import { ENDPOINTS } from '../constants/api';
-import type { UnifiedFileItem, PaginatedResponse, FileItem, Tag } from '../types';
-import { fileStore } from '../services/fileStore';
-import { actionQueue } from '../services/actionQueue';
-import { useNetworkStatus } from './useNetworkStatus';
-
-function recordToUnifiedItem(record: ReturnType): UnifiedFileItem | null {
- if (!record) return null;
- return {
- id: record.id,
- backendResourceId: record.backendId ?? undefined,
- name: record.name,
- mimeType: record.mimeType,
- size: record.size,
- createdAt: record.createdAt,
- updatedAt: record.updatedAt,
- source: record.source as UnifiedFileItem['source'],
- syncStatus: record.syncStatus as UnifiedFileItem['syncStatus'],
- localUri: record.localUri ?? undefined,
- ocrText: record.ocrText ?? undefined,
- tags: record.tags ?? [],
- isFolder: record.isFolder === 1,
- parentResourceId: record.parentResourceId ?? undefined,
- ownerId: record.ownerId ?? undefined,
- thumbnailUrl: record.thumbnailUrl ?? undefined,
- thumbnailLocal: record.thumbnailLocal ?? undefined,
- isDeviceFile: record.source === 'local' && !record.backendId,
- };
-}
-
-export function useFiles(parentId?: string | null, page: number = 1, limit: number = 100) {
- const queryKey = parentId
- ? ['resources', parentId, page, limit]
- : ['resources', 'root', page, limit];
-
- const { isOnline } = useNetworkStatus();
-
- return useQuery({
- queryKey,
- queryFn: async () => {
-
- if ( !isOnline ) {
-
- if ( parentId ) {
-
- const children = fileStore.getChildrenByParent(parentId);
-
- return {
- data: children.map((r) => recordToUnifiedItem(r)!).filter(Boolean),
- meta: { page, total: children.length },
- };
-
- }
-
- const cached = fileStore.getRootFiles();
- const localDeviceFiles = fileStore.getLocalDeviceFiles();
- const validFiles = cached.files.filter(
- (f) => !f.backendId || returnedIds.has(f.backendId) || f.source === 'local',
- );
- const validIds = new Set(validFiles.map((f) => f.id));
- const extraLocal = localDeviceFiles.filter((f) => !validIds.has(f.id));
- const mergedFiles = [...validFiles, ...extraLocal];
-
- return {
- data: mergedFiles.map((r) => recordToUnifiedItem(r)!).filter(Boolean),
- meta: { page, total: ( cached.total) + extraLocal.length },
-
- };
-
- }
-
-
- if (parentId) {
- const backendRes = await apiClient.get>(
- `${ENDPOINTS.RESOURCES}/folders/${parentId}/resources?page=${page}&limit=${limit}&thumbnail=thumbnail_small`,
- );
- fileStore.mergeFromBackend(
- backendRes.data.map((f) => ({
- id: f.id,
- name: f.name,
- mimeType: f.mimeType,
- size: f.size,
- createdAt: f.createdAt,
- updatedAt: f.updatedAt,
- ocrText: f.ocrText,
- tags: f.tags,
- isFolder: f.isFolder,
- parentResourceId: f.parentResourceId,
- ownerId: f.ownerId,
- thumbnailUrl: f.thumbnailUrl,
- })),
- );
- const children = fileStore.getChildrenByParent(parentId);
- return {
- data: children.map((r) => recordToUnifiedItem(r)!).filter(Boolean),
- meta: { page, total: backendRes.meta?.total ?? children.length },
- };
- }
-
- const backendRes = await apiClient.get>(
- `${ENDPOINTS.RESOURCES}?page=${page}&limit=${limit}&thumbnail=thumbnail_small`,
- );
- const returnedIds = new Set(backendRes.data.map((f) => f.id));
-
- fileStore.mergeFromBackend(
- backendRes.data.map((f) => ({
- id: f.id,
- name: f.name,
- mimeType: f.mimeType,
- size: f.size,
- createdAt: f.createdAt,
- updatedAt: f.updatedAt,
- ocrText: f.ocrText,
- tags: f.tags,
- isFolder: f.isFolder,
- parentResourceId: f.parentResourceId,
- ownerId: f.ownerId,
- thumbnailUrl: f.thumbnailUrl,
- })),
- );
-
- const cached = fileStore.getRootFiles();
- const localDeviceFiles = fileStore.getLocalDeviceFiles();
- const validFiles = cached.files.filter(
- (f) => !f.backendId || returnedIds.has(f.backendId) || f.source === 'local',
- );
- const validIds = new Set(validFiles.map((f) => f.id));
- const extraLocal = localDeviceFiles.filter((f) => !validIds.has(f.id));
- const mergedFiles = [...validFiles, ...extraLocal];
- return {
- data: mergedFiles.map((r) => recordToUnifiedItem(r)!).filter(Boolean),
- meta: { page, total: (backendRes.meta?.total ?? cached.total) + extraLocal.length },
- };
- },
- placeholderData: keepPreviousData,
- initialData: () => {
- if (!parentId) {
- const cached = fileStore.getRootFiles();
- const localDeviceFiles = fileStore.getLocalDeviceFiles();
-
- const validIds = new Set(cached.files.map((f) => f.id));
- const extraLocal = localDeviceFiles.filter((f) => !validIds.has(f.id));
- const allFiles = [...cached.files, ...extraLocal];
- if (allFiles.length === 0) return undefined;
- return {
- data: allFiles.map((r) => recordToUnifiedItem(r)!).filter(Boolean),
- meta: { page: 0, total: cached.total + extraLocal.length },
- };
- }
- const children = fileStore.getChildrenByParent(parentId);
- if (children.length === 0) return undefined;
- return {
- data: children.map((r) => recordToUnifiedItem(r)!).filter(Boolean),
- meta: { page: 0, total: children.length },
- };
- },
- staleTime: 30_000,
- });
-}
-
-export function useFile(id: string) {
- return useQuery({
- queryKey: ['resources', id],
- queryFn: async () => {
- const res = await apiClient.get<{ data: FileItem }>(`${ENDPOINTS.RESOURCES}/${id}?thumbnail=thumbnail_small`);
- return res.data;
- },
- enabled: !!id,
- });
-}
-
-export function useDeleteFile() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: async (id: string) => {
- const record = fileStore.getByBackendId(id) ?? fileStore.getById(id);
-
- if (record?.backendId) {
- actionQueue.enqueue('delete', { backendId: record.backendId }, record.backendId);
- }
- fileStore.deleteByBackendId(id);
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['resources'] });
- },
- });
-}
-
-export function useAddTags() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: ({ fileId, tags }: { fileId: string; tags: string[] }) => {
- const record = fileStore.getById(fileId) ?? fileStore.getByBackendId(fileId);
- if (record) {
- const existingTags = record.tags ?? [];
- const newTags = [...existingTags, ...tags.map((t) => ({ id: t, tag_name: t }))];
- const uniqueTags = newTags.filter((t, i, arr) => arr.findIndex((x) => x.tag_name === t.tag_name) === i);
- fileStore.updatePartial(record.id, {});
- actionQueue.enqueue('tag_add', { fileId: record.backendId ?? record.id, tags }, record.backendId ?? record.id);
- }
- return Promise.resolve();
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['resources'] });
- },
- });
-}
-
-export function useMoveResources() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: ({ resourceIds, parentResourceId }: { resourceIds: string[]; parentResourceId: string | null }) => {
- const backendIds: string[] = [];
- for (const id of resourceIds) {
- const record = fileStore.getById(id) ?? fileStore.getByBackendId(id);
- if (record?.backendId) {
- backendIds.push(record.backendId);
- fileStore.updatePartial(record.id, { parentResourceId: parentResourceId ?? null });
- }
- }
- if (backendIds.length > 0) {
- actionQueue.enqueue('move', { resourceIds: backendIds, parentResourceId });
- }
- return Promise.resolve();
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['resources'] });
- },
- });
-}
-
-export function useFolders() {
- return useQuery({
- queryKey: ['folders'],
- queryFn: async () => {
- const backendRes = await apiClient.get<{ data: FileItem[] }>(ENDPOINTS.FOLDERS);
- fileStore.mergeFromBackend(
- backendRes.data.map((f) => ({
- id: f.id,
- name: f.name,
- mimeType: f.mimeType,
- size: f.size,
- createdAt: f.createdAt,
- updatedAt: f.updatedAt,
- ocrText: f.ocrText,
- tags: f.tags,
- isFolder: f.isFolder,
- parentResourceId: f.parentResourceId,
- ownerId: f.ownerId,
- thumbnailUrl: f.thumbnailUrl,
- })),
- );
- return fileStore.getAllFolders();
- },
- initialData: () => {
- const folders = fileStore.getAllFolders();
- return folders.length > 0 ? folders : undefined;
- },
- staleTime: 60_000,
- });
-}
-
-export function useCreateFolder() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: async ({ name, parentResourceId }: { name: string; parentResourceId?: string }) => {
- const now = new Date().toISOString();
- const localId = `local_folder_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
- fileStore.upsert({
- id: localId,
- backendId: null,
- name,
- mimeType: 'inode/directory',
- size: 0,
- source: 'local',
- localUri: null,
- syncStatus: 'local',
- parentResourceId: parentResourceId ?? null,
- isFolder: 1,
- ocrText: null,
- thumbnailUrl: null,
- ownerId: null,
- createdAt: now,
- updatedAt: now,
- lastSyncedAt: null,
- });
- actionQueue.enqueue('create_folder', { name, parentResourceId }, localId);
- return { id: localId, name };
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['resources'] });
- queryClient.invalidateQueries({ queryKey: ['folders'] });
- },
- });
-}
-
-export function useFilesByParent(parentId: string) {
- return useFiles(parentId);
-}
-
-export function useDownloadFile() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: async (file: UnifiedFileItem): Promise => {
- if (file.syncStatus !== 'cloud') {
- return file.localUri ?? '';
- }
-
- const res = await apiClient.get<{ url: string }>(
- `${ENDPOINTS.RESOURCES}/${file.backendResourceId ?? file.id}`,
- );
-
- const { downloadAsync, documentDirectory, makeDirectoryAsync } = await import('expo-file-system/legacy');
- const DOWNLOAD_DIR = `${documentDirectory}synced-files/`;
- await makeDirectoryAsync(DOWNLOAD_DIR, { intermediates: true });
-
- let hash = 0;
- for (let i = 0; i < file.name.length; i++) {
- hash = ((hash << 5) - hash + file.name.charCodeAt(i)) | 0;
- }
- const cacheKey = Math.abs(hash).toString(36);
- const dot = file.name.lastIndexOf('.');
- const ext = dot >= 0 ? file.name.slice(dot) : '';
- const fileUri = `${DOWNLOAD_DIR}${cacheKey}${ext}`;
-
- const result = await downloadAsync(res.url, fileUri);
-
- fileStore.upsert({
- id: file.backendResourceId ?? file.id,
- backendId: file.backendResourceId ?? file.id,
- name: file.name,
- mimeType: file.mimeType,
- size: file.size,
- source: 'synced',
- localUri: result.uri,
- syncStatus: 'synced',
- parentResourceId: file.parentResourceId ?? null,
- isFolder: 0,
- ocrText: file.ocrText ?? null,
- thumbnailUrl: file.thumbnailUrl ?? null,
- ownerId: file.ownerId ?? null,
- createdAt: file.createdAt,
- updatedAt: file.updatedAt ?? file.createdAt,
- lastSyncedAt: new Date().toISOString(),
- tags: file.tags,
- });
-
- queryClient.invalidateQueries({ queryKey: ['resources'] });
- return result.uri;
- },
- });
-}
-
-export function useFreeLocalSpace() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: async (fileIds: string[]) => {
- const { deleteAsync } = await import('expo-file-system/legacy');
- for (const fid of fileIds) {
- const entry = fileStore.getByBackendId(fid);
- if (!entry) continue;
-
- if (entry.localUri) {
- try {
- await deleteAsync(entry.localUri, { idempotent: true });
- } catch {}
- }
-
- fileStore.markAsCloudOnly(entry.id);
- }
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['resources'] });
- },
- });
-}
diff --git a/mobile/hooks/useLocalFiles.ts b/mobile/hooks/useLocalFiles.ts
deleted file mode 100644
index bc594fa..0000000
--- a/mobile/hooks/useLocalFiles.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import { useMemo, useEffect, useRef } from 'react';
-import { useQueryClient } from '@tanstack/react-query';
-import { useDeviceFiles } from './useDeviceFiles';
-import { fileStore } from '../services/fileStore';
-import { generateLocalThumbnail } from '../services/thumbnail';
-import { UnifiedFileItem } from '../types';
-
-export function useLocalFiles() {
- const { files: deviceFiles, isLoading: deviceLoading, rescan, pickAndScanRecursive, folders, refreshFolders, discovered } = useDeviceFiles();
- const queryClient = useQueryClient();
- const lastDeviceCount = useRef(0);
- const thumbnailQueue = useRef>(new Set());
-
- useEffect(() => {
- if (deviceFiles.length === 0) return;
- if (deviceFiles.length === lastDeviceCount.current) return;
- lastDeviceCount.current = deviceFiles.length;
-
- const toMerge = deviceFiles.map((df) => ({
- id: df.id,
- uri: df.uri,
- name: df.name,
- mimeType: df.mimeType,
- size: df.size,
- createdAt: df.createdAt,
- folderId: df.folderId,
- }));
-
- fileStore.mergeFromDevice(toMerge);
-
- const jobs: Promise[] = [];
- for (const df of toMerge) {
- if (thumbnailQueue.current.has(df.id)) continue;
- const mime = (df.mimeType ?? '').toLowerCase();
- if (!mime.startsWith('image/')) continue;
- if (fileStore.getById(df.id)?.thumbnailLocal) continue;
- thumbnailQueue.current.add(df.id);
- jobs.push(
- generateLocalThumbnail(df.uri, df.mimeType).then((thumb) => {
- try {
- if (thumb) fileStore.setThumbnailLocal(df.id, thumb);
- } catch {}
- }).finally(() => {
- thumbnailQueue.current.delete(df.id);
- }),
- );
- }
- if (jobs.length > 0) {
- Promise.all(jobs).finally(() => {
- queryClient.invalidateQueries({ queryKey: ['resources'] });
- });
- }
- }, [deviceFiles, queryClient]);
-
- const localFiles = useMemo(() => {
- const registryEntries = fileStore.getAllLocal();
- const merged = new Map();
-
- for (const entry of registryEntries) {
- merged.set(entry.id, {
- id: entry.id,
- backendResourceId: entry.backendId ?? undefined,
- name: entry.name,
- mimeType: entry.mimeType,
- size: entry.size,
- createdAt: entry.createdAt,
- source: entry.source as UnifiedFileItem['source'],
- syncStatus: entry.syncStatus as UnifiedFileItem['syncStatus'],
- localUri: entry.localUri ?? undefined,
- thumbnailUrl: entry.thumbnailUrl ?? undefined,
- thumbnailLocal: entry.thumbnailLocal ?? undefined,
- tags: entry.tags ?? [],
- isFolder: entry.isFolder === 1,
- parentResourceId: entry.parentResourceId ?? undefined,
- isDeviceFile: entry.source === 'local' && !entry.backendId,
- });
- }
-
- for (const df of deviceFiles) {
- if (!merged.has(df.id) && !fileStore.isDeleted(df.id)) {
- merged.set(df.id, {
- id: df.id,
- name: df.name,
- mimeType: df.mimeType,
- size: df.size,
- createdAt: df.createdAt,
- source: 'local',
- syncStatus: 'local',
- localUri: df.uri,
- tags: [],
- isFolder: false,
- isDeviceFile: true,
- parentResourceId: df.folderId,
- });
- }
- }
-
- return Array.from(merged.values());
- }, [deviceFiles]);
-
- return {
- localFiles,
- isLoading: deviceLoading,
- rescan,
- pickAndScanRecursive,
- folders,
- refreshFolders,
- discovered,
- };
-}
diff --git a/mobile/hooks/useNetworkStatus.ts b/mobile/hooks/useNetworkStatus.ts
deleted file mode 100644
index 9a23b65..0000000
--- a/mobile/hooks/useNetworkStatus.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useSyncExternalStore, useRef } from 'react';
-import NetInfo, { NetInfoState, NetInfoSubscription } from '@react-native-community/netinfo';
-
-type Listener = () => void;
-
-let state: NetInfoState | null = null;
-const listeners = new Set();
-let subscription: NetInfoSubscription | null = null;
-
-function subscribe(listener: Listener): () => void {
- listeners.add(listener);
- return () => { listeners.delete(listener); };
-}
-
-function getSnapshot(): boolean {
- return state?.isConnected ?? true;
-}
-
-function initIfNeeded() {
- if (subscription) return;
- NetInfo.fetch().then((info) => {
- state = info;
- listeners.forEach((l) => l());
- });
- subscription = NetInfo.addEventListener((info) => {
- state = info;
- listeners.forEach((l) => l());
- });
-}
-
-export interface NetworkStatus {
- isOnline: boolean;
- isWifi: boolean;
- isCellular: boolean;
- connectionType: string;
- isInternetReachable: boolean | null;
-}
-
-export function useNetworkStatus(): NetworkStatus {
- const mountRef = useRef(false);
- if (!mountRef.current) {
- initIfNeeded();
- mountRef.current = true;
- }
-
- const isConnected = useSyncExternalStore(subscribe, getSnapshot);
-
- return {
- isOnline: isConnected,
- isWifi: state?.type === 'wifi',
- isCellular: state?.type === 'cellular',
- connectionType: state?.type ?? 'unknown',
- isInternetReachable: state?.isInternetReachable ?? null,
- };
-}
diff --git a/mobile/hooks/usePdfGeneration.ts b/mobile/hooks/usePdfGeneration.ts
deleted file mode 100644
index 25694be..0000000
--- a/mobile/hooks/usePdfGeneration.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useState, useCallback } from 'react';
-import * as Print from 'expo-print';
-
-export function usePdfGeneration() {
- const [generating, setGenerating] = useState(false);
- const [progress, setProgress] = useState(0);
-
- const generatePdf = useCallback(async (items: { uri: string }[], progressCb?: (pct: number) => void): Promise => {
- if (items.length === 0) return null;
-
- setGenerating(true);
- setProgress(0);
- progressCb?.(0);
-
- try {
- const imagesHtml = items.map((item) => {
- return `
-

-
`;
- }).join('');
-
- const html = `
-
-
-
-
-
-
-${imagesHtml}
-`;
-
- const { uri } = await Print.printToFileAsync({ html });
- setProgress(100);
- progressCb?.(100);
- return uri;
- } catch (err) {
- console.error('PDF generation failed:', err);
- return null;
- } finally {
- setGenerating(false);
- }
- }, []);
-
- return {
- generatePdf,
- generating,
- progress,
- };
-}
diff --git a/mobile/hooks/usePendingActions.ts b/mobile/hooks/usePendingActions.ts
deleted file mode 100644
index f73348a..0000000
--- a/mobile/hooks/usePendingActions.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useState, useEffect, useCallback, useSyncExternalStore, useRef } from 'react';
-import { actionQueue } from '../services/actionQueue';
-import { useNetworkStatus } from './useNetworkStatus';
-
-function subscribe(listener: () => void): () => void {
- return actionQueue.subscribe(listener);
-}
-
-function getSnapshot(): number {
- return actionQueue.getPendingCount();
-}
-
-export function usePendingActions() {
- const pendingCount = useSyncExternalStore(subscribe, getSnapshot);
- const { isOnline } = useNetworkStatus();
- const wasOffline = useRef(!isOnline);
-
- useEffect(() => {
- if (isOnline && wasOffline.current && pendingCount > 0) {
- actionQueue.processAll();
- }
- wasOffline.current = !isOnline;
- }, [isOnline, pendingCount]);
-
- useEffect(() => {
- if (pendingCount > 0 && isOnline) {
- actionQueue.scheduleRetry(5_000);
- }
- return () => actionQueue.cancelSchedule();
- }, [pendingCount, isOnline]);
-
- return {
- pendingCount,
- isProcessing: actionQueue.isProcessing(),
- processAll: useCallback(() => actionQueue.processAll(), []),
- };
-}
diff --git a/mobile/hooks/usePollOcr.ts b/mobile/hooks/usePollOcr.ts
deleted file mode 100644
index 320147d..0000000
--- a/mobile/hooks/usePollOcr.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import React, { useEffect } from 'react';
-import { apiClient } from '../api/client';
-import { ENDPOINTS } from '../constants/api';
-import { fileStore } from '../services/fileStore';
-import { useOcrDone } from '../contexts/SseContext';
-
-function handleOcrDone(resourceId: string) {
- apiClient
- .get<{ data: { ocrText?: string } }>(`${ENDPOINTS.RESOURCES}/${resourceId}`)
- .then((detail) => {
- const ocrText = detail.data?.ocrText;
- if (ocrText) {
- fileStore.updatePartial(resourceId, { ocrText });
- }
- })
- .catch(() => {});
-}
-
-export function SseOcrListener() {
- const { onOcrDone } = useOcrDone();
-
- useEffect(() => {
- const unsub = onOcrDone(handleOcrDone);
- return unsub;
- }, [onOcrDone]);
-
- return null;
-}
diff --git a/mobile/hooks/usePullSync.ts b/mobile/hooks/usePullSync.ts
deleted file mode 100644
index 2c45031..0000000
--- a/mobile/hooks/usePullSync.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { useCallback, useRef } from 'react';
-import { useQueryClient } from '@tanstack/react-query';
-import { downloadAsync, documentDirectory, makeDirectoryAsync } from 'expo-file-system/legacy';
-import { fileStore } from '../services/fileStore';
-import { apiClient } from '../api/client';
-import { ENDPOINTS } from '../constants/api';
-import { setIsSyncing } from './useSyncQueue';
-
-const SYNC_DIR = `${documentDirectory}synced-files/`;
-
-export function usePullSync() {
- const queryClient = useQueryClient();
- const isRunning = useRef(false);
-
- const pullNewFiles = useCallback(async () => {
- if (isRunning.current) return { pulled: 0 };
- isRunning.current = true;
-
- try {
- setIsSyncing(true);
-
- let page = 1;
- const limit = 100;
- let total = 0;
- const backendResources: Array<{
- id: string;
- name: string;
- mimeType: string;
- size: number;
- createdAt: string;
- url?: string;
- thumbnailUrl?: string;
- ownerId?: string;
- }> = [];
-
- do {
- const res = await apiClient.get<{ data: Array; 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 registry = fileStore.getAllSynced();
- const existingBackendIds = new Set(
- registry.filter((e) => e.backendId).map((e) => e.backendId)
- );
-
- let pulled = 0;
-
- for (const br of backendResources) {
- if (existingBackendIds.has(br.id)) continue;
- if (br.size === 0) continue;
-
- try {
- const detail = await apiClient.get<{ url: string }>(`${ENDPOINTS.RESOURCES}/${br.id}`);
- const downloadUrl = detail.url;
-
- await makeDirectoryAsync(SYNC_DIR, { intermediates: true });
- const safeName = br.name.replace(/[^a-zA-Z0-9._-]/g, '_');
- const fileUri = `${SYNC_DIR}${br.id}_${safeName}`;
-
- const result = await downloadAsync(downloadUrl, fileUri);
-
- fileStore.upsert({
- id: br.id,
- backendId: br.id,
- name: br.name,
- mimeType: br.mimeType,
- size: br.size,
- source: 'synced',
- localUri: result.uri,
- syncStatus: 'synced',
- parentResourceId: null,
- isFolder: 0,
- ocrText: null,
- thumbnailUrl: br.thumbnailUrl ?? null,
- ownerId: br.ownerId ?? null,
- createdAt: br.createdAt,
- updatedAt: br.createdAt,
- lastSyncedAt: new Date().toISOString(),
- });
- pulled++;
- } catch {
- // skip individual file failures
- }
- }
-
- if (pulled > 0) {
- queryClient.invalidateQueries({ queryKey: ['resources'] });
- }
-
- return { pulled };
- } finally {
- setIsSyncing(false);
- isRunning.current = false;
- }
- }, [queryClient]);
-
- return { pullNewFiles };
-}
diff --git a/mobile/hooks/useSSEResource.ts b/mobile/hooks/useSSEResource.ts
deleted file mode 100644
index bfe7028..0000000
--- a/mobile/hooks/useSSEResource.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { useEffect } from 'react';
-import { useQueryClient } from '@tanstack/react-query';
-import { useResourceCreated } from '../contexts/SseContext';
-
-export function SseResourceListener() {
- const queryClient = useQueryClient();
- const { onResourceCreated } = useResourceCreated();
-
- useEffect(() => {
- const unsub = onResourceCreated(() => {
- queryClient.invalidateQueries({ queryKey: ['resources'] });
- });
- return unsub;
- }, [onResourceCreated, queryClient]);
-
- return null;
-}
diff --git a/mobile/hooks/useSearch.ts b/mobile/hooks/useSearch.ts
deleted file mode 100644
index 4080f68..0000000
--- a/mobile/hooks/useSearch.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { useMemo } from 'react';
-import { fileStore } from '../services/fileStore';
-import { UnifiedFileItem } from '../types';
-
-function recordToUnifiedItem(record: ReturnType): UnifiedFileItem | null {
- if (!record) return null;
- return {
- id: record.id,
- backendResourceId: record.backendId ?? undefined,
- name: record.name,
- mimeType: record.mimeType,
- size: record.size,
- createdAt: record.createdAt,
- updatedAt: record.updatedAt,
- source: record.source as UnifiedFileItem['source'],
- syncStatus: record.syncStatus as UnifiedFileItem['syncStatus'],
- localUri: record.localUri ?? undefined,
- ocrText: record.ocrText ?? undefined,
- tags: record.tags ?? [],
- isFolder: record.isFolder === 1,
- parentResourceId: record.parentResourceId ?? undefined,
- ownerId: record.ownerId ?? undefined,
- thumbnailUrl: record.thumbnailUrl ?? undefined,
- thumbnailLocal: record.thumbnailLocal ?? undefined,
- isDeviceFile: record.source === 'local' && !record.backendId,
- };
-}
-
-export function useSearch(query: string) {
- const results = useMemo(() => {
- if (!query.trim()) return [];
- const records = fileStore.searchFts(query);
- if (records.length === 0) {
- const fallback = fileStore.search(query);
- return fallback.map((r) => recordToUnifiedItem(r)!).filter(Boolean);
- }
- return records.map((r) => recordToUnifiedItem(r)!).filter(Boolean);
- }, [query]);
-
- return {
- data: results,
- isLoading: false,
- };
-}
diff --git a/mobile/hooks/useShare.ts b/mobile/hooks/useShare.ts
deleted file mode 100644
index 274940e..0000000
--- a/mobile/hooks/useShare.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { apiClient } from '../api/client';
-import { ShareEntry } from '../types';
-
-function shareEndpoint(resourceId: string) {
- return `/resources/${resourceId}/share`;
-}
-
-export function useShares(resourceId: string) {
- return useQuery({
- queryKey: ['shares', resourceId],
- queryFn: () => apiClient.get(shareEndpoint(resourceId)),
- enabled: !!resourceId,
- });
-}
-
-export function useGrantShare() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: ({ resourceId, subjectUserId, role }: { resourceId: string; subjectUserId: string; role: string }) =>
- apiClient.post(shareEndpoint(resourceId), { subject_user_id: subjectUserId, role }),
- onSuccess: (_data, variables) => {
- queryClient.invalidateQueries({ queryKey: ['shares', variables.resourceId] });
- },
- });
-}
-
-export function useRevokeShare() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: ({ resourceId, userId }: { resourceId: string; userId: string }) =>
- apiClient.delete(`${shareEndpoint(resourceId)}/${userId}`),
- onSuccess: (_data, variables) => {
- queryClient.invalidateQueries({ queryKey: ['shares', variables.resourceId] });
- },
- });
-}
-
-export function useCheckAccess(resourceId: string) {
- return useQuery({
- queryKey: ['access', resourceId],
- queryFn: () => apiClient.get<{ role: string; access: boolean }>(`/resources/${resourceId}/access`),
- enabled: !!resourceId,
- });
-}
diff --git a/mobile/hooks/useSyncPull.ts b/mobile/hooks/useSyncPull.ts
deleted file mode 100644
index 69b08a3..0000000
--- a/mobile/hooks/useSyncPull.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useCallback, useRef } from 'react';
-import { apiClient } from '../api/client';
-import { ENDPOINTS } from '../constants/api';
-import { SyncQueueItem } from '../types';
-
-export function useSyncPull() {
- const isRunning = useRef(false);
-
- const pull = useCallback(async (locationId?: string) => {
- if (isRunning.current) return { items: [] };
- isRunning.current = true;
-
- try {
- const body = locationId ? { location_id: locationId } : {};
- const result = await apiClient.post(ENDPOINTS.SYNC_PULL, body);
- return { items: result };
- } finally {
- isRunning.current = false;
- }
- }, []);
-
- return { pull };
-}
diff --git a/mobile/hooks/useSyncPush.ts b/mobile/hooks/useSyncPush.ts
deleted file mode 100644
index 0c18502..0000000
--- a/mobile/hooks/useSyncPush.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useCallback, useRef } from 'react';
-import { apiClient } from '../api/client';
-import { ENDPOINTS } from '../constants/api';
-
-export function useSyncPush() {
- const isRunning = useRef(false);
-
- const push = useCallback(async (locationId: string) => {
- if (isRunning.current) return { pending: 0 };
- isRunning.current = true;
-
- try {
- const result = await apiClient.post<{ pending: number; message: string }>(ENDPOINTS.SYNC_PUSH, {
- location_id: locationId,
- });
- return { pending: result.pending };
- } finally {
- isRunning.current = false;
- }
- }, []);
-
- return { push };
-}
diff --git a/mobile/hooks/useSyncQueue.ts b/mobile/hooks/useSyncQueue.ts
deleted file mode 100644
index dddb580..0000000
--- a/mobile/hooks/useSyncQueue.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import { useState, useEffect, useCallback, useSyncExternalStore } from 'react';
-import { createMMKV } from 'react-native-mmkv';
-import { fileStore } from '../services/fileStore';
-
-export type SyncProgress = {
- files: Array<{ id: string; name: string }>;
- currentIndex: number;
-} | null;
-
-const syncStateStorage = createMMKV({ id: 'vaultdrop-sync-state' });
-const PROGRESS_KEY = 'sync_progress';
-
-let syncRunning = false;
-let cancelRequested = false;
-const listeners = new Set<() => void>();
-
-function notify() {
- listeners.forEach((l) => l());
-}
-
-export function getIsSyncing(): boolean {
- return syncStateStorage.getString('is_syncing') === 'true';
-}
-
-export function setIsSyncing(value: boolean) {
- syncStateStorage.set('is_syncing', value ? 'true' : 'false');
- notify();
-}
-
-export function getSyncProgress(): SyncProgress {
- return parseProgress(syncStateStorage.getString(PROGRESS_KEY));
-}
-
-function parseProgress(raw: string | undefined): SyncProgress {
- if (!raw) return null;
- try {
- const parsed = JSON.parse(raw) as { files: Array<{ id: string; name: string }>; currentIndex: number };
- if (!parsed || !Array.isArray(parsed.files)) return null;
- return parsed;
- } catch {
- return null;
- }
-}
-
-export function setSyncProgress(value: SyncProgress) {
- if (value === null) {
- syncStateStorage.remove(PROGRESS_KEY);
- } else {
- syncStateStorage.set(PROGRESS_KEY, JSON.stringify(value));
- }
- notify();
-}
-
-export function isSyncLoopRunning(): boolean {
- return syncRunning;
-}
-
-export function setSyncLoopRunning(value: boolean) {
- syncRunning = value;
-}
-
-export function requestSyncCancel() {
- cancelRequested = true;
-}
-
-export function isSyncCancelRequested(): boolean {
- return cancelRequested;
-}
-
-export function consumeSyncCancel(): boolean {
- const value = cancelRequested;
- cancelRequested = false;
- return value;
-}
-
-export function resetSyncState() {
- syncStateStorage.remove('is_syncing');
- syncStateStorage.remove(PROGRESS_KEY);
- syncRunning = false;
- cancelRequested = false;
- notify();
-}
-
-function subscribe(listener: () => void): () => void {
- listeners.add(listener);
- return () => listeners.delete(listener);
-}
-
-let cachedIsSyncing = getIsSyncing();
-let lastIsSyncingRaw: string | undefined;
-
-function getIsSyncingSnapshot(): boolean {
- const raw = syncStateStorage.getString('is_syncing');
- if (raw !== lastIsSyncingRaw) {
- lastIsSyncingRaw = raw;
- cachedIsSyncing = raw === 'true';
- }
- return cachedIsSyncing;
-}
-
-let cachedProgress = getSyncProgress();
-let lastProgressRaw: string | undefined;
-
-function getSyncProgressSnapshot(): SyncProgress {
- const raw = syncStateStorage.getString(PROGRESS_KEY);
- if (raw !== lastProgressRaw) {
- lastProgressRaw = raw;
- cachedProgress = parseProgress(raw);
- }
- return cachedProgress;
-}
-
-export function useSyncQueue() {
- const isSyncing = useSyncExternalStore(subscribe, getIsSyncingSnapshot, getIsSyncingSnapshot);
- const [pendingCount, setPendingCount] = useState(0);
-
- const refresh = useCallback(() => {
- setPendingCount(fileStore.countPendingSync());
- }, []);
-
- useEffect(() => {
- refresh();
- const interval = setInterval(refresh, 5000);
- return () => clearInterval(interval);
- }, [refresh]);
-
- return { pendingCount, isSyncing, refresh };
-}
-
-export function useSyncProgress() {
- const syncProgress = useSyncExternalStore(subscribe, getSyncProgressSnapshot, getSyncProgressSnapshot);
- return { syncProgress };
-}
\ No newline at end of file
diff --git a/mobile/hooks/useUpload.ts b/mobile/hooks/useUpload.ts
deleted file mode 100644
index d73cf23..0000000
--- a/mobile/hooks/useUpload.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { File, UploadType } from 'expo-file-system';
-import { apiClient } from '../api/client';
-import { API_BASE_URL, ENDPOINTS } from '../constants/api';
-import { ApiError, UploadError } from '../types';
-
-export type UploadFile = { uri: string; type: string; name: string };
-export type UploadResult = { name: string; id: string };
-
-export function useUpload() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: async (files: UploadFile[]) => {
- const results = await Promise.allSettled(
- files.map(async (file) => {
- const fsFile = new File(file.uri);
- const headers: Record = {};
- const token = apiClient.getAccessToken();
- if (token) {
- headers['Authorization'] = `Bearer ${token}`;
- }
- const result = await fsFile.upload(`${API_BASE_URL}${ENDPOINTS.UPLOAD}`, {
- httpMethod: 'POST',
- uploadType: UploadType.MULTIPART,
- fieldName: 'file',
- mimeType: file.type,
- headers,
- });
-
- if (result.status >= 400) {
- let serverMessage = 'Erreur serveur';
- let serverCode: string | undefined;
- try {
- const body: ApiError = JSON.parse(result.body);
- serverMessage = body.error?.message || serverMessage;
- serverCode = body.error?.code;
- } catch {
- serverMessage = result.body || serverMessage;
- }
- throw new UploadError(file.name, result.status, serverMessage, serverCode);
- }
-
- const body = JSON.parse(result.body);
- const items = body.data ?? body;
- const item = Array.isArray(items) ? items[0] : items;
- return item as UploadResult;
- })
- );
-
- const uploaded: UploadResult[] = [];
- const errors: UploadError[] = [];
-
- results.forEach((r, i) => {
- if (r.status === 'fulfilled') {
- uploaded.push(r.value);
- } else {
- const reason = r.reason;
- if (reason instanceof UploadError) {
- errors.push(reason);
- } else {
- errors.push(new UploadError(files[i].name, 0, reason?.message || 'Upload failed'));
- }
- }
- });
-
- if (errors.length > 0 && uploaded.length === 0) {
- throw errors[0];
- }
-
- return { uploaded, errors };
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['resources'] });
- },
- });
-}
-
-
diff --git a/mobile/hooks/useUploadQueue.ts b/mobile/hooks/useUploadQueue.ts
deleted file mode 100644
index 92ee1c6..0000000
--- a/mobile/hooks/useUploadQueue.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { useSyncExternalStore } from 'react';
-import { uploadQueue, UploadFile } from '../services/uploadQueue';
-
-export function useUploadQueue() {
- const tasks = useSyncExternalStore(
- uploadQueue.subscribe.bind(uploadQueue),
- uploadQueue.getTasks.bind(uploadQueue),
- );
-
- return {
- tasks,
- enqueue: (files: UploadFile[]) => uploadQueue.enqueue(files),
- cancel: (id: string) => uploadQueue.cancel(id),
- retry: (id: string) => uploadQueue.retry(id),
- retryAll: () => uploadQueue.retryAll(),
- };
-}
diff --git a/mobile/modules/expo-download-detect/android/build.gradle b/mobile/modules/expo-download-detect/android/build.gradle
deleted file mode 100644
index 9c2faae..0000000
--- a/mobile/modules/expo-download-detect/android/build.gradle
+++ /dev/null
@@ -1,15 +0,0 @@
-plugins {
- id 'com.android.library'
- id 'expo-module-gradle-plugin'
-}
-
-group = 'host.exp.exponent'
-version = '0.1.0'
-
-android {
- namespace "expo.modules.downloaddetect"
- defaultConfig {
- versionCode 1
- versionName "0.1.0"
- }
-}
diff --git a/mobile/modules/expo-download-detect/android/src/main/AndroidManifest.xml b/mobile/modules/expo-download-detect/android/src/main/AndroidManifest.xml
deleted file mode 100644
index c4e83de..0000000
--- a/mobile/modules/expo-download-detect/android/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/mobile/modules/expo-download-detect/android/src/main/java/expo/modules/downloaddetect/DownloadBroadcastReceiver.kt b/mobile/modules/expo-download-detect/android/src/main/java/expo/modules/downloaddetect/DownloadBroadcastReceiver.kt
deleted file mode 100644
index 1d372ba..0000000
--- a/mobile/modules/expo-download-detect/android/src/main/java/expo/modules/downloaddetect/DownloadBroadcastReceiver.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package expo.modules.downloaddetect
-
-import android.app.DownloadManager
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-
-data class DetectedFile(
- val id: String,
- val uri: String,
- val name: String,
- val mimeType: String,
- val size: Long,
- val createdAt: Long
-)
-
-class DownloadBroadcastReceiver(
- private val onFileDetected: (DetectedFile) -> Unit
-) : BroadcastReceiver() {
-
- override fun onReceive(context: Context, intent: Intent) {
- if (intent.action != DownloadManager.ACTION_DOWNLOAD_COMPLETE) return
-
- val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
- if (downloadId == -1L) return
-
- val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
- val query = DownloadManager.Query().setFilterById(downloadId)
-
- dm.query(query)?.use { cursor ->
- if (!cursor.moveToFirst()) return
-
- val uriIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
- val titleIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TITLE)
- val mimeTypeIndex = cursor.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE)
- val sizeIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
- val dateIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP)
-
- val uri = if (uriIndex >= 0) cursor.getString(uriIndex) ?: "" else ""
- val title = if (titleIndex >= 0) cursor.getString(titleIndex) ?: "unknown" else "unknown"
- val mimeType = if (mimeTypeIndex >= 0) cursor.getString(mimeTypeIndex) ?: "application/octet-stream" else "application/octet-stream"
- val size = if (sizeIndex >= 0) cursor.getLong(sizeIndex) else 0L
- val date = if (dateIndex >= 0) cursor.getLong(dateIndex) else System.currentTimeMillis()
-
- // Extract filename from URI
- val name = extractFileName(uri, title)
-
- onFileDetected(DetectedFile(
- id = "download_$downloadId",
- uri = uri,
- name = name,
- mimeType = mimeType,
- size = size,
- createdAt = date
- ))
- }
- }
-
- private fun extractFileName(uri: String, fallback: String): String {
- if (uri.isNotEmpty()) {
- // content://media/external/downloads/123 or file:///storage/...
- val path = uri.substringAfterLast("/")
- if (path.isNotEmpty() && path.all { it.isDigit() }.not()) {
- return decodeFileName(path)
- }
- }
- return fallback
- }
-
- private fun decodeFileName(encoded: String): String {
- return try {
- java.net.URLDecoder.decode(encoded, "UTF-8")
- } catch (_: Exception) {
- encoded
- }
- }
-}
diff --git a/mobile/modules/expo-download-detect/android/src/main/java/expo/modules/downloaddetect/ExpoDownloadDetectModule.kt b/mobile/modules/expo-download-detect/android/src/main/java/expo/modules/downloaddetect/ExpoDownloadDetectModule.kt
deleted file mode 100644
index 69250e5..0000000
--- a/mobile/modules/expo-download-detect/android/src/main/java/expo/modules/downloaddetect/ExpoDownloadDetectModule.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-package expo.modules.downloaddetect
-
-import android.app.DownloadManager
-import android.content.Context
-import android.content.IntentFilter
-import android.net.Uri
-import android.os.Handler
-import android.os.Looper
-import android.provider.MediaStore
-import androidx.core.os.bundleOf
-import expo.modules.kotlin.modules.Module
-import expo.modules.kotlin.modules.ModuleDefinition
-
-class ExpoDownloadDetectModule : Module() {
-
- private var broadcastReceiver: DownloadBroadcastReceiver? = null
- private var contentObserver: MediaStoreObserver? = null
- private var isWatching = false
-
- override fun definition() = ModuleDefinition {
- Name("ExpoDownloadDetect")
-
- Events("onNewFile")
-
- Function("startWatching") {
- if (!isWatching) startObserving()
- null
- }
-
- Function("stopWatching") {
- stopObserving()
- }
-
- AsyncFunction("getRecentDownloads") {
- queryRecentFiles()
- }
- }
-
- private val context: Context
- get() = requireNotNull(appContext.reactContext)
-
- private fun startObserving() {
- val ctx = context
-
- // Register BroadcastReceiver for DownloadManager
- broadcastReceiver = DownloadBroadcastReceiver { event ->
- sendEvent("onNewFile", bundleOf(
- "id" to event.id,
- "uri" to event.uri,
- "name" to event.name,
- "mimeType" to event.mimeType,
- "size" to event.size,
- "createdAt" to event.createdAt,
- "source" to "download"
- ))
- }
-
- val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
- ctx.registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
-
- // Register ContentObserver on MediaStore
- val handler = Handler(Looper.getMainLooper())
- contentObserver = MediaStoreObserver(handler, ctx) { newFiles ->
- for (file in newFiles) {
- sendEvent("onNewFile", bundleOf(
- "id" to file.id,
- "uri" to file.uri,
- "name" to file.name,
- "mimeType" to file.mimeType,
- "size" to file.size,
- "createdAt" to file.createdAt,
- "source" to "mediastore"
- ))
- }
- }
-
- val uri = MediaStore.Files.getContentUri("external")
- ctx.contentResolver.registerContentObserver(uri, true, contentObserver!!)
-
- // Scan existing files on startup
- val existingFiles = queryRecentFiles()
- for (file in existingFiles) {
- sendEvent("onNewFile", bundleOf(
- "id" to file.id,
- "uri" to file.uri,
- "name" to file.name,
- "mimeType" to file.mimeType,
- "size" to file.size,
- "createdAt" to file.createdAt,
- "source" to "startup"
- ))
- }
-
- isWatching = true
- }
-
- private fun stopObserving() {
- val ctx = context
-
- broadcastReceiver?.let {
- try {
- ctx.unregisterReceiver(it)
- } catch (_: Exception) {}
- broadcastReceiver = null
- }
-
- contentObserver?.let {
- ctx.contentResolver.unregisterContentObserver(it)
- contentObserver = null
- }
-
- isWatching = false
- }
-
- private fun queryRecentFiles(): List {
- val files = mutableListOf()
- val projection = arrayOf(
- MediaStore.Files.FileColumns._ID,
- MediaStore.Files.FileColumns.DISPLAY_NAME,
- MediaStore.Files.FileColumns.MIME_TYPE,
- MediaStore.Files.FileColumns.SIZE,
- MediaStore.Files.FileColumns.DATE_ADDED
- )
-
- val selection = "${MediaStore.Files.FileColumns.SIZE} > 0"
- val sortOrder = "${MediaStore.Files.FileColumns.DATE_ADDED} DESC"
- val limit = 100
-
- context.contentResolver.query(
- MediaStore.Files.getContentUri("external"),
- projection,
- selection,
- null,
- sortOrder
- )?.use { cursor ->
- val idCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
- val nameCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
- val mimeCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE)
- val sizeCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE)
- val dateCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_ADDED)
-
- var count = 0
- while (cursor.moveToNext() && count < limit) {
- val id = cursor.getLong(idCol)
- val name = cursor.getString(nameCol) ?: continue
- val mimeType = cursor.getString(mimeCol) ?: "application/octet-stream"
- val size = cursor.getLong(sizeCol)
- val dateAdded = cursor.getLong(dateCol)
-
- val uri = Uri.withAppendedPath(
- MediaStore.Files.getContentUri("external"),
- id.toString()
- ).toString()
-
- files.add(DetectedFile(
- id = "media_$id",
- uri = uri,
- name = name,
- mimeType = mimeType,
- size = size,
- createdAt = dateAdded * 1000L
- ))
- count++
- }
- }
-
- return files
- }
-}
diff --git a/mobile/modules/expo-download-detect/android/src/main/java/expo/modules/downloaddetect/MediaStoreObserver.kt b/mobile/modules/expo-download-detect/android/src/main/java/expo/modules/downloaddetect/MediaStoreObserver.kt
deleted file mode 100644
index 0720700..0000000
--- a/mobile/modules/expo-download-detect/android/src/main/java/expo/modules/downloaddetect/MediaStoreObserver.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-package expo.modules.downloaddetect
-
-import android.content.ContentResolver
-import android.content.Context
-import android.database.ContentObserver
-import android.database.Cursor
-import android.net.Uri
-import android.os.Handler
-import android.provider.MediaStore
-
-class MediaStoreObserver(
- handler: Handler,
- private val context: Context,
- private val onNewFiles: (List) -> Unit
-) : ContentObserver(handler) {
-
- private var lastScanTimestamp: Long = System.currentTimeMillis() / 1000L
-
- override fun onChange(selfChange: Boolean, uri: Uri?) {
- super.onChange(selfChange, uri)
- queryNewFiles()
- }
-
- override fun onChange(selfChange: Boolean, uri: Uri?, flags: Int) {
- super.onChange(selfChange, uri, flags)
- queryNewFiles()
- }
-
- private fun queryNewFiles() {
- val files = mutableListOf()
- val projection = arrayOf(
- MediaStore.Files.FileColumns._ID,
- MediaStore.Files.FileColumns.DISPLAY_NAME,
- MediaStore.Files.FileColumns.MIME_TYPE,
- MediaStore.Files.FileColumns.SIZE,
- MediaStore.Files.FileColumns.DATE_ADDED
- )
-
- val selection = "${MediaStore.Files.FileColumns.DATE_ADDED} > ? AND ${MediaStore.Files.FileColumns.SIZE} > 0"
- val selectionArgs = arrayOf(lastScanTimestamp.toString())
- val sortOrder = "${MediaStore.Files.FileColumns.DATE_ADDED} DESC"
-
- val resolver: ContentResolver = context.contentResolver
- var cursor: Cursor? = null
-
- try {
- // Try MediaStore.Downloads first
- cursor = try {
- resolver.query(
- MediaStore.Downloads.getContentUri("external"),
- projection, selection, selectionArgs, sortOrder
- )
- } catch (_: Exception) {
- null
- }
-
- // Fallback to MediaStore.Files if Downloads didn't work
- if (cursor == null || cursor.count == 0) {
- cursor?.close()
- cursor = resolver.query(
- MediaStore.Files.getContentUri("external"),
- projection, selection, selectionArgs, sortOrder
- )
- }
-
- cursor?.use { c ->
- val idCol = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
- val nameCol = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
- val mimeCol = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE)
- val sizeCol = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE)
- val dateCol = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_ADDED)
-
- while (c.moveToNext()) {
- val id = c.getLong(idCol)
- val name = c.getString(nameCol) ?: continue
- val mimeType = c.getString(mimeCol) ?: "application/octet-stream"
- val size = c.getLong(sizeCol)
- val dateAdded = c.getLong(dateCol)
-
- val fileUri = Uri.withAppendedPath(
- MediaStore.Files.getContentUri("external"),
- id.toString()
- ).toString()
-
- files.add(DetectedFile(
- id = "media_$id",
- uri = fileUri,
- name = name,
- mimeType = mimeType,
- size = size,
- createdAt = dateAdded * 1000L
- ))
- }
- }
- } catch (_: Exception) {
- } finally {
- cursor?.close()
- }
-
- if (files.isNotEmpty()) {
- lastScanTimestamp = System.currentTimeMillis() / 1000L
- onNewFiles(files)
- }
- }
-}
diff --git a/mobile/modules/expo-download-detect/expo-module.config.json b/mobile/modules/expo-download-detect/expo-module.config.json
deleted file mode 100644
index 8e019d1..0000000
--- a/mobile/modules/expo-download-detect/expo-module.config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "platforms": ["android"],
- "android": {
- "modules": ["expo.modules.downloaddetect.ExpoDownloadDetectModule"]
- }
-}
diff --git a/mobile/modules/expo-download-detect/index.ts b/mobile/modules/expo-download-detect/index.ts
deleted file mode 100644
index 9a08708..0000000
--- a/mobile/modules/expo-download-detect/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { ExpoDownloadDetectModule } from './src';
-export type { FileDetectedEvent } from './src';
diff --git a/mobile/modules/expo-download-detect/package.json b/mobile/modules/expo-download-detect/package.json
deleted file mode 100644
index 076b8c0..0000000
--- a/mobile/modules/expo-download-detect/package.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "expo-download-detect",
- "version": "0.1.0",
- "description": "Detect new files via DownloadManager and MediaStore ContentObserver",
- "main": "index.ts",
- "android": {
- "sourceDir": "android"
- },
- "peerDependencies": {
- "expo": ">=57.0.0"
- }
-}
diff --git a/mobile/modules/expo-download-detect/src/ExpoDownloadDetect.types.ts b/mobile/modules/expo-download-detect/src/ExpoDownloadDetect.types.ts
deleted file mode 100644
index 950bd0f..0000000
--- a/mobile/modules/expo-download-detect/src/ExpoDownloadDetect.types.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export type FileDetectedEvent = {
- id: string;
- uri: string;
- name: string;
- mimeType: string;
- size: number;
- createdAt: number;
- source: 'download' | 'mediastore' | 'startup';
-};
-
-export type ExpoDownloadDetectModuleEvents = {
- onNewFile: (event: FileDetectedEvent) => void;
-};
diff --git a/mobile/modules/expo-download-detect/src/ExpoDownloadDetectModule.ts b/mobile/modules/expo-download-detect/src/ExpoDownloadDetectModule.ts
deleted file mode 100644
index 6d5ffb3..0000000
--- a/mobile/modules/expo-download-detect/src/ExpoDownloadDetectModule.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { NativeModule, requireNativeModule } from 'expo';
-import { ExpoDownloadDetectModuleEvents, FileDetectedEvent } from './ExpoDownloadDetect.types';
-
-declare class ExpoDownloadDetectModule extends NativeModule {
- startWatching(): void;
- stopWatching(): void;
- getRecentDownloads(): Promise;
-}
-
-export default requireNativeModule('ExpoDownloadDetect');
diff --git a/mobile/modules/expo-download-detect/src/index.ts b/mobile/modules/expo-download-detect/src/index.ts
deleted file mode 100644
index 09c04f6..0000000
--- a/mobile/modules/expo-download-detect/src/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as ExpoDownloadDetectModule } from './ExpoDownloadDetectModule';
-export type { FileDetectedEvent, ExpoDownloadDetectModuleEvents } from './ExpoDownloadDetect.types';
diff --git a/mobile/package-lock.json b/mobile/package-lock.json
index cd75448..208db38 100644
--- a/mobile/package-lock.json
+++ b/mobile/package-lock.json
@@ -1,51 +1,21 @@
{
"name": "webui",
- "version": "1.0.0",
+ "version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "webui",
- "version": "1.0.0",
+ "version": "2.0.0",
"dependencies": {
- "@expo/vector-icons": "^15.0.2",
- "@react-native-community/netinfo": "12.0.1",
- "@react-navigation/native": "^7.3.8",
- "@react-navigation/native-stack": "^7.17.10",
- "@tanstack/query-async-storage-persister": "^5.101.4",
- "@tanstack/react-query": "^5.101.2",
- "@tanstack/react-query-persist-client": "^5.101.4",
- "babel-preset-expo": "^57.0.4",
- "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",
- "expo-secure-store": "~57.0.1",
- "expo-sqlite": "~57.0.1",
"expo-status-bar": "~57.0.1",
- "expo-task-manager": "~57.0.6",
"react": "19.2.3",
- "react-native": "0.86.0",
- "react-native-gesture-handler": "~2.32.0",
- "react-native-mmkv": "^4.3.2",
- "react-native-nitro-image": "^0.15.1",
- "react-native-nitro-modules": "^0.36.1",
- "react-native-reanimated": "4.5.0",
- "react-native-safe-area-context": "~5.7.0",
- "react-native-screens": "~4.26.0",
- "react-native-vision-camera": "^5.1.0",
- "react-native-worklets": "0.10.0"
+ "react-native": "0.86.0"
},
"devDependencies": {
- "@expo/metro-config": "^57.0.7",
"@types/react": "~19.2.2",
- "drizzle-kit": "^0.31.10",
+ "babel-preset-expo": "^57.0.4",
"typescript": "~6.0.3"
}
},
@@ -102,23 +72,14 @@
"url": "https://opencollective.com/babel"
}
},
- "node_modules/@babel/core/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@babel/generator": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
- "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+ "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7",
+ "@babel/parser": "^7.29.8",
+ "@babel/types": "^7.29.8",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -155,15 +116,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz",
@@ -185,15 +137,6 @@
"@babel/core": "^7.0.0"
}
},
- "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@babel/helper-create-regexp-features-plugin": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz",
@@ -211,15 +154,6 @@
"@babel/core": "^7.0.0"
}
},
- "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.6.8",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz",
@@ -411,12 +345,12 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
- "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+ "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.7"
+ "@babel/types": "^7.29.8"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -568,21 +502,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-arrow-functions": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz",
- "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-transform-async-generator-functions": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz",
@@ -956,38 +875,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-react-jsx-self": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
- "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-source": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
- "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-transform-react-pure-annotations": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz",
@@ -1004,22 +891,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-regenerator": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz",
- "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-transform-runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz",
@@ -1040,45 +911,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-runtime/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/plugin-transform-shorthand-properties": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz",
- "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-template-literals": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz",
- "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-transform-typescript": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz",
@@ -1157,17 +989,17 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
- "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
+ "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.7",
+ "@babel/generator": "^7.29.8",
"@babel/helper-globals": "^7.29.7",
- "@babel/parser": "^7.29.7",
+ "@babel/parser": "^7.29.8",
"@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7",
+ "@babel/types": "^7.29.8",
"debug": "^4.3.1"
},
"engines": {
@@ -1175,9 +1007,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
- "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.29.7",
@@ -1187,903 +1019,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@drizzle-team/brocli": {
- "version": "0.10.2",
- "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
- "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/@egjs/hammerjs": {
- "version": "2.0.17",
- "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
- "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
- "license": "MIT",
- "dependencies": {
- "@types/hammerjs": "^2.0.36"
- },
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/@esbuild-kit/core-utils": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
- "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
- "deprecated": "Merged into tsx: https://tsx.hirok.io",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "~0.18.20",
- "source-map-support": "^0.5.21"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
- "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
- "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
- "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
- "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
- "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
- "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
- "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
- "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
- "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
- "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
- "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
- "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
- "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
- "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
- "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
- "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
- "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
- "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
- "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
- "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
- "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
- "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
- "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=12"
- },
- "optionalDependencies": {
- "@esbuild/android-arm": "0.18.20",
- "@esbuild/android-arm64": "0.18.20",
- "@esbuild/android-x64": "0.18.20",
- "@esbuild/darwin-arm64": "0.18.20",
- "@esbuild/darwin-x64": "0.18.20",
- "@esbuild/freebsd-arm64": "0.18.20",
- "@esbuild/freebsd-x64": "0.18.20",
- "@esbuild/linux-arm": "0.18.20",
- "@esbuild/linux-arm64": "0.18.20",
- "@esbuild/linux-ia32": "0.18.20",
- "@esbuild/linux-loong64": "0.18.20",
- "@esbuild/linux-mips64el": "0.18.20",
- "@esbuild/linux-ppc64": "0.18.20",
- "@esbuild/linux-riscv64": "0.18.20",
- "@esbuild/linux-s390x": "0.18.20",
- "@esbuild/linux-x64": "0.18.20",
- "@esbuild/netbsd-x64": "0.18.20",
- "@esbuild/openbsd-x64": "0.18.20",
- "@esbuild/sunos-x64": "0.18.20",
- "@esbuild/win32-arm64": "0.18.20",
- "@esbuild/win32-ia32": "0.18.20",
- "@esbuild/win32-x64": "0.18.20"
- }
- },
- "node_modules/@esbuild-kit/esm-loader": {
- "version": "2.6.5",
- "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
- "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
- "deprecated": "Merged into tsx: https://tsx.hirok.io",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@esbuild-kit/core-utils": "^3.3.2",
- "get-tsconfig": "^4.7.0"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
- "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/@expo/code-signing-certificates": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz",
@@ -2094,15 +1029,15 @@
}
},
"node_modules/@expo/config": {
- "version": "57.0.6",
- "resolved": "https://registry.npmjs.org/@expo/config/-/config-57.0.6.tgz",
- "integrity": "sha512-VpMJpB/De/fb9bBFVVBiK6Ntg9lt0kAleLH9hcZz85CYRUQ3jVFVA8rNC5f8y4cp2+FiiPNFp62+kEOFI6pDiw==",
+ "version": "57.0.9",
+ "resolved": "https://registry.npmjs.org/@expo/config/-/config-57.0.9.tgz",
+ "integrity": "sha512-dmzlKraIFxa7wLwV6K7WzI8jp6QZpW6Mc5mGjLimJUFjzh4uQdYaT3m3plEutM5yxBoBEwqzks7l+I/ljCbxAQ==",
"license": "MIT",
"dependencies": {
- "@expo/config-plugins": "~57.0.6",
+ "@expo/config-plugins": "~57.0.9",
"@expo/config-types": "^57.0.2",
"@expo/json-file": "^11.0.1",
- "@expo/require-utils": "^57.0.4",
+ "@expo/require-utils": "^57.0.5",
"deepmerge": "^4.3.1",
"getenv": "^2.0.0",
"glob": "^13.0.0",
@@ -2112,15 +1047,15 @@
}
},
"node_modules/@expo/config-plugins": {
- "version": "57.0.6",
- "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-57.0.6.tgz",
- "integrity": "sha512-7CmKrS5Rnu8aSZyNlxH2qzA7Ls1HEa4EQvEVOAkHDKPr1e4Cg/nz7I7dUl09QDTVjMvkKYDH4Th0DuAsgqASaw==",
+ "version": "57.0.9",
+ "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-57.0.9.tgz",
+ "integrity": "sha512-hHgfL1avkCdEvDSw7IwlKwRYYNgcxzbNNMIk6W6lTkJpY0MajinAfeJUS0J+wPCsjUfGbVqOJM+XhPaO5ulUxg==",
"license": "MIT",
"dependencies": {
"@expo/config-types": "^57.0.2",
"@expo/json-file": "~11.0.1",
"@expo/plist": "^0.8.1",
- "@expo/require-utils": "^57.0.4",
+ "@expo/require-utils": "^57.0.5",
"@expo/sdk-runtime-versions": "^1.0.0",
"chalk": "^4.1.2",
"debug": "^4.3.5",
@@ -2132,12 +1067,36 @@
"xml2js": "0.6.0"
}
},
+ "node_modules/@expo/config-plugins/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@expo/config-types": {
"version": "57.0.2",
"resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-57.0.2.tgz",
"integrity": "sha512-ewW08OonrcRIsRKIlFvvcmmafE5zemb1ocu3HkNwtVPyRtj2w42pZCAkMIROYpcVBaPnc3mDT9UZDzwXWC3i6g==",
"license": "MIT"
},
+ "node_modules/@expo/config/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@expo/devcert": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.1.tgz",
@@ -2179,9 +1138,9 @@
}
},
"node_modules/@expo/env": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.4.2.tgz",
- "integrity": "sha512-28pqaEqwnmLduZ00Pq9HkSzE5wbj1MTwp5/n8nm8rD8MCjR9eUnVOwmNksPI3Be2ReAPO/DbPn1puy0mvoocsQ==",
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.4.3.tgz",
+ "integrity": "sha512-M1NXeZCA1mkMkYOyIe7PlyRX0/jqFtMoJgyblnlq/vpCRfmueFT7RnGSQG8uEFDF5WHOFGijAQ3fogPh3/n5Ng==",
"license": "MIT",
"dependencies": {
"chalk": "^4.0.0",
@@ -2199,12 +1158,12 @@
"license": "MIT"
},
"node_modules/@expo/fingerprint": {
- "version": "0.20.6",
- "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.20.6.tgz",
- "integrity": "sha512-cmC/6BOPRbdKr77Mgjwszb8aM0hY2RKBpMRCmjSdn9zIcn2FGor/ic4fHVr46cQFa1G6RDGg1GyAjRw3US4CCQ==",
+ "version": "0.20.12",
+ "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.20.12.tgz",
+ "integrity": "sha512-FIR5fkZYeFaLSowmjgyB6RPKl8AXeE8HuCBHHvyxK8UhOjpPfCAmzT4E7v2yub4qqDafKnfcOFCYxvpUEu+01w==",
"license": "MIT",
"dependencies": {
- "@expo/env": "^2.4.2",
+ "@expo/env": "^2.4.3",
"@expo/spawn-async": "^1.8.0",
"arg": "^5.0.2",
"chalk": "^4.1.2",
@@ -2220,13 +1179,25 @@
"fingerprint": "bin/cli.js"
}
},
+ "node_modules/@expo/fingerprint/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@expo/image-utils": {
- "version": "0.11.4",
- "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.11.4.tgz",
- "integrity": "sha512-pn/4770DIEOcYZr484uazuwg20FX/qaDkeMRF6J+oxejynDmEmO8wLsCudaNShFE0BhyKGQTYrs2rsRhqrqESw==",
+ "version": "0.11.5",
+ "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.11.5.tgz",
+ "integrity": "sha512-KPQBTpmpAfy/Vu9y4wPW808/qtZxjYmyJg8cm2QCPAupp+qEWA3b5zmk0ulOwQ9OgeHxuCPgUqWgkwHFo7UsrQ==",
"license": "MIT",
"dependencies": {
- "@expo/require-utils": "^57.0.4",
+ "@expo/require-utils": "^57.0.5",
"@expo/spawn-async": "^1.8.0",
"chalk": "^4.0.0",
"getenv": "^2.0.0",
@@ -2235,13 +1206,25 @@
"semver": "^7.6.0"
}
},
+ "node_modules/@expo/image-utils/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@expo/inline-modules": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@expo/inline-modules/-/inline-modules-0.1.3.tgz",
- "integrity": "sha512-eHSxWYfgq65mP3Qz8PclVjUkSrDIlGl3va9U7PMcTpGItOvee/i0ZzGinH5A25oARR5ouD64eESBKwtT/CwdHg==",
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/@expo/inline-modules/-/inline-modules-0.1.7.tgz",
+ "integrity": "sha512-Bz/khd1gIJqDkje7t5ejD5e9jFbm4xEJzWSwRKadRo6gruepbw1xJ3Eb+e58OS8fY1ZNQYjzZbspnuph67sN0g==",
"license": "MIT",
"dependencies": {
- "@expo/config-plugins": "~57.0.5"
+ "@expo/config-plugins": "~57.0.9"
}
},
"node_modules/@expo/json-file": {
@@ -2255,107 +1238,41 @@
}
},
"node_modules/@expo/local-build-cache-provider": {
- "version": "57.0.4",
- "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-57.0.4.tgz",
- "integrity": "sha512-B/cI73shkLSYBYuFyh+zCbS+WhqJgawWPW4MPdMiNLJKv9RmV4dv1FGjsidiIiG2k4kYKerBDLK4bLbC7qERQQ==",
+ "version": "57.0.8",
+ "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-57.0.8.tgz",
+ "integrity": "sha512-SEdE0pAQrr90bRh3MNR0ZuwoIBw390cYdFgbn7Vk0Mtm9EHaBfP7kYj+2hXnXZJgOEm3/JMtfoD3rV7rWA9FGg==",
"license": "MIT",
"dependencies": {
- "@expo/config": "~57.0.5",
+ "@expo/config": "~57.0.9",
"chalk": "^4.1.2"
}
},
"node_modules/@expo/metro": {
- "version": "56.0.0",
- "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-56.0.0.tgz",
- "integrity": "sha512-5gIgQHtEpjjvsjKfVtIv23a98LLRV0/y07PDShEwYSytAMlE3FSF8RHXqtHc1sUJL6dn7hnuIBpIbrLXXuVi0A==",
+ "version": "56.0.2",
+ "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-56.0.2.tgz",
+ "integrity": "sha512-Ld5AeYMCCDa8bLeWhfuLbZFFjlV3f6ORqyPz2glGh6RltIngMuLf9BTC2yvHFjkKuGxL5SynijmA8xmNNWn5iA==",
"license": "MIT",
"dependencies": {
- "metro": "0.84.4",
- "metro-babel-transformer": "0.84.4",
- "metro-cache": "0.84.4",
- "metro-cache-key": "0.84.4",
- "metro-config": "0.84.4",
- "metro-core": "0.84.4",
- "metro-file-map": "0.84.4",
- "metro-minify-terser": "0.84.4",
- "metro-resolver": "0.84.4",
- "metro-runtime": "0.84.4",
- "metro-source-map": "0.84.4",
- "metro-symbolicate": "0.84.4",
- "metro-transform-plugins": "0.84.4",
- "metro-transform-worker": "0.84.4"
- }
- },
- "node_modules/@expo/metro-config": {
- "version": "57.0.7",
- "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-57.0.7.tgz",
- "integrity": "sha512-bVfEkg4zF1cA62OqAdYXmFOooJ6TB/I+REi7Se6Ct+PbSC+89TwSqWXnYx34L08eIs4z+1ilgbATakTZpgefmQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.20.0",
- "@babel/core": "^7.20.0",
- "@babel/generator": "^7.20.5",
- "@expo/config": "~57.0.6",
- "@expo/env": "~2.4.2",
- "@expo/json-file": "~11.0.1",
- "@expo/metro": "~56.0.0",
- "@expo/require-utils": "^57.0.4",
- "@expo/spawn-async": "^1.8.0",
- "@jridgewell/gen-mapping": "^0.3.13",
- "@jridgewell/remapping": "^2.3.5",
- "@jridgewell/sourcemap-codec": "^1.5.5",
- "browserslist": "^4.25.0",
- "chalk": "^4.1.0",
- "debug": "^4.3.2",
- "getenv": "^2.0.0",
- "glob": "^13.0.0",
- "hermes-parser": "^0.36.0",
- "jsc-safe-url": "^0.2.4",
- "lightningcss": "^1.30.1",
- "picomatch": "^4.0.4",
- "postcss": "^8.5.14",
- "resolve-from": "^5.0.0"
- },
- "peerDependencies": {
- "expo": "*"
- },
- "peerDependenciesMeta": {
- "expo": {
- "optional": true
- }
- }
- },
- "node_modules/@expo/metro-config/node_modules/hermes-estree": {
- "version": "0.36.1",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.1.tgz",
- "integrity": "sha512-guv1nQ6IJ7S83NRFPWc3SA7IBZrdNC9kapwOq6uXvF4wP+sDCgjzQbKPCoyYmoyZRzztF/n/c36l/rccCZSiCw==",
- "license": "MIT"
- },
- "node_modules/@expo/metro-config/node_modules/hermes-parser": {
- "version": "0.36.1",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.1.tgz",
- "integrity": "sha512-GApNk4zLHi2UWoWZZkx7LNCOSzLSc5lB55pZ/PhK7ycFeg7u5LcF88p/WbpIi1XUDtE0MpHE3uRR3u3KB7TjSQ==",
- "license": "MIT",
- "dependencies": {
- "hermes-estree": "0.36.1"
- }
- },
- "node_modules/@expo/metro-config/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
+ "metro": "0.84.5",
+ "metro-babel-transformer": "0.84.5",
+ "metro-cache": "0.84.5",
+ "metro-cache-key": "0.84.5",
+ "metro-config": "0.84.5",
+ "metro-core": "0.84.5",
+ "metro-file-map": "0.84.5",
+ "metro-minify-terser": "0.84.5",
+ "metro-resolver": "0.84.5",
+ "metro-runtime": "0.84.5",
+ "metro-source-map": "0.84.5",
+ "metro-symbolicate": "0.84.5",
+ "metro-transform-plugins": "0.84.5",
+ "metro-transform-worker": "0.84.5"
}
},
"node_modules/@expo/metro-file-map": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/@expo/metro-file-map/-/metro-file-map-57.0.1.tgz",
- "integrity": "sha512-8JXfVstZN7QnP4NianZZnlTVboOWR0sG8trUDNajOjnbGlPln29vponXM84tY+3tAHapz5/TxE53L0ixUwqPtA==",
+ "version": "57.0.3",
+ "resolved": "https://registry.npmjs.org/@expo/metro-file-map/-/metro-file-map-57.0.3.tgz",
+ "integrity": "sha512-1OXy+uPYY5uc7Tm4VBsd2NRn+3wHhqeqNuEO/Xo4kmYgv8FjYgUAc+bUXON9FpC2ikcLn4EVlGM9ce2exx9Mlg==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.4",
@@ -2404,27 +1321,45 @@
}
},
"node_modules/@expo/prebuild-config": {
- "version": "57.0.9",
- "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-57.0.9.tgz",
- "integrity": "sha512-8g7RoXFvO/dxvLzRE/bvphzDL4bfV0w3/4Aj6DfwvgymZ1ULz5gW2x0js94opZRoZvWw4SolH6/74hLlZT3rAA==",
+ "version": "57.0.15",
+ "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-57.0.15.tgz",
+ "integrity": "sha512-xTbWHroj0PDmlbqvmU+zF9ZZxveJkiuyiPoeRJYRGruFHebRAWnoTdw5S7d/UCzDBI8ropGGu9g2eb2nMxtvAw==",
"license": "MIT",
"dependencies": {
- "@expo/config": "~57.0.6",
- "@expo/config-plugins": "~57.0.6",
+ "@expo/config": "~57.0.9",
+ "@expo/config-plugins": "~57.0.9",
"@expo/config-types": "^57.0.2",
- "@expo/image-utils": "^0.11.4",
+ "@expo/image-utils": "^0.11.5",
"@expo/json-file": "^11.0.1",
- "@react-native/normalize-colors": "0.86.0",
+ "@react-native/normalize-colors": "0.86.3",
"debug": "^4.3.1",
- "expo-modules-autolinking": "~57.0.9",
+ "expo-modules-autolinking": "~57.0.12",
"resolve-from": "^5.0.0",
"semver": "^7.6.0"
}
},
+ "node_modules/@expo/prebuild-config/node_modules/@react-native/normalize-colors": {
+ "version": "0.86.3",
+ "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.3.tgz",
+ "integrity": "sha512-Cv3CDkprb67GrzuaS9BGbBJC/6G4lIw3nyKOHRKTqTTum4bn37y5+R0Z04L8mcbQN85eEohNrRwb7IOM4j6uvg==",
+ "license": "MIT"
+ },
+ "node_modules/@expo/prebuild-config/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@expo/require-utils": {
- "version": "57.0.4",
- "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-57.0.4.tgz",
- "integrity": "sha512-e7xbg/9BTQcsZE/oErafZXtI7kh5IgfasLJ97J5sFSzX2cA74pDvdlhW1KHVSaDkQyQv6h1LSLhsY7dEeOk7hw==",
+ "version": "57.0.5",
+ "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-57.0.5.tgz",
+ "integrity": "sha512-kTAXj9lDFEIPMsbAOGCGbjBbMF0oi7CqkYM79KOX0DDD9wSwXmlKL1z2h8OwsrBf7mbOo2DjlRvZu4BEjrIxGw==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.20.0",
@@ -2470,21 +1405,10 @@
"integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==",
"license": "MIT"
},
- "node_modules/@expo/vector-icons": {
- "version": "15.1.1",
- "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz",
- "integrity": "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==",
- "license": "MIT",
- "peerDependencies": {
- "expo-font": ">=14.0.4",
- "react": "*",
- "react-native": "*"
- }
- },
"node_modules/@expo/xcpretty": {
- "version": "4.4.4",
- "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.4.tgz",
- "integrity": "sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw==",
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.5.tgz",
+ "integrity": "sha512-J3eL4n4h5QTwfD0SIz8OIk6/+sOL/hFZAMacgCM07UNlxBQfJipEpIC2AQxvGkbYeStByJ0TVhQAJo+DeNgaSQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/code-frame": "^7.20.0",
@@ -2573,9 +1497,9 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
@@ -2588,16 +1512,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
- "node_modules/@react-native-community/netinfo": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-12.0.1.tgz",
- "integrity": "sha512-P/3caXIvfYSJG8AWJVefukg+ZGRPs+M4Lp3pNJtgcTYoJxCjWrKQGNnCkj/Cz//zWa/avGed0i/wzm0T8vV2IQ==",
- "license": "MIT",
- "peerDependencies": {
- "react": "*",
- "react-native": ">=0.59"
- }
- },
"node_modules/@react-native/assets-registry": {
"version": "0.86.0",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.86.0.tgz",
@@ -2608,70 +1522,22 @@
}
},
"node_modules/@react-native/babel-plugin-codegen": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.0.tgz",
- "integrity": "sha512-qdsABWNW7uTll90l4Vh03gjeyu3WVDi2CyiiyvYGMRDcoYbjbQi6df3BMAm9lQI2yslZ1T14LlDDAsgTwNxplA==",
+ "version": "0.86.3",
+ "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.3.tgz",
+ "integrity": "sha512-O6Xza4JBGPIU8J7YbKTyBoYL4thpy8jMW/oaLDWdAyOwYHKIjK47pAL5HUEbOe2bWz2PEKjbYRF2ApkJv1ottQ==",
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.29.0",
- "@react-native/codegen": "0.86.0"
+ "@react-native/codegen": "0.86.3"
},
"engines": {
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
}
},
- "node_modules/@react-native/babel-preset": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.86.0.tgz",
- "integrity": "sha512-bYQcWiPySNvF4dns9Ls9gMmwgq66ohvM9Fwc/Kn8r85t66UNHxch3p1QwPiSorDelFauZwJbgo9+ReibTgvpbA==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/core": "^7.25.2",
- "@babel/plugin-proposal-export-default-from": "^7.24.7",
- "@babel/plugin-syntax-dynamic-import": "^7.8.3",
- "@babel/plugin-syntax-export-default-from": "^7.24.7",
- "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
- "@babel/plugin-syntax-optional-chaining": "^7.8.3",
- "@babel/plugin-transform-async-generator-functions": "^7.25.4",
- "@babel/plugin-transform-async-to-generator": "^7.24.7",
- "@babel/plugin-transform-block-scoping": "^7.25.0",
- "@babel/plugin-transform-class-properties": "^7.25.4",
- "@babel/plugin-transform-classes": "^7.25.4",
- "@babel/plugin-transform-destructuring": "^7.24.8",
- "@babel/plugin-transform-flow-strip-types": "^7.25.2",
- "@babel/plugin-transform-for-of": "^7.24.7",
- "@babel/plugin-transform-modules-commonjs": "^7.24.8",
- "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7",
- "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
- "@babel/plugin-transform-optional-catch-binding": "^7.24.7",
- "@babel/plugin-transform-optional-chaining": "^7.24.8",
- "@babel/plugin-transform-private-methods": "^7.24.7",
- "@babel/plugin-transform-private-property-in-object": "^7.24.7",
- "@babel/plugin-transform-react-display-name": "^7.24.7",
- "@babel/plugin-transform-react-jsx": "^7.25.2",
- "@babel/plugin-transform-react-jsx-self": "^7.24.7",
- "@babel/plugin-transform-react-jsx-source": "^7.24.7",
- "@babel/plugin-transform-regenerator": "^7.24.7",
- "@babel/plugin-transform-runtime": "^7.24.7",
- "@babel/plugin-transform-typescript": "^7.25.2",
- "@babel/plugin-transform-unicode-regex": "^7.24.7",
- "@react-native/babel-plugin-codegen": "0.86.0",
- "babel-plugin-syntax-hermes-parser": "0.36.0",
- "babel-plugin-transform-flow-enums": "^0.0.2",
- "react-refresh": "^0.14.0"
- },
- "engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
- },
- "peerDependencies": {
- "@babel/core": "*"
- }
- },
"node_modules/@react-native/codegen": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.0.tgz",
- "integrity": "sha512-uTs9DBo3+/lUqinsGZK0FKJRBVClrwMXoZToaDxE1Q2SL2e55vs2GwyZfIKzPl5uJnbu4PfFMIp0/mLXLWUMuA==",
+ "version": "0.86.3",
+ "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.3.tgz",
+ "integrity": "sha512-Ux4jHi0fh+bdtVEcL0gaPLbY56V+SvFUDl/8sRAE1jdb4k+o7fT/4Nc29yz4X+qfjstkSqObQTMBGhdzxH9JvA==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
@@ -2689,21 +1555,6 @@
"@babel/core": "*"
}
},
- "node_modules/@react-native/codegen/node_modules/hermes-estree": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz",
- "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==",
- "license": "MIT"
- },
- "node_modules/@react-native/codegen/node_modules/hermes-parser": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz",
- "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==",
- "license": "MIT",
- "dependencies": {
- "hermes-estree": "0.36.0"
- }
- },
"node_modules/@react-native/community-cli-plugin": {
"version": "0.86.0",
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.86.0.tgz",
@@ -2734,6 +1585,18 @@
}
}
},
+ "node_modules/@react-native/community-cli-plugin/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@react-native/debugger-frontend": {
"version": "0.86.0",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.86.0.tgz",
@@ -2798,58 +1661,6 @@
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
}
},
- "node_modules/@react-native/metro-babel-transformer": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.0.tgz",
- "integrity": "sha512-SjKej3E5qIahqo/G+rSOrmJUQM44RyKtWtO+VfmKAAMoJWkBFomM22hTLKCIS5cdbIAJ9COAmU+KAi2wVSO0wQ==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/core": "^7.25.2",
- "@react-native/babel-preset": "0.86.0",
- "hermes-parser": "0.36.0",
- "nullthrows": "^1.1.1"
- },
- "engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
- },
- "peerDependencies": {
- "@babel/core": "*"
- }
- },
- "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-estree": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz",
- "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-parser": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz",
- "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "hermes-estree": "0.36.0"
- }
- },
- "node_modules/@react-native/metro-config": {
- "version": "0.86.0",
- "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.0.tgz",
- "integrity": "sha512-7v+xbTeEci9ZcQ/Z1OqI4RXcqN69wSMDYL5BAMvOReZ7U04+aDQ0/SQhClYPn6x2/RxM4WzMKSAuNyLKqvYVtw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@react-native/js-polyfills": "0.86.0",
- "@react-native/metro-babel-transformer": "0.86.0",
- "metro-config": "^0.84.3",
- "metro-runtime": "^0.84.3"
- },
- "engines": {
- "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
- }
- },
"node_modules/@react-native/normalize-colors": {
"version": "0.86.0",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.0.tgz",
@@ -2879,180 +1690,10 @@
}
}
},
- "node_modules/@react-navigation/core": {
- "version": "7.21.5",
- "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.21.5.tgz",
- "integrity": "sha512-3hpV7uR41LBW+GHDoLhztZCb/i5ySRJISZ/rez4d7DCHSZo6ej4gNxYclaS6LRguoLiKG7SOCNa6O390AQklZQ==",
- "license": "MIT",
- "dependencies": {
- "@react-navigation/routers": "^7.6.0",
- "escape-string-regexp": "^4.0.0",
- "fast-deep-equal": "^3.1.3",
- "nanoid": "^3.3.11",
- "query-string": "^7.1.3",
- "react-is": "^19.1.0",
- "use-latest-callback": "^0.2.4",
- "use-sync-external-store": "^1.5.0"
- },
- "peerDependencies": {
- "react": ">= 18.2.0"
- }
- },
- "node_modules/@react-navigation/core/node_modules/react-is": {
- "version": "19.2.7",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz",
- "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==",
- "license": "MIT"
- },
- "node_modules/@react-navigation/elements": {
- "version": "2.9.30",
- "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.30.tgz",
- "integrity": "sha512-2isleieiRMmP4WNMV2Q1u3qP1M47ZqsJ2hJ/Og11FeKXK8YmUTHya7PW7ecsgAh2CXKTxAcbfJDFTSvq2D++Tw==",
- "license": "MIT",
- "dependencies": {
- "color": "^4.2.3",
- "use-latest-callback": "^0.2.4",
- "use-sync-external-store": "^1.5.0"
- },
- "peerDependencies": {
- "@react-native-masked-view/masked-view": ">= 0.2.0",
- "@react-navigation/native": "^7.3.8",
- "react": ">= 18.2.0",
- "react-native": "*",
- "react-native-safe-area-context": ">= 4.0.0"
- },
- "peerDependenciesMeta": {
- "@react-native-masked-view/masked-view": {
- "optional": true
- }
- }
- },
- "node_modules/@react-navigation/native": {
- "version": "7.3.8",
- "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.3.8.tgz",
- "integrity": "sha512-zHmQcxWBT8GOwsofEOmHqpdM5twkwE/esa9JFGlW4hpXeQTTe/dRcPSLjwsvePtolbDKz0YZbK+I5KrW3j63LQ==",
- "license": "MIT",
- "dependencies": {
- "@react-navigation/core": "^7.21.5",
- "escape-string-regexp": "^4.0.0",
- "fast-deep-equal": "^3.1.3",
- "nanoid": "^3.3.11",
- "standard-navigation": "^0.0.7",
- "use-latest-callback": "^0.2.4"
- },
- "peerDependencies": {
- "react": ">= 18.2.0",
- "react-native": "*"
- }
- },
- "node_modules/@react-navigation/native-stack": {
- "version": "7.17.10",
- "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.17.10.tgz",
- "integrity": "sha512-m1BWVEaOPX9k30DbmhsD7IlrUdl4J7Ogmo71rcNlbZQPMNB126av/nfwqB+qN7DIsiwTAnORnT+SwzD56qqD0Q==",
- "license": "MIT",
- "dependencies": {
- "@react-navigation/elements": "^2.9.30",
- "color": "^4.2.3",
- "sf-symbols-typescript": "^2.1.0",
- "warn-once": "^0.1.1"
- },
- "peerDependencies": {
- "@react-navigation/native": "^7.3.8",
- "react": ">= 18.2.0",
- "react-native": "*",
- "react-native-safe-area-context": ">= 4.0.0",
- "react-native-screens": ">= 4.0.0"
- }
- },
- "node_modules/@react-navigation/routers": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.6.0.tgz",
- "integrity": "sha512-lblhDXfS75jLc7G2K7BZGM+7cjqQXk13X/MA4fq/12r62zM+fBhhreLzYflSitrDDXFRJpSvJXy0ziiGU04Xow==",
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.11"
- }
- },
"node_modules/@sinclair/typebox": {
- "version": "0.27.10",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
- "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==",
- "license": "MIT"
- },
- "node_modules/@tanstack/query-async-storage-persister": {
- "version": "5.101.4",
- "resolved": "https://registry.npmjs.org/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.101.4.tgz",
- "integrity": "sha512-ROenNOVvxIZ1zKsBAiIvvslLo2xD8Me/vFz/tChdmuI7ciU+ET63yRrQ01W899Tx1ayQXdsGQJqYqjjVZk+OBQ==",
- "license": "MIT",
- "dependencies": {
- "@tanstack/query-core": "5.101.4",
- "@tanstack/query-persist-client-core": "5.101.4"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@tanstack/query-core": {
- "version": "5.101.4",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz",
- "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@tanstack/query-persist-client-core": {
- "version": "5.101.4",
- "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.101.4.tgz",
- "integrity": "sha512-Bmu+RfWhwYEyYZEwSeKC0gX4+KTkiEB3yO8oz9BfAY/Jfe3ndz9EnYVhihIZPmWIr8NXayF7JuraNQcrXQQCCg==",
- "license": "MIT",
- "dependencies": {
- "@tanstack/query-core": "5.101.4"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@tanstack/react-query": {
- "version": "5.101.4",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz",
- "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==",
- "license": "MIT",
- "dependencies": {
- "@tanstack/query-core": "5.101.4"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- },
- "peerDependencies": {
- "react": "^18 || ^19"
- }
- },
- "node_modules/@tanstack/react-query-persist-client": {
- "version": "5.101.4",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.101.4.tgz",
- "integrity": "sha512-CY5i7PPKS/5B8OR2zAs9nCbLpgqI4jN8eBhtAxKRMNHyGcQhmyOy4PkkJrAAXT+ecGj++fBJTgQw6OpyCt/2SQ==",
- "license": "MIT",
- "dependencies": {
- "@tanstack/query-persist-client-core": "5.101.4"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- },
- "peerDependencies": {
- "@tanstack/react-query": "^5.101.4",
- "react": "^18 || ^19"
- }
- },
- "node_modules/@types/hammerjs": {
- "version": "2.0.46",
- "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
- "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==",
+ "version": "0.27.12",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz",
+ "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==",
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
@@ -3080,32 +1721,24 @@
}
},
"node_modules/@types/node": {
- "version": "26.1.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
- "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
+ "version": "26.5.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.0.tgz",
+ "integrity": "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A==",
"license": "MIT",
"dependencies": {
- "undici-types": "~8.3.0"
+ "undici-types": "~8.9.0"
}
},
"node_modules/@types/react": {
- "version": "19.2.17",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
- "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
+ "version": "19.2.18",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
+ "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
- "node_modules/@types/react-test-renderer": {
- "version": "19.1.0",
- "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
- "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==",
- "license": "MIT",
- "dependencies": {
- "@types/react": "*"
- }
- },
"node_modules/@types/yargs": {
"version": "17.0.35",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
@@ -3122,15 +1755,15 @@
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
- "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz",
+ "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
"license": "ISC"
},
"node_modules/@xmldom/xmldom": {
- "version": "0.8.13",
- "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
- "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
+ "version": "0.8.15",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz",
+ "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -3162,9 +1795,9 @@
}
},
"node_modules/acorn": {
- "version": "8.17.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
- "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -3183,9 +1816,9 @@
}
},
"node_modules/agent-cli-detector": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/agent-cli-detector/-/agent-cli-detector-0.1.4.tgz",
- "integrity": "sha512-qPgevFvpaQoBaRJVKzr8R7h1WPvV3DtbgRIQlne4le66KBzXx5hNBwo/+NTw67LgkKBlhCzksrdautpUdlls0Q==",
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/agent-cli-detector/-/agent-cli-detector-0.1.7.tgz",
+ "integrity": "sha512-d8OWDVdZMgjhLUT9ZPgSv/BdFFF9pVuscC0JdUSz3bjwE15gcp6u/o0/JooM2yyAWC49KThFhXlgTXRa9B7yng==",
"license": "MIT",
"bin": {
"agent-cli-detector": "dist/cli.js"
@@ -3269,12 +1902,6 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
- "node_modules/await-lock": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz",
- "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==",
- "license": "MIT"
- },
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.17",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
@@ -3289,15 +1916,6 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
- "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/babel-plugin-polyfill-corejs3": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz",
@@ -3339,27 +1957,27 @@
"license": "MIT"
},
"node_modules/babel-plugin-syntax-hermes-parser": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.36.0.tgz",
- "integrity": "sha512-LhD0xdoedDw7ansQgXbB2DADLZIK/LRXuWNBPuVzMc5S2WK5GyT89tCM+cQzxFGO0mGyLK6D5TrVOJJzAoDy8Q==",
+ "version": "0.36.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.36.1.tgz",
+ "integrity": "sha512-ycduwJbvdvIMmVvlAZqGggS+pm5Eu4Bk9pcV9Sm2Z4PJNRVsKkv0g7vHj+LeuC1gHTeF67sJXFOq61IlqCa2hA==",
"license": "MIT",
"dependencies": {
- "hermes-parser": "0.36.0"
+ "hermes-parser": "0.36.1"
}
},
"node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-estree": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz",
- "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==",
+ "version": "0.36.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.1.tgz",
+ "integrity": "sha512-guv1nQ6IJ7S83NRFPWc3SA7IBZrdNC9kapwOq6uXvF4wP+sDCgjzQbKPCoyYmoyZRzztF/n/c36l/rccCZSiCw==",
"license": "MIT"
},
"node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-parser": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz",
- "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==",
+ "version": "0.36.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.1.tgz",
+ "integrity": "sha512-GApNk4zLHi2UWoWZZkx7LNCOSzLSc5lB55pZ/PhK7ycFeg7u5LcF88p/WbpIi1XUDtE0MpHE3uRR3u3KB7TjSQ==",
"license": "MIT",
"dependencies": {
- "hermes-estree": "0.36.0"
+ "hermes-estree": "0.36.1"
}
},
"node_modules/babel-plugin-transform-flow-enums": {
@@ -3372,9 +1990,9 @@
}
},
"node_modules/babel-preset-expo": {
- "version": "57.0.4",
- "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.4.tgz",
- "integrity": "sha512-EkFcNoE23HVzQT6ZNXs/adN8+G7rqEAF6tQn6LpRPYa1YY7wmX5GxhJF0kaYMNtBndNrMTN4+0rYE17VG13KFg==",
+ "version": "57.0.11",
+ "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.11.tgz",
+ "integrity": "sha512-R0NouDI3nzQUsjBh5TUJSDXLmuVzE2bp2YE0ywJkKxzrdWVjShpAlZMBuDeOQy6iKGB6ZCCvMqbQvpqgNMJ7iw==",
"license": "MIT",
"dependencies": {
"@babel/generator": "^7.20.5",
@@ -3413,7 +2031,7 @@
"@babel/plugin-transform-typescript": "^7.25.2",
"@babel/plugin-transform-unicode-regex": "^7.24.7",
"@babel/preset-typescript": "^7.23.0",
- "@react-native/babel-plugin-codegen": "0.86.0",
+ "@react-native/babel-plugin-codegen": "0.86.3",
"babel-plugin-react-compiler": "^1.0.0",
"babel-plugin-react-native-web": "~0.21.0",
"babel-plugin-syntax-hermes-parser": "^0.36.0",
@@ -3423,7 +2041,7 @@
"peerDependencies": {
"@babel/runtime": "^7.20.0",
"expo": "*",
- "expo-widgets": "^57.0.6",
+ "expo-widgets": "^57.0.18",
"react-refresh": ">=0.14.0 <1.0.0"
},
"peerDependenciesMeta": {
@@ -3468,9 +2086,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.10.43",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz",
- "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==",
+ "version": "2.11.21",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz",
+ "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -3510,9 +2128,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
- "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -3534,9 +2152,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.28.5",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz",
- "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==",
+ "version": "4.28.9",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz",
+ "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==",
"funding": [
{
"type": "opencollective",
@@ -3553,11 +2171,11 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.10.42",
- "caniuse-lite": "^1.0.30001800",
- "electron-to-chromium": "^1.5.387",
- "node-releases": "^2.0.50",
- "update-browserslist-db": "^1.2.3"
+ "baseline-browser-mapping": "^2.11.20",
+ "caniuse-lite": "^1.0.30001810",
+ "electron-to-chromium": "^1.5.420",
+ "node-releases": "^2.0.54",
+ "update-browserslist-db": "^1.3.2"
},
"bin": {
"browserslist": "cli.js"
@@ -3603,9 +2221,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001803",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz",
- "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==",
+ "version": "1.0.30001810",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
+ "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
"funding": [
{
"type": "opencollective",
@@ -3722,19 +2340,6 @@
"node": ">=0.8"
}
},
- "node_modules/color": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
- "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1",
- "color-string": "^1.9.0"
- },
- "engines": {
- "node": ">=12.5.0"
- }
- },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3753,16 +2358,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
- "node_modules/color-string": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
- "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
- "license": "MIT",
- "dependencies": {
- "color-name": "^1.0.0",
- "simple-swizzle": "^0.2.2"
- }
- },
"node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
@@ -3856,6 +2451,19 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
+ "node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -3863,12 +2471,15 @@
"license": "MIT"
},
"node_modules/core-js-compat": {
- "version": "3.49.0",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
- "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==",
+ "version": "3.50.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz",
+ "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==",
"license": "MIT",
"dependencies": {
- "browserslist": "^4.28.1"
+ "browserslist": "^4.28.7"
+ },
+ "engines": {
+ "node": ">=6.4.0"
},
"funding": {
"type": "opencollective",
@@ -3893,6 +2504,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -3912,15 +2524,6 @@
}
}
},
- "node_modules/decode-uri-component": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
- "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10"
- }
- },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -3976,147 +2579,6 @@
"integrity": "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==",
"license": "MIT"
},
- "node_modules/drizzle-kit": {
- "version": "0.31.10",
- "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz",
- "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@drizzle-team/brocli": "^0.10.2",
- "@esbuild-kit/esm-loader": "^2.5.5",
- "esbuild": "^0.25.4",
- "tsx": "^4.21.0"
- },
- "bin": {
- "drizzle-kit": "bin.cjs"
- }
- },
- "node_modules/drizzle-orm": {
- "version": "0.45.2",
- "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz",
- "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==",
- "license": "Apache-2.0",
- "peerDependencies": {
- "@aws-sdk/client-rds-data": ">=3",
- "@cloudflare/workers-types": ">=4",
- "@electric-sql/pglite": ">=0.2.0",
- "@libsql/client": ">=0.10.0",
- "@libsql/client-wasm": ">=0.10.0",
- "@neondatabase/serverless": ">=0.10.0",
- "@op-engineering/op-sqlite": ">=2",
- "@opentelemetry/api": "^1.4.1",
- "@planetscale/database": ">=1.13",
- "@prisma/client": "*",
- "@tidbcloud/serverless": "*",
- "@types/better-sqlite3": "*",
- "@types/pg": "*",
- "@types/sql.js": "*",
- "@upstash/redis": ">=1.34.7",
- "@vercel/postgres": ">=0.8.0",
- "@xata.io/client": "*",
- "better-sqlite3": ">=7",
- "bun-types": "*",
- "expo-sqlite": ">=14.0.0",
- "gel": ">=2",
- "knex": "*",
- "kysely": "*",
- "mysql2": ">=2",
- "pg": ">=8",
- "postgres": ">=3",
- "sql.js": ">=1",
- "sqlite3": ">=5"
- },
- "peerDependenciesMeta": {
- "@aws-sdk/client-rds-data": {
- "optional": true
- },
- "@cloudflare/workers-types": {
- "optional": true
- },
- "@electric-sql/pglite": {
- "optional": true
- },
- "@libsql/client": {
- "optional": true
- },
- "@libsql/client-wasm": {
- "optional": true
- },
- "@neondatabase/serverless": {
- "optional": true
- },
- "@op-engineering/op-sqlite": {
- "optional": true
- },
- "@opentelemetry/api": {
- "optional": true
- },
- "@planetscale/database": {
- "optional": true
- },
- "@prisma/client": {
- "optional": true
- },
- "@tidbcloud/serverless": {
- "optional": true
- },
- "@types/better-sqlite3": {
- "optional": true
- },
- "@types/pg": {
- "optional": true
- },
- "@types/sql.js": {
- "optional": true
- },
- "@upstash/redis": {
- "optional": true
- },
- "@vercel/postgres": {
- "optional": true
- },
- "@xata.io/client": {
- "optional": true
- },
- "better-sqlite3": {
- "optional": true
- },
- "bun-types": {
- "optional": true
- },
- "expo-sqlite": {
- "optional": true
- },
- "gel": {
- "optional": true
- },
- "knex": {
- "optional": true
- },
- "kysely": {
- "optional": true
- },
- "mysql2": {
- "optional": true
- },
- "pg": {
- "optional": true
- },
- "postgres": {
- "optional": true
- },
- "prisma": {
- "optional": true
- },
- "sql.js": {
- "optional": true
- },
- "sqlite3": {
- "optional": true
- }
- }
- },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -4124,9 +2586,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.389",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz",
- "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==",
+ "version": "1.5.425",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.425.tgz",
+ "integrity": "sha512-QvPtl41EUOnuT1HBvMKgxXRIaHNcagBPs50u7VULzhZXaGfqTbZyE16LQsctZ/RQHlGu+FOWeDTR4mY6YbeF1g==",
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -4162,48 +2624,6 @@
"node": ">= 0.4"
}
},
- "node_modules/esbuild": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.12",
- "@esbuild/android-arm": "0.25.12",
- "@esbuild/android-arm64": "0.25.12",
- "@esbuild/android-x64": "0.25.12",
- "@esbuild/darwin-arm64": "0.25.12",
- "@esbuild/darwin-x64": "0.25.12",
- "@esbuild/freebsd-arm64": "0.25.12",
- "@esbuild/freebsd-x64": "0.25.12",
- "@esbuild/linux-arm": "0.25.12",
- "@esbuild/linux-arm64": "0.25.12",
- "@esbuild/linux-ia32": "0.25.12",
- "@esbuild/linux-loong64": "0.25.12",
- "@esbuild/linux-mips64el": "0.25.12",
- "@esbuild/linux-ppc64": "0.25.12",
- "@esbuild/linux-riscv64": "0.25.12",
- "@esbuild/linux-s390x": "0.25.12",
- "@esbuild/linux-x64": "0.25.12",
- "@esbuild/netbsd-arm64": "0.25.12",
- "@esbuild/netbsd-x64": "0.25.12",
- "@esbuild/openbsd-arm64": "0.25.12",
- "@esbuild/openbsd-x64": "0.25.12",
- "@esbuild/openharmony-arm64": "0.25.12",
- "@esbuild/sunos-x64": "0.25.12",
- "@esbuild/win32-arm64": "0.25.12",
- "@esbuild/win32-ia32": "0.25.12",
- "@esbuild/win32-x64": "0.25.12"
- }
- },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -4250,31 +2670,31 @@
}
},
"node_modules/expo": {
- "version": "57.0.8",
- "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.8.tgz",
- "integrity": "sha512-0IxxoPZbT54IH4fHL5NihkvED9HBVQx3uNdPvyv8pFUHWJ81RdFjL0aJys1IB7hbo09KTz4xW4I2aWVOXKAcJQ==",
+ "version": "57.0.21",
+ "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.21.tgz",
+ "integrity": "sha512-lQmC0kCJCleO+uLUwHXY0pLDzcvedKEsX+pmJp4mSxn3JlWDTZvUb0e5UIlV7+bqlN68LpAeKG4c2lrN4zuP0Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.0",
- "@expo/cli": "^57.0.10",
- "@expo/config": "~57.0.6",
- "@expo/config-plugins": "~57.0.6",
+ "@expo/cli": "^57.0.23",
+ "@expo/config": "~57.0.9",
+ "@expo/config-plugins": "~57.0.9",
"@expo/devtools": "~57.0.1",
"@expo/dom-webview": "~57.0.1",
- "@expo/fingerprint": "^0.20.6",
- "@expo/local-build-cache-provider": "^57.0.4",
- "@expo/log-box": "^57.0.1",
- "@expo/metro": "~56.0.0",
- "@expo/metro-config": "~57.0.7",
+ "@expo/fingerprint": "^0.20.12",
+ "@expo/local-build-cache-provider": "^57.0.8",
+ "@expo/log-box": "^57.0.4",
+ "@expo/metro": "~56.0.2",
+ "@expo/metro-config": "~57.0.12",
"@ungap/structured-clone": "^1.3.0",
- "babel-preset-expo": "~57.0.4",
- "expo-asset": "~57.0.7",
- "expo-constants": "~57.0.7",
- "expo-file-system": "~57.0.1",
- "expo-font": "~57.0.1",
+ "babel-preset-expo": "~57.0.11",
+ "expo-asset": "~57.0.16",
+ "expo-constants": "~57.0.17",
+ "expo-file-system": "~57.0.6",
+ "expo-font": "~57.0.3",
"expo-keep-awake": "~57.0.1",
- "expo-modules-autolinking": "~57.0.9",
- "expo-modules-core": "~57.0.7",
+ "expo-modules-autolinking": "~57.0.12",
+ "expo-modules-core": "~57.0.17",
"pretty-format": "^29.7.0",
"react-refresh": "^0.14.2",
"whatwg-url-minimum": "^0.1.2"
@@ -4311,120 +2731,13 @@
}
}
},
- "node_modules/expo-background-task": {
- "version": "57.0.6",
- "resolved": "https://registry.npmjs.org/expo-background-task/-/expo-background-task-57.0.6.tgz",
- "integrity": "sha512-nqKqFBvPAyQfIBXhm7OvwY99zP52EeUkoaJJum6JzFGugBSJu3Lgf7zM+0sz9klqlPtHkO7ksKyGdwH2jvz+rA==",
- "license": "MIT",
- "dependencies": {
- "expo-task-manager": "~57.0.6"
- },
- "peerDependencies": {
- "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",
- "integrity": "sha512-qBwM5oxDZ3I9kwFD3pUE1oK/WNv9artoEKO6UpqhQgNRr0XA1ALRVWYjkF4+ge9lUNDRehjTm/jenINkzqg84g==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*"
- }
- },
- "node_modules/expo-file-system": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.1.tgz",
- "integrity": "sha512-w7/ERvQFrGP2apTO9lDtZ+O6JQIhfakL7+Xqzh+rfMO9B4LB4qwrz+YvLgir8KFRVX64JHBnRuYBVLY1oQZcqw==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-font": {
- "version": "57.0.0",
- "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.0.tgz",
- "integrity": "sha512-zF+J7WrNFjqyAADwdvDgkFEoIQv9DqcjJ57HVstNEH7/7Tx1ThPEhKraodEQOwSjMYWirP+4BYsVbdb+/Zr4QQ==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "fontfaceobserver": "^2.1.0"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "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",
- "integrity": "sha512-EP0lisd2bUqtErry4weRcMW9bLMxtKsht/MLLK3/3do5u4ZMiJbWkY5zfYV+WYmeGab7x9G0sjbeFeEagYGjMw==",
- "license": "MIT",
- "dependencies": {
- "sf-symbols-typescript": "^2.2.0"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*",
- "react-native-web": "*"
- },
- "peerDependenciesMeta": {
- "react-native-web": {
- "optional": true
- }
- }
- },
- "node_modules/expo-image-loader": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-57.0.1.tgz",
- "integrity": "sha512-uhrZKLT/cTl2mXyR28kPpVkS5O+PK9N1QA/07IFM4f5T4g0lTW1JHT3NEWwEEsGFldPmVX4j7LwUVVZxE+woug==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*"
- }
- },
- "node_modules/expo-image-manipulator": {
- "version": "57.0.14",
- "resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-57.0.14.tgz",
- "integrity": "sha512-ObL2DZG53M26xvtbMa5kc9VQtIx7FngvqCgPsdX2Ws/TejHQTxG53z3yPpuT07PNiDMXhzZwwCvw9+fQojarag==",
- "license": "MIT",
- "dependencies": {
- "expo-image-loader": "~57.0.1"
- },
- "peerDependencies": {
- "expo": "*"
- }
- },
"node_modules/expo-modules-autolinking": {
- "version": "57.0.9",
- "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.9.tgz",
- "integrity": "sha512-lj2nsAKMMRLXSFnGgaaQrWJ2fdSpLPc/bca6Rkiw4g8zYPn7qX4MRUJgavvzm/hBrzvMnlhXVgJtidOGuwBh+w==",
+ "version": "57.0.12",
+ "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.12.tgz",
+ "integrity": "sha512-Q8KAlq37nLKsQ+HsS9NpQVpd5jCgqtu694TDUNHBUBpV9ViD82mRBh8Uug/h68RG9xnLS+kuL4nYaCuFRghHjg==",
"license": "MIT",
"dependencies": {
- "@expo/require-utils": "^57.0.4",
+ "@expo/require-utils": "^57.0.5",
"@expo/spawn-async": "^1.8.0",
"chalk": "^4.1.0",
"commander": "^7.2.0"
@@ -4434,13 +2747,13 @@
}
},
"node_modules/expo-modules-core": {
- "version": "57.0.7",
- "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.7.tgz",
- "integrity": "sha512-5HrbCfgYmLs0a5dzfM4GmRGelVTPIg+eYp0vmSbWnKRTOPj5DyVOfg1rHGEsuNKp07QwDxfLJfQVp8CNbuvxMQ==",
+ "version": "57.0.17",
+ "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.17.tgz",
+ "integrity": "sha512-hHJwGHW0sMQiOLzEEl1QbJeWkBEBgjlPjrdBVGWlzvgUbCXJfFbDU2nhxoyzLMZnvHkpIzpiJjj5cM5W8gbudA==",
"license": "MIT",
"dependencies": {
"@expo/expo-modules-macros-plugin": "0.6.1",
- "expo-modules-jsi": "~57.0.4",
+ "expo-modules-jsi": "~57.1.0",
"invariant": "^2.2.4"
},
"peerDependencies": {
@@ -4455,56 +2768,23 @@
}
},
"node_modules/expo-modules-jsi": {
- "version": "57.0.4",
- "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.0.4.tgz",
- "integrity": "sha512-vt7FyqUqqFXiRVnBqYD7y+GSPTgeua5Ocoy0+SYt+RSHkZEA2Fyop7If3g1TYDzQObYybPRo7TG2Rle1XLaWFw==",
+ "version": "57.1.0",
+ "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.1.0.tgz",
+ "integrity": "sha512-a5ckeHfnbYfonhcHGkM0EU/a0Keh+/OufXy/HyFS+spr5Ib0N4Oh1ZsSYFQhp5xdOUFkVofKRhfH+VmcccIIpA==",
"license": "MIT",
"peerDependencies": {
"react-native": "*"
}
},
- "node_modules/expo-print": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-print/-/expo-print-57.0.1.tgz",
- "integrity": "sha512-cEfxM+sV5suZyqbXsOmDGRHD+UmfAh2GWabbakdkl1s70L4MFiObPtC73ATXNjeUe+mlzxLhudoTxsuyJxk8Mg==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-secure-store": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-57.0.1.tgz",
- "integrity": "sha512-tLa1VmSadOq19mA/dwkl99RbHyjLE0T1qqBYMY3/OsguZTI+rlrDy/DDJjupqlVtmr95hD7o1pYqx5aL+B4YMA==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*"
- }
- },
"node_modules/expo-server": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.1.tgz",
- "integrity": "sha512-sBfVDH6dmKVHZxqUxbfkzS00PZELMZt1IpnHKxcOTMZtR/t7CtRAFrbXcisG+EyzeqHSVDacZT+1tbYfZt5D8w==",
+ "version": "57.0.3",
+ "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.3.tgz",
+ "integrity": "sha512-aK+LdKzauHSGmsOStZtyxdzv0zWssCkxTw3m4QuOhfDSJsZaMRTd9O41d8ixU/QfELTbaJ0oRNcF7JFV/7O9YQ==",
"license": "MIT",
"engines": {
"node": ">=20.16.0"
}
},
- "node_modules/expo-sqlite": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-57.0.1.tgz",
- "integrity": "sha512-I6KoUvfGIiROTKxr5D3H+jRIGA/iEvWEtqHK4XMukAA7tVTinz/YdS8zOz6/DdG6vgrNmvF7gyOcbhLinlfxzQ==",
- "license": "MIT",
- "dependencies": {
- "await-lock": "^2.2.2"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
"node_modules/expo-status-bar": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-57.0.1.tgz",
@@ -4516,50 +2796,37 @@
"react-native": "*"
}
},
- "node_modules/expo-task-manager": {
- "version": "57.0.6",
- "resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-57.0.6.tgz",
- "integrity": "sha512-nD6tvC3HNzjpOJir3Q8QDNOMtKrY2f1ik+fmKcOl7i/o822AEgGr7HpmDF/kpCQYGchdqBrodmkKcYwb9gxliQ==",
- "license": "MIT",
- "dependencies": {
- "unimodules-app-loader": "~57.0.1"
- },
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
"node_modules/expo/node_modules/@expo/cli": {
- "version": "57.0.10",
- "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.10.tgz",
- "integrity": "sha512-mP+B9ZrTnRE94bxPM/kCVKPyuVuOTRvvsqbXVxdXtrAfOfF94YIZLGUtw/151cGFtdUPbsBsJ9gW02LhU+QMZA==",
+ "version": "57.0.23",
+ "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.23.tgz",
+ "integrity": "sha512-stzSYxVwbWGbKR+mrYT6s4Rt6HrcUxb0JrHyebDGL4ehpvfRTv0rMzn4Q7fqB6f5wJ1KCmf6OAgqgK+TTB8/FA==",
"license": "MIT",
"dependencies": {
"@expo/code-signing-certificates": "^0.0.6",
- "@expo/config": "~57.0.6",
- "@expo/config-plugins": "~57.0.6",
+ "@expo/config": "~57.0.9",
+ "@expo/config-plugins": "~57.0.9",
"@expo/devcert": "^1.2.1",
- "@expo/env": "~2.4.2",
- "@expo/image-utils": "^0.11.4",
- "@expo/inline-modules": "^0.1.3",
+ "@expo/env": "~2.4.3",
+ "@expo/image-utils": "^0.11.5",
+ "@expo/inline-modules": "^0.1.7",
"@expo/json-file": "^11.0.1",
- "@expo/log-box": "^57.0.1",
- "@expo/metro": "~56.0.0",
- "@expo/metro-config": "~57.0.7",
- "@expo/metro-file-map": "^57.0.1",
+ "@expo/log-box": "^57.0.4",
+ "@expo/metro": "~56.0.2",
+ "@expo/metro-config": "~57.0.12",
+ "@expo/metro-file-map": "^57.0.3",
"@expo/osascript": "^2.7.1",
"@expo/package-manager": "^1.13.1",
"@expo/plist": "^0.8.1",
- "@expo/prebuild-config": "^57.0.9",
- "@expo/require-utils": "^57.0.4",
- "@expo/router-server": "^57.0.4",
+ "@expo/prebuild-config": "^57.0.15",
+ "@expo/require-utils": "^57.0.5",
+ "@expo/router-server": "^57.0.9",
"@expo/schema-utils": "^57.0.2",
"@expo/spawn-async": "^1.8.0",
"@expo/ws-tunnel": "^2.0.0",
"@expo/xcpretty": "^4.4.4",
- "@react-native/dev-middleware": "0.86.0",
+ "@react-native/dev-middleware": "0.86.3",
"accepts": "^1.3.8",
- "agent-cli-detector": "^0.1.2",
+ "agent-cli-detector": "0.1.7",
"arg": "^5.0.2",
"bplist-creator": "0.1.0",
"bplist-parser": "^0.3.1",
@@ -4569,12 +2836,12 @@
"connect": "^3.7.0",
"debug": "^4.3.4",
"dnssd-advertise": "^1.1.4",
- "expo-server": "^57.0.1",
+ "expo-server": "^57.0.3",
"fetch-nodeshim": "^0.4.10",
"getenv": "^2.0.0",
"glob": "^13.0.0",
"lan-network": "^0.2.1",
- "multitars": "^1.0.0",
+ "multitars": "^1.0.2",
"node-forge": "^1.3.3",
"npm-package-arg": "^11.0.0",
"ora": "^3.4.0",
@@ -4583,6 +2850,7 @@
"progress": "^2.0.3",
"prompts": "^2.3.2",
"resolve-from": "^5.0.0",
+ "sandbox-cli-detector": "^0.2.0",
"semver": "^7.6.0",
"send": "^0.19.0",
"slugify": "^1.3.4",
@@ -4612,20 +2880,20 @@
}
},
"node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": {
- "version": "57.0.4",
- "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.4.tgz",
- "integrity": "sha512-ucqCP0hK8nZb9+S8QJYdQxNCkfRClzkKdg2RpWYDKDYgRIHyoRyxEDRgDgvbhH+q78yfY83abU8OTx8MMOrf1g==",
+ "version": "57.0.9",
+ "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.9.tgz",
+ "integrity": "sha512-/PxRQozFesIyCJZOAtrQE8XcmcojNiL5ctPMQnbE4ojC2EJPu0zc7c0Y4PuXsoRxrZxm8Usw76/lCVrXcfTZ2w==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
"peerDependencies": {
- "@expo/metro-runtime": "^57.0.7",
+ "@expo/metro-runtime": "^57.0.15",
"expo": "*",
- "expo-constants": "^57.0.7",
- "expo-font": "^57.0.1",
+ "expo-constants": "^57.0.17",
+ "expo-font": "^57.0.3",
"expo-router": "*",
- "expo-server": "^57.0.1",
+ "expo-server": "^57.0.3",
"react": "*",
"react-dom": "*",
"react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1"
@@ -4657,9 +2925,9 @@
}
},
"node_modules/expo/node_modules/@expo/log-box": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-57.0.1.tgz",
- "integrity": "sha512-fuVNHhOerdRWtpq27gD6JTSVYESsfRu+SMdrNCWxW+gFnusS6dGKfx3lKGBZ4ZkMNiLWn8maBHo39YKzJNXFYQ==",
+ "version": "57.0.4",
+ "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-57.0.4.tgz",
+ "integrity": "sha512-IxwS9s1L2muj8mj8AQSuiy7u8OFJdc02NRFo2me/Tj6DiaeG5SREqmpBE4rQpR2cadqSg5jl8Qab8Cjie616dg==",
"license": "MIT",
"dependencies": {
"@expo/dom-webview": "^57.0.1",
@@ -4673,6 +2941,45 @@
"react-native": "*"
}
},
+ "node_modules/expo/node_modules/@expo/metro-config": {
+ "version": "57.0.12",
+ "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-57.0.12.tgz",
+ "integrity": "sha512-S62Lrq35HZqBFD55423pmWb8PjaiR/W02zQC1uECBmw1vTN8WZaFz4TJ0i21EeJzwfebMb9MLxL8JZ102Z6VbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.20.0",
+ "@babel/core": "^7.20.0",
+ "@babel/generator": "^7.20.5",
+ "@expo/config": "~57.0.9",
+ "@expo/env": "~2.4.3",
+ "@expo/json-file": "~11.0.1",
+ "@expo/metro": "~56.0.2",
+ "@expo/require-utils": "^57.0.5",
+ "@expo/spawn-async": "^1.8.0",
+ "@jridgewell/gen-mapping": "^0.3.13",
+ "@jridgewell/remapping": "^2.3.5",
+ "@jridgewell/sourcemap-codec": "^1.5.5",
+ "browserslist": "^4.25.0",
+ "chalk": "^4.1.0",
+ "debug": "^4.3.2",
+ "getenv": "^2.0.0",
+ "glob": "^13.0.0",
+ "hermes-parser": "^0.36.0",
+ "jsc-safe-url": "^0.2.4",
+ "lightningcss": "^1.30.1",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.14",
+ "resolve-from": "^5.0.0"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ }
+ }
+ },
"node_modules/expo/node_modules/@expo/ws-tunnel": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-2.0.0.tgz",
@@ -4682,6 +2989,73 @@
"ws": "^8.0.0"
}
},
+ "node_modules/expo/node_modules/@react-native/debugger-frontend": {
+ "version": "0.86.3",
+ "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.86.3.tgz",
+ "integrity": "sha512-TQmeofQ0PcuylhhlleOeuzHYZfbrgm3gayXzowqUEzgRisTm1D40/J3ggqs7XkQi5HP5ZA3n8dHmKL9vIzPcsw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/expo/node_modules/@react-native/debugger-shell": {
+ "version": "0.86.3",
+ "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.86.3.tgz",
+ "integrity": "sha512-O4ds+J7xZfxkbih9T+cAGegBdvKSPKYJm/lDgC9CpEjFMkmzWTpVLU3Qsv9sqZuo58z+sGhIcJfPsCFFWHpqbQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.4.0",
+ "fb-dotslash": "0.5.8"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/expo/node_modules/@react-native/dev-middleware": {
+ "version": "0.86.3",
+ "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.86.3.tgz",
+ "integrity": "sha512-LiEPTqTg/63bYUnrPyHLfjTDCNhA/+CUqI1+DsA9tYyewtSbULd5awsva6SgE10I+2iMhgKXS3ymkhU/kSCrGA==",
+ "license": "MIT",
+ "dependencies": {
+ "@isaacs/ttlcache": "^1.4.1",
+ "@react-native/debugger-frontend": "0.86.3",
+ "@react-native/debugger-shell": "0.86.3",
+ "chrome-launcher": "^0.15.2",
+ "chromium-edge-launcher": "^0.3.0",
+ "connect": "^3.6.5",
+ "debug": "^4.4.0",
+ "invariant": "^2.2.4",
+ "nullthrows": "^1.1.1",
+ "open": "^7.0.3",
+ "serve-static": "^1.16.2",
+ "ws": "^7.5.10"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
+ }
+ },
+ "node_modules/expo/node_modules/@react-native/dev-middleware/node_modules/ws": {
+ "version": "7.5.13",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz",
+ "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/expo/node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -4711,13 +3085,13 @@
}
},
"node_modules/expo/node_modules/expo-asset": {
- "version": "57.0.7",
- "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.7.tgz",
- "integrity": "sha512-TA6MRQq1HL0N6KGvNMzL6djdH5tGJ/eHPIhML+6bVu52mnXldmup2swdvP37zDnvoMUUXRVGsvwVHFmvhCbW6Q==",
+ "version": "57.0.16",
+ "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.16.tgz",
+ "integrity": "sha512-IBRfQdW3iFT+GOBERMZLZM1MUNyrjMgMskuD0elVZ1ae44858UFlTJkHO3f8vi+u5zuv7O7KofsiN8NMG/uWzw==",
"license": "MIT",
"dependencies": {
- "@expo/image-utils": "^0.11.4",
- "expo-constants": "~57.0.7"
+ "@expo/image-utils": "^0.11.5",
+ "expo-constants": "~57.0.17"
},
"peerDependencies": {
"expo": "*",
@@ -4726,22 +3100,32 @@
}
},
"node_modules/expo/node_modules/expo-constants": {
- "version": "57.0.7",
- "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.7.tgz",
- "integrity": "sha512-ShDwaKnh3UieCQ/dG0kO8PuicTTatn3WDGmXbq/fukyzPXWhdUxf37DINVDQS6D3DDG7nfqItij+QvnDHSQhTg==",
+ "version": "57.0.17",
+ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.17.tgz",
+ "integrity": "sha512-cPWYBKN1SEbg2lXg2f8VkePJqGZrJPLvVdQDCTfbFu9sQHO1M31Y1zILReUuGnmt/VKeUz40ze579vR00xGu/A==",
"license": "MIT",
"dependencies": {
- "@expo/env": "~2.4.2"
+ "@expo/env": "~2.4.3"
},
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
+ "node_modules/expo/node_modules/expo-file-system": {
+ "version": "57.0.6",
+ "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.6.tgz",
+ "integrity": "sha512-pm8PMYEW6BnVOCBJ7df9FcDmQtE1tqImuYphlfYe1ipQRLYtdCayRczJcbKIsnB3mqEyp4gC2gWmMbsAsAOjVg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo/node_modules/expo-font": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.1.tgz",
- "integrity": "sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ==",
+ "version": "57.0.3",
+ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.3.tgz",
+ "integrity": "sha512-kiVUnc2A8vAvO2FfDJTsQa5BwmY+PAkof/1wRb5MOkcX1jtiaSTwz9gCUAyscMBFLundZDmZrLy1P7LZVC+NvA==",
"license": "MIT",
"dependencies": {
"fontfaceobserver": "^2.1.0"
@@ -4793,9 +3177,9 @@
}
},
"node_modules/expo/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -4804,10 +3188,31 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/expo/node_modules/react-refresh": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
+ "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expo/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/expo/node_modules/ws": {
- "version": "8.21.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
- "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -4831,12 +3236,6 @@
"integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
"license": "Apache-2.0"
},
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "license": "MIT"
- },
"node_modules/fb-dotslash": {
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz",
@@ -4876,15 +3275,6 @@
"node": ">=8"
}
},
- "node_modules/filter-obj": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
- "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -4939,21 +3329,6 @@
"node": ">= 0.6"
}
},
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -4981,19 +3356,6 @@
"node": "6.* || 8.* || >= 10.*"
}
},
- "node_modules/get-tsconfig": {
- "version": "4.14.0",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
- "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "resolve-pkg-maps": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
- }
- },
"node_modules/getenv": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz",
@@ -5054,35 +3416,20 @@
"license": "MIT"
},
"node_modules/hermes-estree": {
- "version": "0.35.0",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz",
- "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==",
+ "version": "0.36.0",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz",
+ "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==",
"license": "MIT"
},
"node_modules/hermes-parser": {
- "version": "0.35.0",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz",
- "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==",
+ "version": "0.36.0",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz",
+ "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==",
"license": "MIT",
"dependencies": {
- "hermes-estree": "0.35.0"
+ "hermes-estree": "0.36.0"
}
},
- "node_modules/hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "react-is": "^16.7.0"
- }
- },
- "node_modules/hoist-non-react-statics/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
- },
"node_modules/hosted-git-info": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
@@ -5152,21 +3499,6 @@
"node": ">= 4"
}
},
- "node_modules/image-size": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
- "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
- "license": "MIT",
- "dependencies": {
- "queue": "6.0.2"
- },
- "bin": {
- "image-size": "bin/image-size.js"
- },
- "engines": {
- "node": ">=16.x"
- }
- },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -5182,12 +3514,6 @@
"loose-envify": "^1.0.0"
}
},
- "node_modules/is-arrayish": {
- "version": "0.3.4",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
- "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
- "license": "MIT"
- },
"node_modules/is-core-module": {
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
@@ -5355,9 +3681,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
- "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
"funding": [
{
"type": "github",
@@ -5851,9 +4177,9 @@
"license": "MIT"
},
"node_modules/metro": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.4.tgz",
- "integrity": "sha512-8ETTubqfD6ornDy2zYDvRcKnVDOXdFJsjetYDBsY4oAsb6NJkiwFR+FaMESyGppFmQUyBQA4H4sFGxzcQSGtFA==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.5.tgz",
+ "integrity": "sha512-r1liLkyFZMVSEMNjU1CJU5pRzs3NdkxHqXS60O25c0rCIqAR+cGk7rPydw/g0WAIKVXojIBIF45yYBPagJGcgw==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
@@ -5871,23 +4197,22 @@
"flow-enums-runtime": "^0.0.6",
"graceful-fs": "^4.2.4",
"hermes-parser": "0.35.0",
- "image-size": "^1.0.2",
"invariant": "^2.2.4",
"jest-worker": "^29.7.0",
"jsc-safe-url": "^0.2.2",
"lodash.throttle": "^4.1.1",
- "metro-babel-transformer": "0.84.4",
- "metro-cache": "0.84.4",
- "metro-cache-key": "0.84.4",
- "metro-config": "0.84.4",
- "metro-core": "0.84.4",
- "metro-file-map": "0.84.4",
- "metro-resolver": "0.84.4",
- "metro-runtime": "0.84.4",
- "metro-source-map": "0.84.4",
- "metro-symbolicate": "0.84.4",
- "metro-transform-plugins": "0.84.4",
- "metro-transform-worker": "0.84.4",
+ "metro-babel-transformer": "0.84.5",
+ "metro-cache": "0.84.5",
+ "metro-cache-key": "0.84.5",
+ "metro-config": "0.84.5",
+ "metro-core": "0.84.5",
+ "metro-file-map": "0.84.5",
+ "metro-resolver": "0.84.5",
+ "metro-runtime": "0.84.5",
+ "metro-source-map": "0.84.5",
+ "metro-symbolicate": "0.84.5",
+ "metro-transform-plugins": "0.84.5",
+ "metro-transform-worker": "0.84.5",
"mime-types": "^3.0.1",
"nullthrows": "^1.1.1",
"serialize-error": "^2.1.0",
@@ -5904,40 +4229,55 @@
}
},
"node_modules/metro-babel-transformer": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.4.tgz",
- "integrity": "sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.5.tgz",
+ "integrity": "sha512-2WbHILKMiJUzfdjmGOQOqU1bWi9//gqiclc/tkk/AIsrrVw3efhZ1uhkOwMTxUEPOzqoo091H0olLmVZH5FHGQ==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
"flow-enums-runtime": "^0.0.6",
"hermes-parser": "0.35.0",
- "metro-cache-key": "0.84.4",
+ "metro-cache-key": "0.84.5",
"nullthrows": "^1.1.1"
},
"engines": {
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
}
},
+ "node_modules/metro-babel-transformer/node_modules/hermes-estree": {
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz",
+ "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==",
+ "license": "MIT"
+ },
+ "node_modules/metro-babel-transformer/node_modules/hermes-parser": {
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz",
+ "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==",
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.35.0"
+ }
+ },
"node_modules/metro-cache": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.4.tgz",
- "integrity": "sha512-gpcFQdSLUwUCk71saKoE64jLFbx2nwTfVCcPSULMNT8QYq0p1eZZE29Jvd0HtT/UlhC3ZOutLxJME5xqD2JUZg==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.5.tgz",
+ "integrity": "sha512-WHS0n2OxQqtwEjSeQFPePNrMvEFhmQcUQM9cRJMHByWoi/GMWFBEWOf7hVkAM/0KRutAXNbDlSu/cZB6CyxgQQ==",
"license": "MIT",
"dependencies": {
"exponential-backoff": "^3.1.1",
"flow-enums-runtime": "^0.0.6",
"https-proxy-agent": "^7.0.5",
- "metro-core": "0.84.4"
+ "metro-core": "0.84.5"
},
"engines": {
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
}
},
"node_modules/metro-cache-key": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.4.tgz",
- "integrity": "sha512-wVO79aGrkYImpnaVS4+d5RrRBRPX31QtvKB3wKGBuiNSznduZTQHzsrJZRroFJSwnygrzdsGUtDQPuqqFjFdvw==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.5.tgz",
+ "integrity": "sha512-3dPB2TnvGjjf0/9O7AXVQURKXuQNauTZE7WpTGTlR017Gh/B5y0m/2wcqxfveUguHSpu89KhVxCAlr2k/H7uhQ==",
"license": "MIT",
"dependencies": {
"flow-enums-runtime": "^0.0.6"
@@ -5947,18 +4287,18 @@
}
},
"node_modules/metro-config": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.4.tgz",
- "integrity": "sha512-PMotGDjXcXLWo2TMRH+VR99phFNgYTwqh4OoieIKK3yTJa1Jmkl+fZJxDO0jfBvNF+WESHciHvpNuBtXaF3B0Q==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.5.tgz",
+ "integrity": "sha512-zie+uN6oohscowi2S7ByU+wUw6CrT4ZxW9uAbONOObSxx86RGmnIAmjXHLkfmcdYoY7jzOPEbqcI6oeVmqyBQA==",
"license": "MIT",
"dependencies": {
"connect": "^3.6.5",
"flow-enums-runtime": "^0.0.6",
"jest-validate": "^29.7.0",
- "metro": "0.84.4",
- "metro-cache": "0.84.4",
- "metro-core": "0.84.4",
- "metro-runtime": "0.84.4",
+ "metro": "0.84.5",
+ "metro-cache": "0.84.5",
+ "metro-core": "0.84.5",
+ "metro-runtime": "0.84.5",
"yaml": "^2.6.1"
},
"engines": {
@@ -5966,23 +4306,23 @@
}
},
"node_modules/metro-core": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.4.tgz",
- "integrity": "sha512-HONpWC5LGXZn3ffkd4Hu6AIrfE7j4Z0g0wMo/goV24WOB3lhuFZ40KgvaDiSw8iyQHloMYay5N/wPX+z8oN/PQ==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.5.tgz",
+ "integrity": "sha512-xwm605hCi5Y6eJTTb8ZWo6pkUcoBEIyiQOfkZh5GwtDwUrP9SNhTQZhzJHrBCwwxlf3Ptl/pxWJgQ1rsNYMnrA==",
"license": "MIT",
"dependencies": {
"flow-enums-runtime": "^0.0.6",
"lodash.throttle": "^4.1.1",
- "metro-resolver": "0.84.4"
+ "metro-resolver": "0.84.5"
},
"engines": {
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
}
},
"node_modules/metro-file-map": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.4.tgz",
- "integrity": "sha512-KSVDi/u60hKPx++NLu3MTIvyjzNoJnFAF8PQFxaj1jiSka/wjw+Ua6sNuJ0TDHQv+7AAoFQxeMgaRAe8Yic5wQ==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.5.tgz",
+ "integrity": "sha512-mlm/JL8toSbSc2akpKIGmzvrVRSCgZ5vkbycI34oMLoOnLGuLyC8WTyVJ6P0hZG/usDaGwZSl/s9BCRriqjGJA==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
@@ -6000,9 +4340,9 @@
}
},
"node_modules/metro-minify-terser": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.4.tgz",
- "integrity": "sha512-5qpbaVOMC7CPitIpuewzVeGw7E+C3ykbv2mqTjQLl85Z3annSVGlSCTcsZjqXZzjupfK4Ztj3dDc4kc44NZwtQ==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.5.tgz",
+ "integrity": "sha512-BJoFwCEDsYnagPqarayInv2+diCDNDdLlaof/p6s9w4gh+gc9HXYM+pDvsKGKKUumpZswNF3Z/ftTMqKl/5IBg==",
"license": "MIT",
"dependencies": {
"flow-enums-runtime": "^0.0.6",
@@ -6013,9 +4353,9 @@
}
},
"node_modules/metro-resolver": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.4.tgz",
- "integrity": "sha512-1qLgbxQ5ZGhhutuPot1Yp348ofDsATL2WkrHF65TobqTT9K3P9qJXw38bomk7ncp5B7OYMfWwtyBZo1lCV792A==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.5.tgz",
+ "integrity": "sha512-VSSnepg1k6LyCwtb6eirWdAWlpKwBG8Rdtsr1mU38rMelFyWgh3/QuMSiZIZAIjwg/fsa8GhW5/FO54CAUPCEA==",
"license": "MIT",
"dependencies": {
"flow-enums-runtime": "^0.0.6"
@@ -6025,9 +4365,9 @@
}
},
"node_modules/metro-runtime": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.4.tgz",
- "integrity": "sha512-Jibypds4g7AhzdRKY+kDoj51s5EXMwgyp5ddtlreDAsWefMdOx+agWqgm0H2XSZ/ueanHHVM89fnf5OJnlxa8Q==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.5.tgz",
+ "integrity": "sha512-U1m2+d1Pr+JO2/iVXBB2OfXXityz7tqwIorxfrT15IEgaHvpJBq/OHiqnOWPKJbUl3JcxjcdviZZOKk85oK4Qg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.0",
@@ -6038,18 +4378,18 @@
}
},
"node_modules/metro-source-map": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.4.tgz",
- "integrity": "sha512-jbWkPxIesVuo1IWkvezmMJld6iu8nD62GsrZiV6jP37AOdbo4OBq1FJ+qkOg8sV05wAHB//jAbziuW0SlJfW4g==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.5.tgz",
+ "integrity": "sha512-2BtV5L9uPc49F13Gn5wiP6bX/EncqzqTIk2VL/0F/96Vo0YEOjluT/qktQjFODfqGFsucwnh5mPEAl/2jVEfeg==",
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"flow-enums-runtime": "^0.0.6",
"invariant": "^2.2.4",
- "metro-symbolicate": "0.84.4",
+ "metro-symbolicate": "0.84.5",
"nullthrows": "^1.1.1",
- "ob1": "0.84.4",
+ "ob1": "0.84.5",
"source-map": "^0.5.6",
"vlq": "^1.0.0"
},
@@ -6058,14 +4398,14 @@
}
},
"node_modules/metro-symbolicate": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.4.tgz",
- "integrity": "sha512-OnfpacxUqGPZQ27t8qK9mFa7uqHIlVWeqRqkCbvMvreEBiamEeOn8krKtcwgP5M4cYDPwuSmCTopHMVthqG4zA==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.5.tgz",
+ "integrity": "sha512-rQ40zYDAkaWBN9yvjUuAD0ZpzBMZSoKyGYXnb5JrfbKjun7fTvfoLHL3KXFYenBTYZkQtlp4cKSCv/1utxFyOw==",
"license": "MIT",
"dependencies": {
"flow-enums-runtime": "^0.0.6",
"invariant": "^2.2.4",
- "metro-source-map": "0.84.4",
+ "metro-source-map": "0.84.5",
"nullthrows": "^1.1.1",
"source-map": "^0.5.6",
"vlq": "^1.0.0"
@@ -6078,9 +4418,9 @@
}
},
"node_modules/metro-transform-plugins": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.4.tgz",
- "integrity": "sha512-kehr6HbAecqD0/a3xLXobELdPaAmRAl8bel0qagPF4vhZtux93nS8S4eq2kgKt6J2GnQpVjSoW1PXdst04mwow==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.5.tgz",
+ "integrity": "sha512-+InaSVGaOyt0DyRo4Y/zIdPI6CZwnbNho5LAL23tgmuGwv7fyfkF7kKfPjZcfxXBcoYdTLLFnCfCH/dHSiCqNg==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
@@ -6095,9 +4435,9 @@
}
},
"node_modules/metro-transform-worker": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.4.tgz",
- "integrity": "sha512-W1IYMvvXTu4MxYr7d9h7CeG2vpIr3bmLLIavkPY4O1ilzDrvS8z/NEe6y+pC44Ff7raMXQgYSfdqDUwN/i39gg==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.5.tgz",
+ "integrity": "sha512-ui1Z8x4s5RL36gMmKLaMMO7O9NNDHNdthEZSCDQHAau3JcAsTaFOK6I+2q4I/kW5u8hSEjJk9L45TXSVJw6g1A==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
@@ -6105,19 +4445,34 @@
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"flow-enums-runtime": "^0.0.6",
- "metro": "0.84.4",
- "metro-babel-transformer": "0.84.4",
- "metro-cache": "0.84.4",
- "metro-cache-key": "0.84.4",
- "metro-minify-terser": "0.84.4",
- "metro-source-map": "0.84.4",
- "metro-transform-plugins": "0.84.4",
+ "metro": "0.84.5",
+ "metro-babel-transformer": "0.84.5",
+ "metro-cache": "0.84.5",
+ "metro-cache-key": "0.84.5",
+ "metro-minify-terser": "0.84.5",
+ "metro-source-map": "0.84.5",
+ "metro-transform-plugins": "0.84.5",
"nullthrows": "^1.1.1"
},
"engines": {
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
}
},
+ "node_modules/metro/node_modules/hermes-estree": {
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz",
+ "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==",
+ "license": "MIT"
+ },
+ "node_modules/metro/node_modules/hermes-parser": {
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz",
+ "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==",
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.35.0"
+ }
+ },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -6220,15 +4575,15 @@
"license": "MIT"
},
"node_modules/multitars": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/multitars/-/multitars-1.0.0.tgz",
- "integrity": "sha512-H/J4fMLedtudftaYMOg7ajzLYgT3/rwbWVJbqr/iUgB8DQztn38ys5HOqI1CzSxx8QhXXwOOnnBvd4v3jG5+Mg==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/multitars/-/multitars-1.0.2.tgz",
+ "integrity": "sha512-6GwVw5eLi9sThdtlS4PKwC7yRLaf45pYhIEzKBHdKxi+YOXGKFX8acIniH+Uh/+k9mS2lQOupTccjoe5r0/1IQ==",
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.16",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
- "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"funding": [
{
"type": "github",
@@ -6244,12 +4599,19 @@
}
},
"node_modules/negotiator": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
- "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
+ "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
"license": "MIT",
+ "dependencies": {
+ "content-type": "^2.1.0"
+ },
"engines": {
- "node": ">= 0.6"
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/node-forge": {
@@ -6268,9 +4630,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.51",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
- "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
+ "version": "2.0.55",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz",
+ "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -6291,6 +4653,18 @@
"node": "^16.14.0 || >=18.0.0"
}
},
+ "node_modules/npm-package-arg/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/nullthrows": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -6298,9 +4672,9 @@
"license": "MIT"
},
"node_modules/ob1": {
- "version": "0.84.4",
- "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.4.tgz",
- "integrity": "sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==",
+ "version": "0.84.5",
+ "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.5.tgz",
+ "integrity": "sha512-aH9RkoZc7w/90HBamFxTw8ZLFr05wXS+iOnvmrgo53Ep8Pyrm5FieQSaPIVROkfFVQISeD/zo92fes26TOwe+A==",
"license": "MIT",
"dependencies": {
"flow-enums-runtime": "^0.0.6"
@@ -6561,9 +4935,9 @@
}
},
"node_modules/plist/node_modules/@xmldom/xmldom": {
- "version": "0.9.10",
- "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
- "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==",
+ "version": "0.9.12",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.12.tgz",
+ "integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==",
"license": "MIT",
"engines": {
"node": ">=14.6"
@@ -6579,9 +4953,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.25",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
- "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
+ "version": "8.5.28",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+ "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
"funding": [
{
"type": "opencollective",
@@ -6598,7 +4972,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.16",
+ "nanoid": "^3.3.18",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -6672,33 +5046,6 @@
"node": ">= 6"
}
},
- "node_modules/query-string": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
- "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
- "license": "MIT",
- "dependencies": {
- "decode-uri-component": "^0.2.2",
- "filter-obj": "^1.1.0",
- "split-on-first": "^1.0.0",
- "strict-uri-encode": "^2.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/queue": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
- "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
- "license": "MIT",
- "dependencies": {
- "inherits": "~2.0.3"
- }
- },
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -6727,18 +5074,6 @@
"ws": "^7"
}
},
- "node_modules/react-freeze": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz",
- "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "react": ">=17.0.0"
- }
- },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -6804,142 +5139,34 @@
}
}
},
- "node_modules/react-native-gesture-handler": {
- "version": "2.32.0",
- "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.32.0.tgz",
- "integrity": "sha512-uYIMOKlKENORq2SABE+jIjbPU+h5I/sQKcq2v16zRq848nwEp1fWRVwML4QWqijc8UcXJC25o54S8GQd4Mf2OA==",
+ "node_modules/react-native/node_modules/@react-native/codegen": {
+ "version": "0.86.0",
+ "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.0.tgz",
+ "integrity": "sha512-uTs9DBo3+/lUqinsGZK0FKJRBVClrwMXoZToaDxE1Q2SL2e55vs2GwyZfIKzPl5uJnbu4PfFMIp0/mLXLWUMuA==",
"license": "MIT",
"dependencies": {
- "@egjs/hammerjs": "^2.0.17",
- "@types/react-test-renderer": "^19.1.0",
- "hoist-non-react-statics": "^3.3.0",
- "invariant": "^2.2.4"
+ "@babel/core": "^7.25.2",
+ "@babel/parser": "^7.29.0",
+ "hermes-parser": "0.36.0",
+ "invariant": "^2.2.4",
+ "nullthrows": "^1.1.1",
+ "tinyglobby": "^0.2.15",
+ "yargs": "^17.6.2"
+ },
+ "engines": {
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
},
"peerDependencies": {
- "react": "*",
- "react-native": "*"
+ "@babel/core": "*"
}
},
- "node_modules/react-native-is-edge-to-edge": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz",
- "integrity": "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==",
- "license": "MIT",
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/react-native-mmkv": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-4.3.2.tgz",
- "integrity": "sha512-49OAyfkg0/TMWiWELZN6VuVQPZPhizwL4DTmp8b7B1md3dB/s3LH3mGfC3T+lp9W0y/rqxZMEnotLFTIbOAenQ==",
- "license": "MIT",
- "peerDependencies": {
- "react": "*",
- "react-native": "*",
- "react-native-nitro-modules": "*"
- }
- },
- "node_modules/react-native-nitro-image": {
- "version": "0.15.1",
- "resolved": "https://registry.npmjs.org/react-native-nitro-image/-/react-native-nitro-image-0.15.1.tgz",
- "integrity": "sha512-slrImfUgasAdzylTiNWuTkawE0R4t7LGh+7N2VFa/D9+SxlQbkKYp1J6hA3XkkEnTnUCKaea39ZVg+1xx665MQ==",
- "license": "MIT",
- "workspaces": [
- "example"
- ],
- "peerDependencies": {
- "react": "*",
- "react-native": "*",
- "react-native-nitro-modules": "*"
- }
- },
- "node_modules/react-native-nitro-modules": {
- "version": "0.36.1",
- "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.36.1.tgz",
- "integrity": "sha512-kBv/VvKqAmkXAvP1DxJMC9b/fRhh7JdSO4EUnPP46hJjrIFeFR8AwKm8mYaKZEuF014M/TVdv2vomVUW0umsQQ==",
- "license": "MIT",
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/react-native-reanimated": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.0.tgz",
- "integrity": "sha512-+iPfvK34PKKYP/p/4TaBliFkbfvjGDIvXuiiaxvISP5ip7sWegvlacwU/uAV6zNDSSmX0tDyER7PurPMKGDipA==",
+ "node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": {
+ "version": "0.36.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.36.0.tgz",
+ "integrity": "sha512-LhD0xdoedDw7ansQgXbB2DADLZIK/LRXuWNBPuVzMc5S2WK5GyT89tCM+cQzxFGO0mGyLK6D5TrVOJJzAoDy8Q==",
"license": "MIT",
"dependencies": {
- "react-native-is-edge-to-edge": "^1.3.1",
- "semver": "^7.7.3"
- },
- "peerDependencies": {
- "react": "*",
- "react-native": "0.83 - 0.86",
- "react-native-worklets": "0.10.x"
- }
- },
- "node_modules/react-native-safe-area-context": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz",
- "integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==",
- "license": "MIT",
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/react-native-screens": {
- "version": "4.26.2",
- "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.26.2.tgz",
- "integrity": "sha512-2XnWsZToKj76trGtEZzx5ELD/qOICFEprEeUntImmitQFVUkea27fiWdUSITArI356Y1qynpXZINW+Unbhky/A==",
- "license": "MIT",
- "dependencies": {
- "react-freeze": "^1.0.0",
- "warn-once": "^0.1.0"
- },
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "node_modules/react-native-vision-camera": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/react-native-vision-camera/-/react-native-vision-camera-5.1.0.tgz",
- "integrity": "sha512-1rsZ4caFLmOhQ9wediPuqd+WEdRzLZ2/tMfo4Itdeuyx4V/sPkerIgYT2DYYvyXbB8swvFgEW7ihIdnUkarqeA==",
- "license": "MIT",
- "peerDependencies": {
- "react": "*",
- "react-native": "*",
- "react-native-nitro-image": "*",
- "react-native-nitro-modules": "*"
- }
- },
- "node_modules/react-native-worklets": {
- "version": "0.10.0",
- "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.0.tgz",
- "integrity": "sha512-JhE6IxDf6iabC0qu3+TAKA4v9RlluXmoIngPQX7/QUByf75lfrsHZ6/dQhyjEWnp1EEQiwzz8Cpew140ZcewDw==",
- "license": "MIT",
- "dependencies": {
- "@babel/plugin-transform-arrow-functions": "^7.27.1",
- "@babel/plugin-transform-class-properties": "^7.28.6",
- "@babel/plugin-transform-classes": "^7.28.6",
- "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
- "@babel/plugin-transform-optional-chaining": "^7.28.6",
- "@babel/plugin-transform-shorthand-properties": "^7.27.1",
- "@babel/plugin-transform-template-literals": "^7.27.1",
- "@babel/plugin-transform-unicode-regex": "^7.27.1",
- "@babel/preset-typescript": "^7.28.5",
- "@babel/types": "^7.27.1",
- "convert-source-map": "^2.0.0",
- "semver": "^7.7.4"
- },
- "peerDependencies": {
- "@babel/core": "*",
- "@react-native/metro-config": "*",
- "react": "*",
- "react-native": "0.83 - 0.86"
+ "hermes-parser": "0.36.0"
}
},
"node_modules/react-native/node_modules/commander": {
@@ -6951,7 +5178,7 @@
"node": ">=18"
}
},
- "node_modules/react-refresh": {
+ "node_modules/react-native/node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
@@ -6960,6 +5187,28 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-native/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -7058,16 +5307,6 @@
"node": ">=8"
}
},
- "node_modules/resolve-pkg-maps": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
- "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
- }
- },
"node_modules/resolve-workspace-root": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.1.tgz",
@@ -7107,6 +5346,18 @@
],
"license": "MIT"
},
+ "node_modules/sandbox-cli-detector": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/sandbox-cli-detector/-/sandbox-cli-detector-0.2.0.tgz",
+ "integrity": "sha512-4lyHX0ZU0AZKwjgZ1InxZAa3PNpyEb8rOQ+Zss1ReYmhNzW0Q+h1zE5nvniXN0HaAWZaZE1zgVNEirb0R7LmNg==",
+ "license": "MIT",
+ "bin": {
+ "sandbox-cli-detector": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=18.18"
+ }
+ },
"node_modules/sax": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
@@ -7123,15 +5374,12 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.8.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
}
},
"node_modules/send": {
@@ -7242,15 +5490,6 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
- "node_modules/sf-symbols-typescript": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz",
- "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7301,15 +5540,6 @@
"plist": "^3.0.5"
}
},
- "node_modules/simple-swizzle": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
- "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
- "license": "MIT",
- "dependencies": {
- "is-arrayish": "^0.3.1"
- }
- },
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -7362,15 +5592,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/split-on-first": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
- "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
@@ -7389,12 +5610,6 @@
"node": ">=6"
}
},
- "node_modules/standard-navigation": {
- "version": "0.0.7",
- "resolved": "https://registry.npmjs.org/standard-navigation/-/standard-navigation-0.0.7.tgz",
- "integrity": "sha512-NCGLCNyuXrFOkGHxdNZFnpsehGtiq1oXbPhKl7ZuxFO5J//H2evqqOchmD4YwEUJnkjO4kH9Xp4hQX6hdAYCKQ==",
- "license": "MIT"
- },
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@@ -7413,15 +5628,6 @@
"node": ">= 0.10.0"
}
},
- "node_modules/strict-uri-encode": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
- "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -7508,9 +5714,9 @@
}
},
"node_modules/terser": {
- "version": "5.49.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz",
- "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==",
+ "version": "5.51.2",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz",
+ "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==",
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@@ -7571,9 +5777,9 @@
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -7615,509 +5821,6 @@
"integrity": "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==",
"license": "MIT"
},
- "node_modules/tsx": {
- "version": "4.23.1",
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz",
- "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "~0.28.0"
- },
- "bin": {
- "tsx": "dist/cli.mjs"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
- "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/android-arm": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
- "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/android-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
- "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/android-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
- "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
- "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
- "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
- "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
- "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-arm": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
- "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
- "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
- "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
- "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
- "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
- "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
- "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
- "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
- "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
- "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
- "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
- "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
- "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
- "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
- "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
- "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
- "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/win32-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
- "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/esbuild": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
- "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.1",
- "@esbuild/android-arm": "0.28.1",
- "@esbuild/android-arm64": "0.28.1",
- "@esbuild/android-x64": "0.28.1",
- "@esbuild/darwin-arm64": "0.28.1",
- "@esbuild/darwin-x64": "0.28.1",
- "@esbuild/freebsd-arm64": "0.28.1",
- "@esbuild/freebsd-x64": "0.28.1",
- "@esbuild/linux-arm": "0.28.1",
- "@esbuild/linux-arm64": "0.28.1",
- "@esbuild/linux-ia32": "0.28.1",
- "@esbuild/linux-loong64": "0.28.1",
- "@esbuild/linux-mips64el": "0.28.1",
- "@esbuild/linux-ppc64": "0.28.1",
- "@esbuild/linux-riscv64": "0.28.1",
- "@esbuild/linux-s390x": "0.28.1",
- "@esbuild/linux-x64": "0.28.1",
- "@esbuild/netbsd-arm64": "0.28.1",
- "@esbuild/netbsd-x64": "0.28.1",
- "@esbuild/openbsd-arm64": "0.28.1",
- "@esbuild/openbsd-x64": "0.28.1",
- "@esbuild/openharmony-arm64": "0.28.1",
- "@esbuild/sunos-x64": "0.28.1",
- "@esbuild/win32-arm64": "0.28.1",
- "@esbuild/win32-ia32": "0.28.1",
- "@esbuild/win32-x64": "0.28.1"
- }
- },
"node_modules/type-fest": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
@@ -8142,9 +5845,9 @@
}
},
"node_modules/undici-types": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
- "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
+ "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@@ -8187,12 +5890,6 @@
"node": ">=4"
}
},
- "node_modules/unimodules-app-loader": {
- "version": "57.0.1",
- "resolved": "https://registry.npmjs.org/unimodules-app-loader/-/unimodules-app-loader-57.0.1.tgz",
- "integrity": "sha512-wey5ChoJkCTq0j0JWdIMu2QB81vVrdhmrNAP14ZZ6WDslnZ7ff7Ezv8rMdEnVHaCKz3xK4mIVXbVU51xHgdyCA==",
- "license": "MIT"
- },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -8203,9 +5900,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
- "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
+ "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
"funding": [
{
"type": "opencollective",
@@ -8232,24 +5929,6 @@
"browserslist": ">= 4.21.0"
}
},
- "node_modules/use-latest-callback": {
- "version": "0.2.6",
- "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz",
- "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==",
- "license": "MIT",
- "peerDependencies": {
- "react": ">=16.8"
- }
- },
- "node_modules/use-sync-external-store": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
- "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -8302,12 +5981,6 @@
"makeerror": "1.0.12"
}
},
- "node_modules/warn-once": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz",
- "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==",
- "license": "MIT"
- },
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@@ -8362,9 +6035,9 @@
}
},
"node_modules/ws": {
- "version": "7.5.11",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz",
- "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==",
+ "version": "7.5.13",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz",
+ "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
diff --git a/mobile/package.json b/mobile/package.json
index a049bf5..9a7d1f7 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -1,46 +1,16 @@
{
"name": "webui",
- "version": "1.0.0",
+ "version": "2.0.0",
"main": "index.ts",
"dependencies": {
- "@expo/vector-icons": "^15.0.2",
- "@react-native-community/netinfo": "12.0.1",
- "@react-navigation/native": "^7.3.8",
- "@react-navigation/native-stack": "^7.17.10",
- "@tanstack/query-async-storage-persister": "^5.101.4",
- "@tanstack/react-query": "^5.101.2",
- "@tanstack/react-query-persist-client": "^5.101.4",
- "babel-preset-expo": "^57.0.4",
- "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",
- "expo-secure-store": "~57.0.1",
- "expo-sqlite": "~57.0.1",
"expo-status-bar": "~57.0.1",
- "expo-task-manager": "~57.0.6",
"react": "19.2.3",
- "react-native": "0.86.0",
- "react-native-gesture-handler": "~2.32.0",
- "react-native-mmkv": "^4.3.2",
- "react-native-nitro-image": "^0.15.1",
- "react-native-nitro-modules": "^0.36.1",
- "react-native-reanimated": "4.5.0",
- "react-native-safe-area-context": "~5.7.0",
- "react-native-screens": "~4.26.0",
- "react-native-vision-camera": "^5.1.0",
- "react-native-worklets": "0.10.0"
+ "react-native": "0.86.0"
},
"devDependencies": {
- "@expo/metro-config": "^57.0.7",
"@types/react": "~19.2.2",
- "drizzle-kit": "^0.31.10",
+ "babel-preset-expo": "^57.0.4",
"typescript": "~6.0.3"
},
"scripts": {
@@ -50,4 +20,4 @@
"web": "expo start --web"
},
"private": true
-}
+}
\ No newline at end of file
diff --git a/mobile/services/actionQueue.ts b/mobile/services/actionQueue.ts
deleted file mode 100644
index 25456e6..0000000
--- a/mobile/services/actionQueue.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-import { fileStore } from './fileStore';
-import { apiClient } from '../api/client';
-import { ENDPOINTS } from '../constants/api';
-import { HttpError } from '../types';
-import type { PendingAction, PendingActionType } from '../types';
-
-type Listener = () => void;
-
-const MAX_ATTEMPTS = 5;
-const BASE_RETRY_DELAY_MS = 2000;
-
-function genId(): string {
- return `action_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
-}
-
-async function executeAction(action: PendingAction): Promise {
- const { type, payload, resourceId } = action;
-
- switch (type) {
- case 'tag_add': {
- const { fileId, tags } = payload as { fileId: string; tags: string[] };
- await apiClient.post(`${ENDPOINTS.RESOURCES}/${fileId}/tags`, { tags });
- break;
- }
- case 'delete': {
- const { backendId } = payload as { backendId: string };
- try {
- await apiClient.delete(`${ENDPOINTS.RESOURCES}/${backendId}`);
- } catch (err) {
- if (err instanceof HttpError && err.status === 404) {
- break;
- }
- throw err;
- }
- break;
- }
- case 'move': {
- const { resourceIds, parentResourceId } = payload as { resourceIds: string[]; parentResourceId: string | null };
- await apiClient.post(ENDPOINTS.MOVE, { resource_ids: resourceIds, parent_resource_id: parentResourceId });
- break;
- }
- case 'create_folder': {
- const { name, parentResourceId } = payload as { name: string; parentResourceId?: string };
- await apiClient.post(ENDPOINTS.FOLDERS, { name, parent_resource_id: parentResourceId });
- break;
- }
- default:
- throw new Error(`Unknown action type: ${type}`);
- }
-}
-
-class ActionQueue {
- private processing = false;
- private listeners = new Set();
- private timer: ReturnType | null = null;
-
- subscribe(listener: Listener): () => void {
- this.listeners.add(listener);
- return () => { this.listeners.delete(listener); };
- }
-
- private notify() {
- this.listeners.forEach((l) => l());
- }
-
- enqueue(type: PendingActionType, payload: Record, resourceId?: string): PendingAction {
- const action: PendingAction = {
- id: genId(),
- type,
- payload,
- status: 'pending',
- attempts: 0,
- lastError: null,
- resourceId: resourceId ?? null,
- createdAt: new Date().toISOString(),
- };
- fileStore.insertPendingAction(action);
- this.notify();
- return action;
- }
-
- async processNext(): Promise {
- const pending = fileStore.getPendingActions();
- if (pending.length === 0) return false;
-
- const action = pending[0];
-
- // Check if a prior delete makes this action obsolete
- if (action.type !== 'delete' && action.resourceId) {
- const priorDelete = fileStore.getPendingActions().find(
- (a) => a.type === 'delete' && a.resourceId === action.resourceId && a.createdAt < action.createdAt
- );
- if (priorDelete) {
- fileStore.markPendingActionObsolete(action.id);
- this.notify();
- return true;
- }
- }
-
- try {
- await executeAction(action);
- fileStore.markPendingActionDone(action.id);
- this.notify();
- return true;
- } catch (err) {
- const msg = err instanceof Error ? err.message : String(err);
- fileStore.markPendingActionError(action.id, msg);
-
- if (action.attempts + 1 >= MAX_ATTEMPTS) {
- fileStore.markPendingActionObsolete(action.id);
- }
-
- this.notify();
- return false;
- }
- }
-
- async processAll(): Promise {
- if (this.processing) return;
- this.processing = true;
-
- try {
- let hasMore = true;
- while (hasMore) {
- const pending = fileStore.getPendingActions();
- if (pending.length === 0) break;
-
- const success = await this.processNext();
- if (!success) {
- // Wait before retrying on error
- await new Promise((r) => setTimeout(r, BASE_RETRY_DELAY_MS));
- // Check if there are still pending actions (not just the one we failed on)
- const remaining = fileStore.getPendingActions();
- if (remaining.length === 0 || remaining[0].status !== 'pending') break;
- }
- }
- } finally {
- this.processing = false;
- fileStore.clearDonePendingActions();
- this.notify();
- }
- }
-
- isProcessing(): boolean {
- return this.processing;
- }
-
- getPendingCount(): number {
- return fileStore.getPendingActionsCount();
- }
-
- scheduleRetry(delayMs: number = 10_000) {
- if (this.timer) return;
- this.timer = setTimeout(() => {
- this.timer = null;
- this.processAll();
- }, delayMs);
- }
-
- cancelSchedule() {
- if (this.timer) {
- clearTimeout(this.timer);
- this.timer = null;
- }
- }
-}
-
-export const actionQueue = new ActionQueue();
diff --git a/mobile/services/backgroundUpload.ts b/mobile/services/backgroundUpload.ts
deleted file mode 100644
index 64dc174..0000000
--- a/mobile/services/backgroundUpload.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import * as BackgroundTask from 'expo-background-task';
-import * as TaskManager from 'expo-task-manager';
-
-const BACKGROUND_UPLOAD_TASK = 'BACKGROUND_UPLOAD';
-
-TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => {
- try {
- const { uploadQueue } = await import('./uploadQueue');
- const { actionQueue } = await import('./actionQueue');
- const { apiClient } = await import('../api/client');
- const { tokenStorage } = await import('../api/secureStorage');
-
- const pendingUploads = uploadQueue.getPendingCount();
- const pendingActions = actionQueue.getPendingCount();
- if (pendingUploads === 0 && pendingActions === 0) {
- return BackgroundTask.BackgroundTaskResult.Success;
- }
-
- const token = await tokenStorage.getAccessToken();
- if (!token) {
- return BackgroundTask.BackgroundTaskResult.Success;
- }
- apiClient.setAccessToken(token);
-
- if (pendingUploads > 0) {
- uploadQueue.retryAll();
- }
- if (pendingActions > 0) {
- actionQueue.processAll();
- }
-
- await new Promise((resolve) => {
- const check = setInterval(() => {
- if (uploadQueue.getPendingCount() === 0 && actionQueue.getPendingCount() === 0) {
- clearInterval(check);
- resolve();
- }
- }, 1000);
- setTimeout(() => {
- clearInterval(check);
- resolve();
- }, 25000);
- });
-
- return BackgroundTask.BackgroundTaskResult.Success;
- } catch {
- return BackgroundTask.BackgroundTaskResult.Failed;
- }
-});
-
-let isRegistered = false;
-
-export async function registerBackgroundUpload() {
- if (isRegistered) return;
- isRegistered = true;
-
- const status = await BackgroundTask.getStatusAsync();
-
- if (status === BackgroundTask.BackgroundTaskStatus.Restricted) {
- console.warn('[BackgroundUpload] Permission refusée');
- return;
- }
-
- await BackgroundTask.registerTaskAsync(BACKGROUND_UPLOAD_TASK, {
- minimumInterval: 15,
- });
-
- console.log('[BackgroundUpload] Enregistré (intervalle: 15min)');
-}
diff --git a/mobile/services/downloadRegistry.ts b/mobile/services/downloadRegistry.ts
deleted file mode 100644
index 49a9275..0000000
--- a/mobile/services/downloadRegistry.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { createMMKV } from 'react-native-mmkv';
-import { FileDetectedEvent } from '../modules/expo-download-detect';
-
-const storage = createMMKV({ id: 'vaultdrop-download-registry' });
-
-const FILES_KEY = 'detected_files';
-
-export type DownloadRegistryEntry = {
- id: string;
- uri: string;
- name: string;
- mimeType: string;
- size: number;
- createdAt: number;
- source: string;
- detectedAt: number;
-};
-
-function getAllRaw(): DownloadRegistryEntry[] {
- const raw = storage.getString(FILES_KEY);
- if (!raw) return [];
- return JSON.parse(raw) as DownloadRegistryEntry[];
-}
-
-function saveAll(entries: DownloadRegistryEntry[]) {
- storage.set(FILES_KEY, JSON.stringify(entries));
-}
-
-export const downloadRegistry = {
- getAll(): DownloadRegistryEntry[] {
- return getAllRaw();
- },
-
- getById(id: string): DownloadRegistryEntry | undefined {
- return getAllRaw().find((e) => e.id === id);
- },
-
- add(event: FileDetectedEvent): DownloadRegistryEntry | null {
- const entries = getAllRaw();
- if (entries.some((e) => e.id === event.id)) return null;
-
- const entry: DownloadRegistryEntry = {
- id: event.id,
- uri: event.uri,
- name: event.name,
- mimeType: event.mimeType,
- size: event.size,
- createdAt: event.createdAt,
- source: event.source,
- detectedAt: Date.now(),
- };
-
- entries.push(entry);
- saveAll(entries);
- return entry;
- },
-
- addBatch(events: FileDetectedEvent[]): DownloadRegistryEntry[] {
- const entries = getAllRaw();
- const existingIds = new Set(entries.map((e) => e.id));
- const newEntries: DownloadRegistryEntry[] = [];
-
- for (const event of events) {
- if (existingIds.has(event.id)) continue;
- const entry: DownloadRegistryEntry = {
- id: event.id,
- uri: event.uri,
- name: event.name,
- mimeType: event.mimeType,
- size: event.size,
- createdAt: event.createdAt,
- source: event.source,
- detectedAt: Date.now(),
- };
- newEntries.push(entry);
- existingIds.add(event.id);
- }
-
- if (newEntries.length > 0) {
- saveAll([...entries, ...newEntries]);
- }
-
- return newEntries;
- },
-
- remove(id: string) {
- const entries = getAllRaw().filter((e) => e.id !== id);
- saveAll(entries);
- },
-
- clear() {
- storage.remove(FILES_KEY);
- },
-
- count(): number {
- return getAllRaw().length;
- },
-};
diff --git a/mobile/services/fileStore/index.ts b/mobile/services/fileStore/index.ts
deleted file mode 100644
index d53b1b3..0000000
--- a/mobile/services/fileStore/index.ts
+++ /dev/null
@@ -1,757 +0,0 @@
-import { drizzle } from 'drizzle-orm/expo-sqlite';
-import * as SQLite from 'expo-sqlite';
-import { eq, like, or, and, desc, asc, sql, isNull, inArray, isNotNull } from 'drizzle-orm';
-import { files, fileTags, deletedFiles, pendingActions } from './schema';
-import type { Tag, PendingAction, PendingActionType, PendingActionStatus } from '../../types';
-
-const DB_NAME = 'vaultdrop-v3.db';
-const SCHEMA_VERSION_KEY = 'schema_version';
-const SCHEMA_VERSION = 5;
-
-let _db: ReturnType | null = null;
-let _sqliteDb: SQLite.SQLiteDatabase | null = null;
-
-export function initDB() {
- if (_db) return _db;
- _sqliteDb = SQLite.openDatabaseSync(DB_NAME);
- _sqliteDb.execSync('PRAGMA journal_mode = WAL;');
- _sqliteDb.execSync('PRAGMA foreign_keys = ON;');
-
- const existingVersion = _sqliteDb.getFirstSync<{ version: number }>(
- `SELECT name as version FROM sqlite_master WHERE type='table' AND name='schema_version'`
- );
-
- if (!existingVersion) {
- createSchema(_sqliteDb);
- _sqliteDb.execSync(`CREATE TABLE schema_version (version INTEGER PRIMARY KEY);`);
- _sqliteDb.execSync(`INSERT INTO schema_version (version) VALUES (${SCHEMA_VERSION});`);
- } else {
- const versionRow = _sqliteDb.getFirstSync<{ version: number }>(
- `SELECT version FROM schema_version ORDER BY version DESC LIMIT 1`
- );
- const currentVersion = versionRow?.version ?? 0;
-
- if (currentVersion < SCHEMA_VERSION) {
- migrate(_sqliteDb, currentVersion, SCHEMA_VERSION);
- _sqliteDb.execSync(`UPDATE schema_version SET version = ${SCHEMA_VERSION};`);
- }
- }
-
- _db = drizzle(_sqliteDb);
- return _db;
-}
-
-const MIGRATIONS: Array<(db: SQLite.SQLiteDatabase) => void> = [
- // v5: add thumbnail_local column
- (db) => {
- db.execSync(`ALTER TABLE files ADD COLUMN thumbnail_local TEXT;`);
- },
-];
-
-function migrate(db: SQLite.SQLiteDatabase, fromVersion: number, toVersion: number) {
- for (let v = fromVersion; v < toVersion; v++) {
- const migration = MIGRATIONS[v - 1];
- if (migration) migration(db);
- }
-}
-
-function createSchema(db: SQLite.SQLiteDatabase) {
- db.execSync(`
- CREATE TABLE IF NOT EXISTS files (
- id TEXT PRIMARY KEY,
- backend_id TEXT,
- name TEXT NOT NULL,
- mime_type TEXT NOT NULL,
- size INTEGER NOT NULL DEFAULT 0,
- source TEXT NOT NULL DEFAULT 'cloud',
- local_uri TEXT,
- sync_status TEXT NOT NULL DEFAULT 'cloud',
- parent_resource_id TEXT,
- is_folder INTEGER NOT NULL DEFAULT 0,
- ocr_text TEXT,
- thumbnail_url TEXT,
- thumbnail_local TEXT,
- owner_id TEXT,
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL,
- last_synced_at TEXT
- );
- `);
- db.execSync(`CREATE INDEX IF NOT EXISTS idx_files_backend_id ON files(backend_id);`);
- db.execSync(`CREATE INDEX IF NOT EXISTS idx_files_parent_resource_id ON files(parent_resource_id);`);
- db.execSync(`CREATE INDEX IF NOT EXISTS idx_files_source ON files(source);`);
- db.execSync(`CREATE INDEX IF NOT EXISTS idx_files_is_folder ON files(is_folder);`);
- db.execSync(`CREATE INDEX IF NOT EXISTS idx_files_sync_status ON files(sync_status);`);
- db.execSync(`CREATE INDEX IF NOT EXISTS idx_files_owner_id ON files(owner_id);`);
- db.execSync(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY);`);
-
- db.execSync(`
- CREATE TABLE IF NOT EXISTS file_tags (
- id TEXT PRIMARY KEY,
- file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,
- tag_name TEXT NOT NULL
- );
- `);
- db.execSync(`CREATE INDEX IF NOT EXISTS idx_file_tags_file_id ON file_tags(file_id);`);
-
- db.execSync(`
- CREATE TABLE IF NOT EXISTS deleted_files (
- id TEXT PRIMARY KEY,
- deleted_at TEXT NOT NULL
- );
- `);
-
- db.execSync(`
- CREATE TABLE IF NOT EXISTS device_info (
- id TEXT PRIMARY KEY,
- server_id TEXT,
- device_name TEXT NOT NULL DEFAULT '',
- platform TEXT NOT NULL DEFAULT '',
- registered_at TEXT
- );
- `);
-
- db.execSync(`
- CREATE TABLE IF NOT EXISTS pending_actions (
- id TEXT PRIMARY KEY,
- type TEXT NOT NULL,
- payload TEXT NOT NULL,
- status TEXT NOT NULL DEFAULT 'pending',
- attempts INTEGER NOT NULL DEFAULT 0,
- last_error TEXT,
- resource_id TEXT,
- created_at TEXT NOT NULL
- );
- `);
- db.execSync(`CREATE INDEX IF NOT EXISTS idx_pending_actions_status ON pending_actions(status);`);
- db.execSync(`CREATE INDEX IF NOT EXISTS idx_pending_actions_type ON pending_actions(type);`);
-
- db.execSync(`
- CREATE VIRTUAL TABLE IF NOT EXISTS resources_fts USING fts5(
- name,
- ocr_text,
- content='files',
- content_rowid='rowid'
- );
- `);
-
- db.execSync(`
- CREATE TRIGGER IF NOT EXISTS resources_fts_insert AFTER INSERT ON files BEGIN
- INSERT INTO resources_fts(rowid, name, ocr_text) VALUES (new.rowid, new.name, new.ocr_text);
- END;
- `);
- db.execSync(`
- CREATE TRIGGER IF NOT EXISTS resources_fts_delete AFTER DELETE ON files BEGIN
- INSERT INTO resources_fts(resources_fts, rowid, name, ocr_text) VALUES('delete', old.rowid, old.name, old.ocr_text);
- END;
- `);
- db.execSync(`
- CREATE TRIGGER IF NOT EXISTS resources_fts_update AFTER UPDATE ON files BEGIN
- INSERT INTO resources_fts(resources_fts, rowid, name, ocr_text) VALUES('delete', old.rowid, old.name, old.ocr_text);
- INSERT INTO resources_fts(rowid, name, ocr_text) VALUES (new.rowid, new.name, new.ocr_text);
- END;
- `);
-}
-
-function getDb() {
- if (!_db) initDB();
- return _db!;
-}
-
-export type FileRecord = {
- id: string;
- backendId: string | null;
- name: string;
- mimeType: string;
- size: number;
- source: string;
- localUri: string | null;
- syncStatus: string;
- parentResourceId: string | null;
- isFolder: number;
- ocrText: string | null;
- thumbnailUrl: string | null;
- thumbnailLocal?: string | null;
- ownerId: string | null;
- createdAt: string;
- updatedAt: string;
- lastSyncedAt: string | null;
- tags?: Tag[];
-};
-
-type FileRow = {
- id: string;
- backendId: string | null;
- name: string;
- mimeType: string;
- size: number;
- source: string;
- localUri: string | null;
- syncStatus: string;
- parentResourceId: string | null;
- isFolder: number;
- ocrText: string | null;
- thumbnailUrl: string | null;
- thumbnailLocal: string | null;
- ownerId: string | null;
- createdAt: string;
- updatedAt: string;
- lastSyncedAt: string | null;
-};
-
-function rowToRecord(row: FileRow, tags?: Tag[]): FileRecord {
- return {
- id: row.id,
- backendId: row.backendId,
- name: row.name,
- mimeType: row.mimeType,
- size: row.size,
- source: row.source,
- localUri: row.localUri,
- syncStatus: row.syncStatus,
- parentResourceId: row.parentResourceId,
- isFolder: row.isFolder,
- ocrText: row.ocrText,
- thumbnailUrl: row.thumbnailUrl,
- thumbnailLocal: row.thumbnailLocal,
- ownerId: row.ownerId,
- createdAt: row.createdAt,
- updatedAt: row.updatedAt,
- lastSyncedAt: row.lastSyncedAt,
- tags,
- };
-}
-
-function getTagsForFile(fileId: string): Tag[] {
- const d = getDb();
- const rows = d.select().from(fileTags).where(eq(fileTags.fileId, fileId)).all();
- return rows.map((r) => ({ id: r.tagName, tag_name: r.tagName }));
-}
-
-function setTagsForFile(fileId: string, tags: Tag[]) {
- const d = getDb();
- d.delete(fileTags).where(eq(fileTags.fileId, fileId)).run();
- if (tags.length === 0) return;
- d.insert(fileTags).values(
- tags.map((t) => ({
- id: `${fileId}_${t.id || t.tag_name}`,
- fileId,
- tagName: t.tag_name,
- })),
- ).run();
-}
-
-function upsertRow(file: FileRecord) {
- const d = getDb();
- d.insert(files).values({
- id: file.id,
- backendId: file.backendId,
- name: file.name,
- mimeType: file.mimeType,
- size: file.size,
- source: file.source,
- localUri: file.localUri,
- syncStatus: file.syncStatus,
- parentResourceId: file.parentResourceId,
- isFolder: file.isFolder,
- ocrText: file.ocrText,
- thumbnailUrl: file.thumbnailUrl,
- thumbnailLocal: file.thumbnailLocal ?? null,
- ownerId: file.ownerId,
- createdAt: file.createdAt,
- updatedAt: file.updatedAt,
- lastSyncedAt: file.lastSyncedAt,
- }).onConflictDoUpdate({
- target: files.id,
- set: {
- backendId: file.backendId,
- name: file.name,
- mimeType: file.mimeType,
- size: file.size,
- source: file.source,
- localUri: file.localUri,
- syncStatus: file.syncStatus,
- parentResourceId: file.parentResourceId,
- isFolder: file.isFolder,
- ocrText: file.ocrText,
- thumbnailUrl: file.thumbnailUrl,
- thumbnailLocal: file.thumbnailLocal ?? null,
- ownerId: file.ownerId,
- updatedAt: file.updatedAt,
- lastSyncedAt: file.lastSyncedAt,
- },
- }).run();
-}
-
-export const fileStore = {
- initDB,
-
- upsert(file: FileRecord) {
- upsertRow(file);
- if (file.tags) setTagsForFile(file.id, file.tags);
- },
-
- upsertBatch(fileList: FileRecord[]) {
- const d = getDb();
- for (const file of fileList) {
- upsertRow(file);
- if (file.tags) setTagsForFile(file.id, file.tags);
- }
- },
-
- getById(id: string): FileRecord | null {
- const d = getDb();
- const row = d.select().from(files).where(eq(files.id, id)).get() as FileRow | undefined;
- if (!row) return null;
- return rowToRecord(row, getTagsForFile(id));
- },
-
- getByBackendId(backendId: string): FileRecord | null {
- const d = getDb();
- const row = d.select().from(files).where(eq(files.backendId, backendId)).get() as FileRow | undefined;
- if (!row) return null;
- return rowToRecord(row, getTagsForFile(row.id));
- },
-
- getByLocalUri(localUri: string): FileRecord | null {
- const d = getDb();
- const row = d.select().from(files).where(eq(files.localUri, localUri)).get() as FileRow | undefined;
- if (!row) return null;
- return rowToRecord(row, getTagsForFile(row.id));
- },
-
- getRootFolders(): FileRecord[] {
- const d = getDb();
- const rows = d.select().from(files)
- .where(and(eq(files.isFolder, 1), isNull(files.parentResourceId)))
- .orderBy(asc(files.name))
- .all() as FileRow[];
- return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
- },
-
- getChildrenByParent(parentId: string): FileRecord[] {
- const d = getDb();
- const rows = d.select().from(files)
- .where(eq(files.parentResourceId, parentId))
- .orderBy(desc(files.isFolder), desc(files.createdAt))
- .all() as FileRow[];
- return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
- },
-
- getRootFiles(): { files: FileRecord[]; total: number } {
- const d = getDb();
- const countRow = d.select({ count: sql`count(*)` })
- .from(files)
- .where(isNull(files.parentResourceId))
- .get();
- const total = countRow?.count ?? 0;
- const rows = d.select().from(files)
- .where(isNull(files.parentResourceId))
- .orderBy(desc(files.isFolder), desc(files.createdAt))
- .all() as FileRow[];
- return {
- files: rows.map((r) => rowToRecord(r, getTagsForFile(r.id))),
- total,
- };
- },
-
- getPaginated(page: number, limit: number): { files: FileRecord[]; total: number } {
- const d = getDb();
- const offset = (page - 1) * limit;
-
- const countRow = d.select({ count: sql`count(*)` })
- .from(files)
- .where(isNull(files.parentResourceId))
- .get();
- const total = countRow?.count ?? 0;
-
- const rows = d.select().from(files)
- .where(isNull(files.parentResourceId))
- .orderBy(desc(files.isFolder), desc(files.createdAt))
- .limit(limit)
- .offset(offset)
- .all() as FileRow[];
-
- return {
- files: rows.map((r) => rowToRecord(r, getTagsForFile(r.id))),
- total,
- };
- },
-
- getAllFolders(): FileRecord[] {
- const d = getDb();
- const rows = d.select().from(files)
- .where(eq(files.isFolder, 1))
- .orderBy(asc(files.name))
- .all() as FileRow[];
- return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
- },
-
- searchFts(query: string): FileRecord[] {
- const d = getDb();
- const sanitized = query.replace(/['"]/g, '').trim();
- if (!sanitized) return [];
- const ftsPattern = sanitized.split(/\s+/).map(w => `"${w}"`).join(' OR ');
- const sqlQuery = `
- SELECT f.* FROM files f
- JOIN resources_fts r ON r.rowid = f.rowid
- WHERE resources_fts MATCH ?
- ORDER BY rank
- LIMIT 100
- `;
- const sqliteDb = _sqliteDb!;
- const stmt = sqliteDb.prepareSync(sqlQuery);
- const result = stmt.executeSync(ftsPattern);
- const rows: FileRow[] = [];
- for (const r of result) {
- rows.push(r as unknown as FileRow);
- }
- return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
- },
-
- search(query: string): FileRecord[] {
- const d = getDb();
- const pattern = `%${query}%`;
- const rows = d.select().from(files)
- .where(or(like(files.name, pattern), like(files.ocrText, pattern)))
- .orderBy(desc(files.createdAt))
- .limit(100)
- .all() as FileRow[];
- return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
- },
-
- mergeFromBackend(backendFiles: Array<{
- id: string;
- name: string;
- mimeType: string;
- size: number;
- createdAt: string;
- updatedAt?: string;
- ocrText?: string;
- tags?: Tag[];
- isFolder: boolean;
- parentResourceId?: string;
- thumbnailUrl?: string;
- ownerId?: string;
- }>) {
- const d = getDb();
- const now = new Date().toISOString();
-
- d.transaction(() => {
- for (const bf of backendFiles) {
- const existing = d.select().from(files).where(eq(files.backendId, bf.id)).get() as FileRow | undefined;
-
- const recordId = existing?.id ?? bf.id;
-
- const source = existing && existing.localUri ? 'synced' : 'cloud';
- const syncStatus = existing && existing.localUri
- ? (existing.syncStatus === 'cloud' ? 'synced' : existing.syncStatus)
- : 'cloud';
-
- upsertRow({
- id: recordId,
- backendId: bf.id,
- name: bf.name,
- mimeType: bf.mimeType,
- size: bf.size,
- source,
- localUri: existing?.localUri ?? null,
- syncStatus,
- parentResourceId: bf.parentResourceId ?? null,
- isFolder: bf.isFolder ? 1 : 0,
- ocrText: bf.ocrText ?? null,
- thumbnailUrl: bf.thumbnailUrl ?? null,
- thumbnailLocal: existing?.thumbnailLocal ?? null,
- ownerId: bf.ownerId ?? null,
- createdAt: bf.createdAt,
- updatedAt: bf.updatedAt ?? now,
- lastSyncedAt: now,
- });
-
- if (bf.tags && bf.tags.length > 0) setTagsForFile(recordId, bf.tags);
- }
- });
- },
-
- mergeFromDevice(deviceFiles: Array<{
- id: string;
- uri: string;
- name: string;
- mimeType: string;
- size: number;
- createdAt: string;
- folderId?: string;
- }>) {
- const d = getDb();
- const now = new Date().toISOString();
-
- d.transaction(() => {
- for (const df of deviceFiles) {
- if (this.isDeleted(df.id)) continue;
- const existing = d.select().from(files).where(eq(files.id, df.id)).get();
- if (existing) continue;
-
- upsertRow({
- id: df.id,
- backendId: null,
- name: df.name,
- mimeType: df.mimeType,
- size: df.size,
- source: 'local',
- localUri: df.uri,
- syncStatus: 'local',
- parentResourceId: df.folderId ?? null,
- isFolder: 0,
- ocrText: null,
- thumbnailUrl: null,
- thumbnailLocal: null,
- ownerId: null,
- createdAt: df.createdAt,
- updatedAt: now,
- lastSyncedAt: null,
- });
- }
- });
- },
-
- updatePartial(id: string, updates: Partial) {
- const d = getDb();
- const setFields: Record = {};
- if (updates.backendId !== undefined) setFields.backendId = updates.backendId;
- if (updates.syncStatus !== undefined) setFields.syncStatus = updates.syncStatus;
- if (updates.localUri !== undefined) setFields.localUri = updates.localUri;
- if (updates.source !== undefined) setFields.source = updates.source;
- if (updates.thumbnailUrl !== undefined) setFields.thumbnailUrl = updates.thumbnailUrl;
- if (updates.thumbnailLocal !== undefined) setFields.thumbnailLocal = updates.thumbnailLocal;
- if (updates.ocrText !== undefined) setFields.ocrText = updates.ocrText;
- if (updates.parentResourceId !== undefined) setFields.parentResourceId = updates.parentResourceId;
- if (updates.name !== undefined) setFields.name = updates.name;
- if (updates.ownerId !== undefined) setFields.ownerId = updates.ownerId;
- setFields.updatedAt = new Date().toISOString();
-
- d.update(files).set(setFields).where(eq(files.id, id)).run();
- },
-
- updateSyncStatus(id: string, syncStatus: string) {
- this.updatePartial(id, { syncStatus });
- },
-
- markAsCloudOnly(id: string) {
- this.updatePartial(id, { syncStatus: 'cloud', localUri: null, source: 'cloud' });
- },
-
- setThumbnailUrl(backendId: string, thumbnailUrl: string) {
- const d = getDb();
- d.update(files).set({ thumbnailUrl, updatedAt: new Date().toISOString() })
- .where(eq(files.backendId, backendId)).run();
- },
-
- setThumbnailLocal(id: string, thumbnailLocal: string) {
- const d = getDb();
- d.update(files).set({ thumbnailLocal, updatedAt: new Date().toISOString() })
- .where(eq(files.id, id)).run();
- },
-
- markDeleted(id: string) {
- const d = getDb();
- d.insert(deletedFiles).values({ id, deletedAt: new Date().toISOString() })
- .onConflictDoUpdate({ target: deletedFiles.id, set: { deletedAt: new Date().toISOString() } })
- .run();
- },
-
- isDeleted(id: string): boolean {
- const d = getDb();
- const row = d.select().from(deletedFiles).where(eq(deletedFiles.id, id)).get();
- return !!row;
- },
-
- deleteById(id: string) {
- const d = getDb();
- this.markDeleted(id);
- d.delete(files).where(eq(files.id, id)).run();
- },
-
- deleteByBackendId(backendId: string) {
- const d = getDb();
- const row = d.select().from(files).where(eq(files.backendId, backendId)).get() as FileRow | undefined;
- if (row) this.markDeleted(row.id);
- d.delete(files).where(eq(files.backendId, backendId)).run();
- },
-
- clear() {
- const d = getDb();
- d.delete(fileTags).run();
- d.delete(files).run();
- },
-
- count(): number {
- const d = getDb();
- const row = d.select({ count: sql`count(*)` }).from(files).get();
- return row?.count ?? 0;
- },
-
- countPendingSync(): number {
- const d = getDb();
- const row = d.select({ count: sql`count(*)` }).from(files).where(
- and(
- or(eq(files.source, 'local'), eq(files.source, 'synced')),
- isNull(files.backendId),
- isNotNull(files.localUri),
- inArray(files.syncStatus, ['local', 'error']),
- )
- ).get();
- return row?.count ?? 0;
- },
-
- getAllLocal(): FileRecord[] {
- const d = getDb();
- const rows = d.select().from(files)
- .where(or(eq(files.source, 'local'), eq(files.source, 'synced')))
- .all() as FileRow[];
- return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
- },
-
- getLocalDeviceFiles(): FileRecord[] {
- const d = getDb();
- const rows = d.select().from(files)
- .where(and(eq(files.source, 'local'), isNull(files.backendId)))
- .orderBy(desc(files.createdAt))
- .all() as FileRow[];
- return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
- },
-
- getAllSynced(): FileRecord[] {
- const d = getDb();
- const rows = d.select().from(files)
- .where(eq(files.source, 'synced'))
- .all() as FileRow[];
- return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
- },
-
- getPendingSync(): FileRecord[] {
- const d = getDb();
- const rows = d.select().from(files)
- .where(and(eq(files.syncStatus, 'local'), sql`${files.backendId} IS NULL`))
- .all() as FileRow[];
- return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
- },
-
- getErrorFiles(): FileRecord[] {
- const d = getDb();
- const rows = d.select().from(files)
- .where(eq(files.syncStatus, 'error'))
- .all() as FileRow[];
- return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
- },
-
- resetSyncError(id: string) {
- this.updatePartial(id, { syncStatus: 'local' });
- },
-
- // --- Pending Actions ---
-
- insertPendingAction(action: PendingAction) {
- const d = getDb();
- d.insert(pendingActions).values({
- id: action.id,
- type: action.type,
- payload: JSON.stringify(action.payload),
- status: action.status,
- attempts: action.attempts,
- lastError: action.lastError,
- resourceId: action.resourceId,
- createdAt: action.createdAt,
- }).run();
- },
-
- getPendingActions(): PendingAction[] {
- const d = getDb();
- const rows = d.select().from(pendingActions)
- .where(eq(pendingActions.status, 'pending'))
- .orderBy(asc(pendingActions.createdAt))
- .all();
- return rows.map(rowToAction);
- },
-
- getPendingActionById(id: string): PendingAction | null {
- const d = getDb();
- const row = d.select().from(pendingActions)
- .where(eq(pendingActions.id, id))
- .get();
- return row ? rowToAction(row) : null;
- },
-
- getPendingActionsCount(): number {
- const d = getDb();
- const row = d.select({ count: sql`count(*)` })
- .from(pendingActions)
- .where(eq(pendingActions.status, 'pending'))
- .get();
- return row?.count ?? 0;
- },
-
- markPendingActionDone(id: string) {
- const d = getDb();
- d.update(pendingActions)
- .set({ status: 'done' })
- .where(eq(pendingActions.id, id))
- .run();
- },
-
- markPendingActionError(id: string, error: string) {
- const d = getDb();
- const row = d.select().from(pendingActions)
- .where(eq(pendingActions.id, id))
- .get();
- if (!row) return;
- d.update(pendingActions)
- .set({
- status: 'error',
- attempts: row.attempts + 1,
- lastError: error,
- })
- .where(eq(pendingActions.id, id))
- .run();
- },
-
- markPendingActionObsolete(id: string) {
- const d = getDb();
- d.update(pendingActions)
- .set({ status: 'obsolete' })
- .where(eq(pendingActions.id, id))
- .run();
- },
-
- deletePendingAction(id: string) {
- const d = getDb();
- d.delete(pendingActions).where(eq(pendingActions.id, id)).run();
- },
-
- clearDonePendingActions() {
- const d = getDb();
- d.delete(pendingActions).where(eq(pendingActions.status, 'done')).run();
- d.delete(pendingActions).where(eq(pendingActions.status, 'obsolete')).run();
- },
-};
-
-function rowToAction(row: {
- id: string;
- type: string;
- payload: string;
- status: string;
- attempts: number;
- lastError: string | null;
- resourceId: string | null;
- createdAt: string;
-}): PendingAction {
- return {
- id: row.id,
- type: row.type as PendingActionType,
- payload: JSON.parse(row.payload),
- status: row.status as PendingActionStatus,
- attempts: row.attempts,
- lastError: row.lastError,
- resourceId: row.resourceId,
- createdAt: row.createdAt,
- };
-}
diff --git a/mobile/services/fileStore/migrate.ts b/mobile/services/fileStore/migrate.ts
deleted file mode 100644
index 38cee24..0000000
--- a/mobile/services/fileStore/migrate.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { initDB } from './index';
-
-export function migrateFromLegacy() {
- initDB();
-}
diff --git a/mobile/services/fileStore/schema.ts b/mobile/services/fileStore/schema.ts
deleted file mode 100644
index c442725..0000000
--- a/mobile/services/fileStore/schema.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core';
-
-export const files = sqliteTable(
- 'files',
- {
- id: text('id').primaryKey(),
- backendId: text('backend_id'),
- name: text('name').notNull(),
- mimeType: text('mime_type').notNull(),
- size: integer('size').notNull(),
- source: text('source').notNull().default('cloud'),
- localUri: text('local_uri'),
- syncStatus: text('sync_status').notNull().default('cloud'),
- parentResourceId: text('parent_resource_id'),
- isFolder: integer('is_folder').notNull().default(0),
- ocrText: text('ocr_text'),
- thumbnailUrl: text('thumbnail_url'),
- thumbnailLocal: text('thumbnail_local'),
- ownerId: text('owner_id'),
- createdAt: text('created_at').notNull(),
- updatedAt: text('updated_at').notNull(),
- lastSyncedAt: text('last_synced_at'),
- },
- (t) => [
- index('idx_files_backend_id').on(t.backendId),
- index('idx_files_parent_resource_id').on(t.parentResourceId),
- index('idx_files_source').on(t.source),
- index('idx_files_is_folder').on(t.isFolder),
- index('idx_files_sync_status').on(t.syncStatus),
- index('idx_files_owner_id').on(t.ownerId),
- ],
-);
-
-export const fileTags = sqliteTable(
- 'file_tags',
- {
- id: text('id').primaryKey(),
- fileId: text('file_id')
- .notNull()
- .references(() => files.id, { onDelete: 'cascade' }),
- tagName: text('tag_name').notNull(),
- },
- (t) => [index('idx_file_tags_file_id').on(t.fileId)],
-);
-
-export const deletedFiles = sqliteTable('deleted_files', {
- id: text('id').primaryKey(),
- deletedAt: text('deleted_at').notNull(),
-});
-
-export const deviceInfo = sqliteTable('device_info', {
- id: text('id').primaryKey(),
- serverId: text('server_id'),
- deviceName: text('device_name').notNull().default(''),
- platform: text('platform').notNull().default(''),
- registeredAt: text('registered_at'),
-});
-
-export const pendingActions = sqliteTable(
- 'pending_actions',
- {
- id: text('id').primaryKey(),
- type: text('type').notNull(),
- payload: text('payload').notNull(),
- status: text('status').notNull().default('pending'),
- attempts: integer('attempts').notNull().default(0),
- lastError: text('last_error'),
- resourceId: text('resource_id'),
- createdAt: text('created_at').notNull(),
- },
- (t) => [
- index('idx_pending_actions_status').on(t.status),
- index('idx_pending_actions_type').on(t.type),
- ],
-);
diff --git a/mobile/services/mmkvPersister.ts b/mobile/services/mmkvPersister.ts
deleted file mode 100644
index f552603..0000000
--- a/mobile/services/mmkvPersister.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { createMMKV } from 'react-native-mmkv';
-import { PersistedClient, Persister } from '@tanstack/react-query-persist-client';
-
-const storage = createMMKV({ id: 'vaultdrop-query-cache' });
-
-export function createMMKVPersister(): Persister {
- return {
- persistClient: async (client: PersistedClient) => {
- storage.set('query-cache', JSON.stringify(client));
- },
- restoreClient: async () => {
- const raw = storage.getString('query-cache');
- if (!raw) return undefined;
- return JSON.parse(raw) as PersistedClient;
- },
- removeClient: async () => {
- storage.remove('query-cache');
- },
- };
-}
diff --git a/mobile/services/onboardingStorage.ts b/mobile/services/onboardingStorage.ts
deleted file mode 100644
index 2984dd1..0000000
--- a/mobile/services/onboardingStorage.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { createMMKV } from 'react-native-mmkv';
-import { ONBOARDING_STEPS, CURRENT_ONBOARDING_VERSION, type OnboardingStep } from '../config/onboarding';
-import { safDirectory } from './safDirectory';
-
-const storage = createMMKV({ id: 'vaultdrop-onboarding' });
-
-const COMPLETED_VERSION_KEY = 'completed_version';
-const SEEN_STEPS_KEY = 'seen_steps';
-
-export const onboardingStorage = {
- getCompletedVersion(): number | undefined {
- const raw = storage.getNumber(COMPLETED_VERSION_KEY);
- return raw != null ? raw : undefined;
- },
-
- setCompletedVersion(version: number) {
- storage.set(COMPLETED_VERSION_KEY, version);
- },
-
- getSeenSteps(): string[] {
- const raw = storage.getString(SEEN_STEPS_KEY);
- if (!raw) return [];
- return JSON.parse(raw) as string[];
- },
-
- markStepSeen(stepId: string) {
- const seen = this.getSeenSteps();
- if (!seen.includes(stepId)) {
- seen.push(stepId);
- storage.set(SEEN_STEPS_KEY, JSON.stringify(seen));
- }
- },
-
- getPendingSteps(): OnboardingStep[] {
- const lastVersion = this.getCompletedVersion();
- const seenIds = this.getSeenSteps();
- const folders = safDirectory.getAll();
-
- return ONBOARDING_STEPS.filter((step) => {
- if (lastVersion != null && step.version <= lastVersion) return false;
- if (seenIds.includes(step.id)) return false;
- if (step.condition === 'has_no_folders' && folders.length > 0) return false;
- return true;
- });
- },
-
- needsOnboarding(): boolean {
- return this.getPendingSteps().length > 0;
- },
-
- reset() {
- storage.remove(COMPLETED_VERSION_KEY);
- storage.remove(SEEN_STEPS_KEY);
- },
-};
diff --git a/mobile/services/safDirectory.ts b/mobile/services/safDirectory.ts
deleted file mode 100644
index 4283d49..0000000
--- a/mobile/services/safDirectory.ts
+++ /dev/null
@@ -1,179 +0,0 @@
-import { createMMKV } from 'react-native-mmkv';
-
-export type SyncMode = 'none' | 'manual' | 'auto';
-export type SyncGlobalMode = 'off' | 'auto' | 'manual';
-export type FolderSource = 'saf' | 'media-library' | 'recursive';
-
-export type StoredFolder = {
- id: string;
- uri: string;
- name: string;
- visible: boolean;
- syncMode: SyncMode;
- syncCellular: boolean;
- source: FolderSource;
- albumId?: string;
- parentUri?: string;
-};
-
-const storage = createMMKV({ id: 'vaultdrop-saf' });
-
-const FOLDERS_KEY = 'saf_folders';
-const SYNC_GLOBAL_MODE_KEY = 'sync_global_mode';
-const SYNC_GLOBAL_CELLULAR_KEY = 'sync_global_cellular';
-const DISCOVERED_KEY = 'folders_discovered';
-
-function generateId(): string {
- return `folder_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
-}
-
-function getAllRaw(): StoredFolder[] {
- const raw = storage.getString(FOLDERS_KEY);
- if (!raw) return [];
- const parsed = JSON.parse(raw) as StoredFolder[];
- let migrated = false;
- for (const f of parsed) {
- if (f.syncMode === undefined) {
- (f as any).syncMode = 'none';
- migrated = true;
- }
- if (f.syncCellular === undefined) {
- (f as any).syncCellular = false;
- migrated = true;
- }
- if (f.source === undefined) {
- (f as any).source = 'saf';
- migrated = true;
- }
- }
- if (migrated) saveAll(parsed);
- return parsed;
-}
-
-function saveAll(folders: StoredFolder[]) {
- storage.set(FOLDERS_KEY, JSON.stringify(folders));
-}
-
-export const safDirectory = {
- getAll(): StoredFolder[] {
- return getAllRaw();
- },
-
- getVisibleFolders(): StoredFolder[] {
- return getAllRaw().filter((f) => f.visible);
- },
-
- addFolder(uri: string, name: string): StoredFolder {
- const folders = getAllRaw();
- if (folders.some((f) => f.uri === uri)) {
- return folders.find((f) => f.uri === uri)!;
- }
- const folder: StoredFolder = { id: generateId(), uri, name, visible: true, syncMode: 'none', syncCellular: false, source: 'saf' };
- folders.push(folder);
- saveAll(folders);
- return folder;
- },
-
- addMediaFolder(albumId: string, name: string, uri: string): StoredFolder {
- const folders = getAllRaw();
- const key = `media://${albumId}`;
- if (folders.some((f) => f.uri === key)) {
- return folders.find((f) => f.uri === key)!;
- }
- const folder: StoredFolder = {
- id: generateId(),
- uri: key,
- name,
- visible: true,
- syncMode: 'none',
- syncCellular: false,
- source: 'media-library',
- albumId,
- };
- folders.push(folder);
- saveAll(folders);
- return folder;
- },
-
- addBatchFolders(folders: Array<{ uri: string; name: string; source?: FolderSource; parentUri?: string }>): StoredFolder[] {
- const current = getAllRaw();
- const added: StoredFolder[] = [];
- for (const f of folders) {
- if (current.some((existing) => existing.uri === f.uri)) continue;
- const folder: StoredFolder = {
- id: generateId(),
- uri: f.uri,
- name: f.name,
- visible: true,
- syncMode: 'none',
- syncCellular: false,
- source: f.source ?? 'recursive',
- parentUri: f.parentUri,
- };
- current.push(folder);
- added.push(folder);
- }
- saveAll(current);
- return added;
- },
-
- getDiscovered(): boolean {
- return storage.getString(DISCOVERED_KEY) === 'true';
- },
-
- setDiscovered() {
- storage.set(DISCOVERED_KEY, 'true');
- },
-
- resetDiscovered() {
- storage.remove(DISCOVERED_KEY);
- },
-
- removeFolder(id: string) {
- const folders = getAllRaw().filter((f) => f.id !== id);
- saveAll(folders);
- },
-
- toggleVisibility(id: string) {
- const folders = getAllRaw().map((f) =>
- f.id === id ? { ...f, visible: !f.visible } : f
- );
- saveAll(folders);
- },
-
- updateSyncMode(id: string, syncMode: SyncMode) {
- const folders = getAllRaw().map((f) =>
- f.id === id ? { ...f, syncMode } : f
- );
- saveAll(folders);
- },
-
- updateSyncCellular(id: string, syncCellular: boolean) {
- const folders = getAllRaw().map((f) =>
- f.id === id ? { ...f, syncCellular } : f
- );
- saveAll(folders);
- },
-
- getGlobalSyncMode(): SyncGlobalMode {
- const raw = storage.getString(SYNC_GLOBAL_MODE_KEY);
- if (raw === 'auto' || raw === 'manual') return raw;
- return 'off';
- },
-
- setGlobalSyncMode(mode: SyncGlobalMode) {
- storage.set(SYNC_GLOBAL_MODE_KEY, mode);
- },
-
- getGlobalSyncCellular(): boolean {
- return storage.getString(SYNC_GLOBAL_CELLULAR_KEY) === 'true';
- },
-
- setGlobalSyncCellular(enabled: boolean) {
- storage.set(SYNC_GLOBAL_CELLULAR_KEY, enabled ? 'true' : 'false');
- },
-
- clear() {
- storage.remove(FOLDERS_KEY);
- },
-};
diff --git a/mobile/services/thumbnail.ts b/mobile/services/thumbnail.ts
deleted file mode 100644
index c53e252..0000000
--- a/mobile/services/thumbnail.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { manipulateAsync, SaveFormat } from 'expo-image-manipulator';
-
-const THUMB_SIZE = 128;
-
-function isGeneratableImage(mimeType: string): boolean {
- return (mimeType ?? '').toLowerCase().startsWith('image/');
-}
-
-export async function generateLocalThumbnail(
- uri: string | undefined,
- mimeType: string,
-): Promise {
- if (!uri || !isGeneratableImage(mimeType)) return null;
- try {
- const result = await manipulateAsync(
- uri,
- [{ resize: { width: THUMB_SIZE } }],
- { format: SaveFormat.JPEG, compress: 0.7, base64: true },
- );
- if (!result.base64) return null;
- return `data:image/jpeg;base64,${result.base64}`;
- } catch {
- return null;
- }
-}
diff --git a/mobile/services/uploadQueue.ts b/mobile/services/uploadQueue.ts
deleted file mode 100644
index a7677c1..0000000
--- a/mobile/services/uploadQueue.ts
+++ /dev/null
@@ -1,343 +0,0 @@
-import { File, UploadType } from 'expo-file-system';
-import { createMMKV } from 'react-native-mmkv';
-import { apiClient } from '../api/client';
-import { API_BASE_URL, ENDPOINTS } from '../constants/api';
-import { ApiError, UploadError } from '../types';
-import { fileStore } from './fileStore';
-
-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 const activeUploadUris = new Set();
-
-export type UploadTaskStatus = 'pending' | 'uploading' | 'done' | 'error';
-
-export type UploadTask = {
- id: string;
- file: UploadFile;
- status: UploadTaskStatus;
- progress: number;
- result?: UploadResult;
- error?: string;
- retryCount: number;
- createdAt: number;
- updatedAt: number;
-};
-
-type Listener = () => void;
-
-const storage = createMMKV({ id: 'vaultdrop-upload-queue' });
-const STORAGE_KEY = 'tasks';
-
-function serialize(task: UploadTask): unknown {
- return {
- id: task.id,
- file: task.file,
- status: task.status,
- progress: task.progress,
- result: task.result ?? null,
- error: task.error ?? null,
- retryCount: task.retryCount,
- createdAt: task.createdAt,
- updatedAt: task.updatedAt,
- };
-}
-
-function deserialize(data: unknown): UploadTask | null {
- const d = data as Record;
- if (!d || !d.id || !d.file) return null;
- const file = d.file as Record;
- if (!file.uri || !file.type || !file.name) return null;
- return {
- id: d.id as string,
- file: { uri: file.uri, type: file.type, name: file.name },
- status: d.status as UploadTaskStatus,
- 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,
- };
-}
-
-function saveTasks(tasks: UploadTask[]) {
- const persistable = tasks
- .filter((t) => t.status !== 'done')
- .map(serialize);
- storage.set(STORAGE_KEY, JSON.stringify(persistable));
-}
-
-function loadTasks(): UploadTask[] {
- try {
- const raw = storage.getString(STORAGE_KEY);
- if (!raw) return [];
- const parsed: unknown[] = JSON.parse(raw);
- if (!Array.isArray(parsed)) return [];
- const tasks: UploadTask[] = [];
- let maxId = 0;
- for (const item of parsed) {
- const t = deserialize(item);
- if (t) {
- tasks.push(t);
- const num = parseInt(t.id.replace('upload_', ''), 10);
- if (num > maxId) maxId = num;
- }
- }
- nextId = maxId;
- return tasks;
- } catch {
- return [];
- }
-}
-
-let nextId = 0;
-function genId() {
- nextId++;
- return `upload_${Date.now()}_${nextId}`;
-}
-
-class UploadQueue {
- private tasks: UploadTask[] = [];
- private listeners = new Set();
- private concurrency = 3;
- private active = 0;
- private cleanupTimer: ReturnType | null = null;
-
- constructor() {
- this.tasks = loadTasks();
- const pendingExist = this.tasks.some(
- (t) => t.status === 'pending' || t.status === 'uploading',
- );
- if (pendingExist) {
- setTimeout(() => {
- this.restartUploading();
- this.processNext();
- }, 0);
- }
- }
-
- private restartUploading() {
- for (const task of this.tasks) {
- if (task.status === 'uploading') {
- task.status = 'pending';
- task.progress = 0;
- task.updatedAt = Date.now();
- }
- }
- this.persist();
- }
-
- getTasks(): UploadTask[] {
- return this.tasks;
- }
-
- subscribe(listener: Listener): () => void {
- this.listeners.add(listener);
- return () => this.listeners.delete(listener);
- }
-
- private notify() {
- this.listeners.forEach((l) => l());
- }
-
- private persist() {
- saveTasks(this.tasks);
- }
-
- enqueue(files: UploadFile[]) {
- const now = Date.now();
- for (const file of files) {
- this.tasks.push({
- id: genId(),
- file,
- status: 'pending',
- progress: 0,
- retryCount: 0,
- createdAt: now,
- updatedAt: now,
- });
- }
- this.persist();
- this.notify();
- this.processNext();
- }
-
- cancel(id: string) {
- const task = this.tasks.find((t) => t.id === id);
- if (!task || task.status === 'done') return;
- task.status = 'error';
- task.error = 'Annulé';
- task.updatedAt = Date.now();
- this.persist();
- this.notify();
- }
-
- retry(id: string) {
- const task = this.tasks.find((t) => t.id === id);
- 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();
- this.persist();
- this.notify();
- this.processNext();
- }
-
- retryAll() {
- for (const task of this.tasks) {
- if (task.status === 'error') {
- task.status = 'pending';
- task.progress = 0;
- task.retryCount = 0;
- task.error = undefined;
- task.result = undefined;
- task.updatedAt = Date.now();
- }
- }
- this.persist();
- this.notify();
- this.processNext();
- }
-
- getPendingCount(): number {
- return this.tasks.filter((t) => t.status === 'pending' || t.status === 'uploading').length;
- }
-
- private processNext() {
- while (this.active < this.concurrency) {
- const next = this.tasks.find((t) => t.status === 'pending');
- if (!next) break;
- this.active++;
- next.status = 'uploading';
- next.updatedAt = Date.now();
- this.persist();
- this.notify();
- this.runTask(next);
- }
- }
-
- private async runTask(task: UploadTask) {
- let willRetry = false;
- activeUploadUris.add(task.file.uri);
- try {
- const fsFile = new File(task.file.uri);
- const headers: Record = {};
- const token = apiClient.getAccessToken();
- if (token) {
- headers['Authorization'] = `Bearer ${token}`;
- }
-
- const result = await fsFile.upload(`${API_BASE_URL}${ENDPOINTS.UPLOAD}`, {
- httpMethod: 'POST',
- uploadType: UploadType.MULTIPART,
- fieldName: 'file',
- mimeType: task.file.type,
- headers,
- onProgress: (progress) => {
- if (progress.totalBytes > 0) {
- task.progress = Math.round((progress.bytesSent / progress.totalBytes) * 100);
- this.notify();
- }
- },
- });
-
- if (result.status >= 400) {
- let serverMessage = 'Erreur serveur';
- try {
- const body: ApiError = JSON.parse(result.body);
- serverMessage = body.error?.message || serverMessage;
- } catch {
- serverMessage = result.body || serverMessage;
- }
- throw new UploadError(task.file.name, result.status, serverMessage);
- }
-
- const body = JSON.parse(result.body);
- const items = body.data ?? body;
- const item = Array.isArray(items) ? items[0] : items;
-
- task.status = 'done';
- task.progress = 100;
- task.result = item as UploadResult;
- task.updatedAt = Date.now();
- this.linkResultToStore(task);
- this.persist();
- this.notify();
- this.scheduleCleanup();
- } catch (err) {
- 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--;
- activeUploadUris.delete(task.file.uri);
- this.notify();
- if (!willRetry) {
- this.processNext();
- }
- }
- }
-
- private linkResultToStore(task: UploadTask) {
- try {
- const backendId = task.result?.id;
- if (!backendId) return;
- const entry = fileStore.getByLocalUri(task.file.uri);
- if (!entry || entry.backendId) return;
- fileStore.updatePartial(entry.id, {
- backendId,
- syncStatus: 'synced',
- source: 'synced',
- });
- } catch {}
- }
-
- private scheduleCleanup() {
- if (this.cleanupTimer) return;
- this.cleanupTimer = setTimeout(() => {
- this.cleanupTimer = null;
- const now = Date.now();
- const before = this.tasks.length;
- this.tasks = this.tasks.filter(
- (t) => t.status !== 'done' || now - t.updatedAt < 5000,
- );
- if (this.tasks.length !== before) {
- this.persist();
- this.notify();
- }
- if (this.tasks.some((t) => t.status === 'done')) {
- this.scheduleCleanup();
- }
- }, 6000);
- }
-}
-
-export const uploadQueue = new UploadQueue();
diff --git a/mobile/types/index.ts b/mobile/types/index.ts
deleted file mode 100644
index 2b4f2d9..0000000
--- a/mobile/types/index.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-export interface Variant {
- id: string;
- pageNumber: number;
- variantType: string;
- width: number;
- height: number;
- url: string;
- mimeType: string;
-}
-
-export interface UnifiedFileItem {
- id: string;
- backendResourceId?: string;
- name: string;
- mimeType: string;
- size: number;
- createdAt: string;
- updatedAt?: string;
- source: 'cloud' | 'local' | 'synced';
- syncStatus: SyncStatus;
- localUri?: string;
- ocrText?: string;
- tags: Tag[];
- isFolder: boolean;
- parentResourceId?: string;
- ownerId?: string;
- url?: string;
- thumbnailUrl?: string;
- thumbnailLocal?: string;
- variants?: Variant[];
- isDeviceFile?: boolean;
- isUploading?: boolean;
- uploadProgress?: number;
- uploadStatus?: 'pending' | 'uploading' | 'done' | 'error';
-}
-
-export type FileItem = UnifiedFileItem;
-
-export function isFolder(file: UnifiedFileItem | { isFolder: boolean }): boolean {
- return file.isFolder;
-}
-
-export interface Tag {
- id: string;
- tag_name: string;
-}
-
-export interface OcrJob {
- id: string;
- fileId: string;
- status: 'pending' | 'processing' | 'completed' | 'failed';
- result?: string;
- createdAt: string;
- completedAt?: string;
-}
-
-export interface PaginatedResponse {
- data: T[];
- meta: {
- page: number;
- total: number;
- };
-}
-
-export interface ApiError {
- error: {
- code: string;
- message: string;
- };
-}
-
-export class HttpError extends Error {
- status: number;
- code?: string;
-
- constructor(status: number, message: string, code?: string) {
- super(message);
- this.name = 'HttpError';
- this.status = status;
- this.code = code;
- }
-}
-
-export class UploadError extends HttpError {
- fileName: string;
-
- constructor(fileName: string, status: number, message: string, code?: string) {
- super(status, message, code);
- this.name = 'UploadError';
- this.fileName = fileName;
- }
-}
-
-export type CapturedPhoto = {
- id: string;
- filePath: string;
- uri: string;
- uploadedId?: string;
- uploadedAt?: string;
-};
-
-export type Batch = {
- id: string;
- name: string;
- createdAt: string;
- photos: CapturedPhoto[];
- tags: string[];
-};
-
-export interface User {
- id: string;
- username: string;
-}
-
-export interface AuthTokens {
- access_token: string;
- refresh_token: string;
-}
-
-export interface AuthResponse {
- user: User;
- access_token: string;
- refresh_token: string;
-}
-
-export interface RefreshResponse {
- access_token: string;
- refresh_token: string;
-}
-
-export type SseEvent = {
- event: string;
- data: string;
-};
-
-export type SyncStatus = 'local' | 'syncing' | 'synced' | 'cloud' | 'conflict';
-
-export interface Device {
- id: string;
- device_name: string;
- role: string;
-}
-
-export interface SyncQueueItem {
- id: string;
- resourceId: string;
- storageLocationId: string;
- operation: string;
- status: string;
- attempts: number;
- createdAt: string;
- updatedAt: string;
-}
-
-export interface ShareEntry {
- user_id: string;
- role: string;
-}
-
-export interface ResourcePlacement {
- id: string;
- resourceId: string;
- storageLocationId: string;
- status: string;
- storageKey: string | null;
- syncedAt: string | null;
-}
-
-export type PendingActionType = 'tag_add' | 'delete' | 'move' | 'create_folder';
-export type PendingActionStatus = 'pending' | 'done' | 'error' | 'obsolete';
-
-export interface PendingAction {
- id: string;
- type: PendingActionType;
- payload: Record;
- status: PendingActionStatus;
- attempts: number;
- lastError: string | null;
- resourceId: string | null;
- createdAt: string;
-}