- Deleted `CharacterComponent` and `CharacterDetail` files from the project. - Refactored related logic to improve code maintainability and reduce redundancy.
353 lines
13 KiB
TypeScript
353 lines
13 KiB
TypeScript
import { useContext } from 'react';
|
|
import System from '@/lib/models/System';
|
|
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/models/SyncedSeries';
|
|
import { useTranslations } from 'next-intl';
|
|
|
|
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 window.electron.invoke<CompleteSeries>('db:series:uploadToServer', seriesId);
|
|
if (!seriesToSync) {
|
|
errorMessage(t('seriesCard.uploadError'));
|
|
return false;
|
|
}
|
|
|
|
const response: boolean = await System.authPostToServer('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 System.authGetQueryToServer(
|
|
'series/sync/download',
|
|
session.accessToken,
|
|
lang,
|
|
{ seriesId }
|
|
);
|
|
|
|
if (!response) {
|
|
errorMessage(t('seriesCard.downloadError'));
|
|
return false;
|
|
}
|
|
|
|
const syncStatus: boolean = await window.electron.invoke<boolean>('db:series:syncSave', 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 System.authPostToServer(
|
|
'series/sync/server-to-client',
|
|
{ seriesToSync: seriesToFetch },
|
|
session.accessToken,
|
|
lang
|
|
);
|
|
|
|
if (!response) {
|
|
errorMessage(t('seriesCard.syncFromServerError'));
|
|
return false;
|
|
}
|
|
|
|
const syncStatus: boolean = await window.electron.invoke<boolean>('db:series:sync:toClient', 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 window.electron.invoke<CompleteSeries>(
|
|
'db:series:sync:toServer',
|
|
seriesToFetch
|
|
);
|
|
if (!seriesToSync) {
|
|
errorMessage(t('seriesCard.syncToServerError'));
|
|
return false;
|
|
}
|
|
|
|
const response: boolean = await System.authPatchToServer(
|
|
'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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refreshes the sync status of all series by comparing local and server data.
|
|
* Updates the context with the latest sync information.
|
|
*/
|
|
async function refreshSeries(): Promise<void> {
|
|
try {
|
|
let localSeriesResponse: SyncedSeries[] = [];
|
|
let serverSeriesResponse: SyncedSeries[] = [];
|
|
|
|
if (!isCurrentlyOffline()) {
|
|
if (offlineMode.isDatabaseInitialized) {
|
|
localSeriesResponse = await window.electron.invoke<SyncedSeries[]>('db:series:synced');
|
|
|
|
// Get lastOnlineTimestamp from localStorage (or 0 if not set)
|
|
const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
|
|
const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
|
|
|
|
// Get local tombstones since lastOnlineTimestamp via IPC
|
|
const localTombstones: RemovedItemRecord[] = await window.electron.invoke<RemovedItemRecord[]>(
|
|
'db:tombstones:since',
|
|
lastOnlineTimestamp
|
|
);
|
|
|
|
// Call server with POST and tombstones
|
|
const serverResponse: SyncedSeriesResponse = await System.authPostToServer<SyncedSeriesResponse>(
|
|
'series/synced',
|
|
{ lastOnlineTimestamp, tombstones: localTombstones },
|
|
session.accessToken,
|
|
lang
|
|
);
|
|
|
|
serverSeriesResponse = serverResponse.series;
|
|
|
|
// Apply server tombstones locally via IPC
|
|
await window.electron.invoke<void>('db:tombstones:apply:series', serverResponse.tombstones);
|
|
} else {
|
|
// No local DB but online - just get server series without tombstones
|
|
const serverResponse: SyncedSeriesResponse = await System.authPostToServer<SyncedSeriesResponse>(
|
|
'series/synced',
|
|
{ lastOnlineTimestamp: 0, tombstones: [] },
|
|
session.accessToken,
|
|
lang
|
|
);
|
|
serverSeriesResponse = serverResponse.series;
|
|
}
|
|
} else {
|
|
if (offlineMode.isDatabaseInitialized) {
|
|
localSeriesResponse = await window.electron.invoke<SyncedSeries[]>('db:series:synced');
|
|
}
|
|
}
|
|
|
|
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,
|
|
refreshSeries,
|
|
localOnlySeries,
|
|
serverOnlySeries,
|
|
seriesToSyncToServer,
|
|
seriesToSyncFromServer
|
|
};
|
|
}
|