- Deleted redundant components (`AddActionButton`, `AlertBox`, `AlertStack`, `BackButton`, `CancelButton`, and `CollapsableArea`) and related files. - Removed unused models (`Book`, `BookSerie`, `BookTables`, `Character`, and `Chapter`) to reduce codebase clutter. - Updated project structure and references to reflect these removals.
347 lines
13 KiB
TypeScript
347 lines
13 KiB
TypeScript
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<boolean> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<void> {
|
|
for (const diff of seriesToSyncToServer) {
|
|
await syncToServer(diff.id);
|
|
}
|
|
}
|
|
|
|
async function syncAllFromServer(): Promise<void> {
|
|
for (const diff of seriesToSyncFromServer) {
|
|
await syncFromServer(diff.id);
|
|
}
|
|
}
|
|
|
|
async function refreshSeries(): Promise<void> {
|
|
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<SyncedSeriesResponse>(
|
|
'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<SyncedSeriesResponse>(
|
|
'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
|
|
};
|
|
}
|