import { useContext } from 'react'; import {apiGet, apiPost, apiPatch} from '@/lib/api/client'; import { SessionContext } from '@/context/SessionContext'; import { LangContext } from '@/context/LangContext'; import { AlertContext } from '@/context/AlertContext'; import OfflineContext from '@/context/OfflineContext'; import { SeriesSyncContext } from '@/context/SeriesSyncContext'; import { SeriesSyncCompare, SyncedSeries } from '@/lib/types/synced-series'; import { useTranslations } from '@/lib/i18n'; import * as tauri from '@/lib/tauri'; interface RemovedItemRecord { removal_id: string; table_name: string; entity_id: string; book_id: string | null; user_id: string; deleted_at: number; } interface SyncedSeriesResponse { series: SyncedSeries[]; tombstones: RemovedItemRecord[]; } /** * Complete series data structure for full upload/download operations. * Mirrors the backend CompleteSeries interface. */ interface CompleteSeries { series: unknown[]; seriesBooks: unknown[]; seriesCharacters: unknown[]; seriesCharacterAttributes: unknown[]; seriesWorlds: unknown[]; seriesWorldElements: unknown[]; seriesLocations: unknown[]; seriesLocationElements: unknown[]; seriesLocationSubElements: unknown[]; seriesSpells: unknown[]; seriesSpellTags: unknown[]; } /** * Hook for managing series synchronization between local database and server. * Provides methods for upload, download, and partial sync operations. */ export default function useSyncSeries() { const t = useTranslations(); const { session } = useContext(SessionContext); const { lang } = useContext(LangContext); const { errorMessage } = useContext(AlertContext); const { isCurrentlyOffline, offlineMode } = useContext(OfflineContext); const { seriesToSyncToServer, seriesToSyncFromServer, localOnlySeries, serverOnlySeries, setLocalOnlySeries, setServerOnlySeries, setServerSyncedSeries, setLocalSyncedSeries, setSeriesToSyncFromServer, setSeriesToSyncToServer } = useContext(SeriesSyncContext); /** * Uploads a local-only series to the server. * @param seriesId - The ID of the series to upload * @returns True if upload was successful, false otherwise */ async function upload(seriesId: string): Promise { if (isCurrentlyOffline()) return false; try { const seriesToSync: CompleteSeries = await tauri.uploadSeriesToServer(seriesId) as CompleteSeries; if (!seriesToSync) { errorMessage(t('seriesCard.uploadError')); return false; } const response: boolean = await apiPost('series/sync/upload', { series: seriesToSync }, session.accessToken, lang); if (!response) { errorMessage(t('seriesCard.uploadError')); return false; } // Move series from local-only to synced const uploadedSeries: SyncedSeries | undefined = localOnlySeries.find( (series: SyncedSeries): boolean => series.id === seriesId ); setLocalOnlySeries((prevSeries: SyncedSeries[]): SyncedSeries[] => { return prevSeries.filter((series: SyncedSeries): boolean => series.id !== seriesId); }); if (uploadedSeries) { setLocalSyncedSeries((prev: SyncedSeries[]): SyncedSeries[] => [...prev, uploadedSeries]); setServerSyncedSeries((prev: SyncedSeries[]): SyncedSeries[] => [...prev, uploadedSeries]); } return true; } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('seriesCard.uploadError')); } return false; } } /** * Downloads a server-only series to the local database. * @param seriesId - The ID of the series to download * @returns True if download was successful, false otherwise */ async function download(seriesId: string): Promise { if (isCurrentlyOffline()) return false; try { const response: CompleteSeries = await apiGet( 'series/sync/download', session.accessToken, lang, { seriesId } ); if (!response) { errorMessage(t('seriesCard.downloadError')); return false; } const syncStatus: boolean = await tauri.syncSaveSeries(response); if (!syncStatus) { errorMessage(t('seriesCard.downloadError')); return false; } // Move series from server-only to synced const downloadedSeries: SyncedSeries | undefined = serverOnlySeries.find( (series: SyncedSeries): boolean => series.id === seriesId ); setServerOnlySeries((prevSeries: SyncedSeries[]): SyncedSeries[] => { return prevSeries.filter((series: SyncedSeries): boolean => series.id !== seriesId); }); if (downloadedSeries) { setLocalSyncedSeries((prev: SyncedSeries[]): SyncedSeries[] => [...prev, downloadedSeries]); setServerSyncedSeries((prev: SyncedSeries[]): SyncedSeries[] => [...prev, downloadedSeries]); } return true; } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('seriesCard.downloadError')); } return false; } } /** * Syncs changes from server to local database for a specific series. * Only transfers entities that have changed based on the comparison. * @param seriesId - The ID of the series to sync * @returns True if sync was successful, false otherwise */ async function syncFromServer(seriesId: string): Promise { if (isCurrentlyOffline()) return false; try { const seriesToFetch: SeriesSyncCompare | undefined = seriesToSyncFromServer.find( (series: SeriesSyncCompare): boolean => series.id === seriesId ); if (!seriesToFetch) { errorMessage(t('seriesCard.syncFromServerError')); return false; } const response: CompleteSeries = await apiPost( 'series/sync/server-to-client', { seriesToSync: seriesToFetch }, session.accessToken, lang ); if (!response) { errorMessage(t('seriesCard.syncFromServerError')); return false; } const syncStatus: boolean = await tauri.syncSeriesToClient(response); if (!syncStatus) { errorMessage(t('seriesCard.syncFromServerError')); return false; } // Remove from pending sync list setSeriesToSyncFromServer((prev: SeriesSyncCompare[]): SeriesSyncCompare[] => prev.filter((series: SeriesSyncCompare): boolean => series.id !== seriesId) ); return true; } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('seriesCard.syncFromServerError')); } return false; } } /** * Syncs local changes to the server for a specific series. * Only transfers entities that have changed based on the comparison. * @param seriesId - The ID of the series to sync * @returns True if sync was successful, false otherwise */ async function syncToServer(seriesId: string): Promise { if (isCurrentlyOffline()) { return false; } try { const seriesToFetch: SeriesSyncCompare | undefined = seriesToSyncToServer.find( (series: SeriesSyncCompare): boolean => series.id === seriesId ); if (!seriesToFetch) { // La série n'est plus dans la liste - probablement déjà sync par AutoSyncOnReconnect // Retourner true car ce n'est pas une erreur, juste déjà fait return true; } const seriesToSync: CompleteSeries = await tauri.syncSeriesToServer(seriesToFetch) as CompleteSeries; if (!seriesToSync) { errorMessage(t('seriesCard.syncToServerError')); return false; } const response: boolean = await apiPatch( 'series/sync/client-to-server', { series: seriesToSync }, session.accessToken, lang ); if (!response) { errorMessage(t('seriesCard.syncToServerError')); return false; } // Remove from pending sync list setSeriesToSyncToServer((prev: SeriesSyncCompare[]): SeriesSyncCompare[] => prev.filter((series: SeriesSyncCompare): boolean => series.id !== seriesId) ); return true; } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('seriesCard.syncToServerError')); } return false; } } /** * Syncs all series that have local changes to the server. */ async function syncAllToServer(): Promise { for (const diff of seriesToSyncToServer) { await syncToServer(diff.id); } } async function syncAllFromServer(): Promise { for (const diff of seriesToSyncFromServer) { await syncFromServer(diff.id); } } async function refreshSeries(): Promise { try { let localSeriesResponse: SyncedSeries[] = []; let serverSeriesResponse: SyncedSeries[] = []; if (!isCurrentlyOffline()) { if (offlineMode.isDatabaseInitialized) { localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[]; const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp'); const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0; const localTombstones: RemovedItemRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as RemovedItemRecord[]; const serverResponse: SyncedSeriesResponse = await apiPost( 'series/synced', { lastOnlineTimestamp, tombstones: localTombstones }, session.accessToken, lang ); serverSeriesResponse = serverResponse.series; await tauri.applySeriesTombstones(serverResponse.tombstones as tauri.TombstoneRecord[]); } else { // No local DB but online - just get server series without tombstones const serverResponse: SyncedSeriesResponse = await apiPost( 'series/synced', { lastOnlineTimestamp: 0, tombstones: [] }, session.accessToken, lang ); serverSeriesResponse = serverResponse.series; } } else { if (offlineMode.isDatabaseInitialized) { localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[]; } } setServerSyncedSeries(serverSeriesResponse); setLocalSyncedSeries(localSeriesResponse); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('seriesCard.refreshError')); } } } return { upload, download, syncFromServer, syncToServer, syncAllToServer, syncAllFromServer, refreshSeries, localOnlySeries, serverOnlySeries, seriesToSyncToServer, seriesToSyncFromServer }; }