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; -}