Refactor and extend offline synchronization logic across components and services

- Integrated sync queue mechanisms with `LocalSyncQueueContext` for offline data handling.
- Updated key sync-related services (e.g., book, chapter, series) to support offline-first functionality.
- Removed redundant database fetch methods to optimize repository logic and improve maintainability.
- Enhanced Tauri IPC usage for sync operations and removed legacy methods in Rust services.
This commit is contained in:
natreex
2026-03-30 21:06:58 -04:00
parent b9606e899a
commit dbbe33b19b
22 changed files with 295 additions and 293 deletions

View File

@@ -20,6 +20,9 @@ import {LangContext, LangContextProps} from "@/context/LangContext";
import {BookProps} from "@/lib/types/book"; import {BookProps} from "@/lib/types/book";
import {SettingRef} from "@/lib/types/settings"; import {SettingRef} from "@/lib/types/settings";
import ImageDropZone from "@/components/form/ImageDropZone"; import ImageDropZone from "@/components/form/ImageDropZone";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/types/synced-book";
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
function BasicInformationSetting(_props: object, ref: React.ForwardedRef<SettingRef>): React.JSX.Element { function BasicInformationSetting(_props: object, ref: React.ForwardedRef<SettingRef>): React.JSX.Element {
const t = useTranslations(); const t = useTranslations();
@@ -30,6 +33,8 @@ function BasicInformationSetting(_props: object, ref: React.ForwardedRef<Setting
const userToken: string = session?.accessToken ? session?.accessToken : ''; const userToken: string = session?.accessToken ? session?.accessToken : '';
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const bookId: string = book?.bookId ? book?.bookId.toString() : ''; const bookId: string = book?.bookId ? book?.bookId.toString() : '';
const [currentImage, setCurrentImage] = useState<string>(book?.coverImage ?? ''); const [currentImage, setCurrentImage] = useState<string>(book?.coverImage ?? '');
@@ -120,14 +125,14 @@ function BasicInformationSetting(_props: object, ref: React.ForwardedRef<Setting
wordCount: wordCount, wordCount: wordCount,
}); });
} else { } else {
response = await apiPost<boolean>('book/basic-information', { const basicInfoData = {
title: title, title, subTitle, summary, publicationDate, wordCount, bookId
subTitle: subTitle, };
summary: summary, response = await apiPost<boolean>('book/basic-information', basicInfoData, userToken, lang);
publicationDate: publicationDate,
wordCount: wordCount, if (isDesktop && localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
bookId: bookId addToQueue('update_book_basic_info', basicInfoData);
}, userToken, lang); }
} }
if (!response) { if (!response) {
errorMessage(t('basicInformationSetting.error.update')); errorMessage(t('basicInformationSetting.error.update'));

View File

@@ -22,31 +22,44 @@ export default function DeleteBook({bookId}: DeleteBookProps) {
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext) const {lang}: LangContextProps = useContext<LangContextProps>(LangContext)
const [showConfirmBox, setShowConfirmBox] = useState<boolean>(false); const [showConfirmBox, setShowConfirmBox] = useState<boolean>(false);
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext) const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext)
const {setServerSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext) const {serverOnlyBooks, setServerOnlyBooks, localOnlyBooks, setLocalOnlyBooks, localSyncedBooks, setLocalSyncedBooks, setServerSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext)
const [deleteLocalToo, setDeleteLocalToo] = useState<boolean>(false);
const ifLocalOnlyBook: SyncedBook | undefined = localOnlyBooks.find((b: SyncedBook): boolean => b.id === bookId);
const ifSyncedBook: SyncedBook | undefined = localSyncedBooks.find((b: SyncedBook): boolean => b.id === bookId);
const {book}: BookContextProps = useContext<BookContextProps>(BookContext); const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
function handleConfirmation(): void { function handleConfirmation(): void {
setDeleteLocalToo(false);
setShowConfirmBox(true); setShowConfirmBox(true);
} }
async function handleDeleteBook(): Promise<void> { async function handleDeleteBook(): Promise<void> {
try { try {
let response: boolean; let response: boolean;
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { const deletedAt: number = Math.floor(Date.now() / 1000);
response = await tauri.deleteBook(bookId, Date.now());
if (isDesktop && (isCurrentlyOffline() || ifLocalOnlyBook)) {
response = await tauri.deleteBook(bookId, deletedAt);
} else { } else {
response = await apiDelete<boolean>('book/delete', { response = await apiDelete<boolean>('book/delete', {id: bookId, deletedAt}, session.accessToken, lang);
id: bookId, if (response && isDesktop && ifSyncedBook && deleteLocalToo) {
}, session.accessToken, lang); await tauri.deleteBook(bookId, deletedAt);
}
} }
if (response) { if (response) {
setShowConfirmBox(false); setShowConfirmBox(false);
if (!response) { if (ifLocalOnlyBook) {
errorMessage("Une erreur est survenue lors de la suppression du livre."); setLocalOnlyBooks(localOnlyBooks.filter((b: SyncedBook): boolean => b.id !== bookId));
return; } else if (ifSyncedBook) {
setLocalSyncedBooks(localSyncedBooks.filter((b: SyncedBook): boolean => b.id !== bookId));
setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => prev.filter((b: SyncedBook): boolean => b.id !== bookId));
if (!deleteLocalToo) {
setLocalOnlyBooks([...localOnlyBooks, ifSyncedBook]);
}
} else {
setServerOnlyBooks(serverOnlyBooks.filter((b: SyncedBook): boolean => b.id !== bookId));
} }
setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => prev.filter((book: SyncedBook): boolean => book.id !== bookId))
} }
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {

View File

@@ -5,6 +5,9 @@ import {SessionContext, SessionContextProps} from "@/context/SessionContext";
import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import {BookContext, BookContextProps} from "@/context/BookContext"; import {BookContext, BookContextProps} from "@/context/BookContext";
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
import {SyncedBook} from '@/lib/types/synced-book';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
import {isDesktop} from '@/lib/configs'; import {isDesktop} from '@/lib/configs';
import {apiDelete, apiGet, apiPatch, apiPost} from '@/lib/api/client'; import {apiDelete, apiGet, apiPatch, apiPost} from '@/lib/api/client';
import * as tauri from '@/lib/tauri'; import * as tauri from '@/lib/tauri';
@@ -57,6 +60,8 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
const {successMessage, errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {successMessage, errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext); const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext);
const {isCurrentlyOffline} = useContext(OfflineContext); const {isCurrentlyOffline} = useContext(OfflineContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const currentEntityId: string = entityId || book?.bookId || ''; const currentEntityId: string = entityId || book?.bookId || '';
const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook); const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook);
@@ -120,6 +125,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
toolName: 'locations', toolName: 'locations',
enabled: enabled enabled: enabled
}, token, lang); }, token, lang);
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
addToQueue('update_book_tool_setting', {bookId: currentEntityId, toolName: 'locations', enabled});
}
if (response && setBook && book) { if (response && setBook && book) {
setToolEnabled(enabled); setToolEnabled(enabled);
setBook({ setBook({
@@ -218,6 +226,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
sectionId = useLocal sectionId = useLocal
? await tauri.addLocationSection(newSectionName, currentEntityId) ? await tauri.addLocationSection(newSectionName, currentEntityId)
: await apiPost<string>('location/section/add', {bookId: currentEntityId, locationName: newSectionName}, token, lang); : await apiPost<string>('location/section/add', {bookId: currentEntityId, locationName: newSectionName}, token, lang);
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
addToQueue('add_location_section', {bookId: currentEntityId, sectionId, locationName: newSectionName});
}
if (!sectionId) { if (!sectionId) {
errorMessage(t('locationComponent.errorUnknownAddSection')); errorMessage(t('locationComponent.errorUnknownAddSection'));
return; return;
@@ -258,6 +269,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
elementId = useLocal elementId = useLocal
? await tauri.addLocationElement(sectionId, newElementNames[sectionId]) ? await tauri.addLocationElement(sectionId, newElementNames[sectionId])
: await apiPost<string>('location/element/add', {bookId: currentEntityId, locationId: sectionId, elementName: newElementNames[sectionId]}, token, lang); : await apiPost<string>('location/element/add', {bookId: currentEntityId, locationId: sectionId, elementName: newElementNames[sectionId]}, token, lang);
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
addToQueue('add_location_element', {bookId: currentEntityId, locationId: sectionId, elementId, elementName: newElementNames[sectionId]});
}
if (!elementId) { if (!elementId) {
errorMessage(t('locationComponent.errorUnknownAddElement')); errorMessage(t('locationComponent.errorUnknownAddElement'));
return; return;
@@ -325,6 +339,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
subElementId = useLocal subElementId = useLocal
? await tauri.addLocationSubElement(parentElementId, newSubElementNames[elementIndex]) ? await tauri.addLocationSubElement(parentElementId, newSubElementNames[elementIndex])
: await apiPost<string>('location/sub-element/add', {elementId: parentElementId, subElementName: newSubElementNames[elementIndex]}, token, lang); : await apiPost<string>('location/sub-element/add', {elementId: parentElementId, subElementName: newSubElementNames[elementIndex]}, token, lang);
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
addToQueue('add_location_subelement', {elementId: parentElementId, subElementId, subElementName: newSubElementNames[elementIndex]});
}
if (!subElementId) { if (!subElementId) {
errorMessage(t('locationComponent.errorUnknownAddSubElement')); errorMessage(t('locationComponent.errorUnknownAddSubElement'));
return; return;
@@ -381,6 +398,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
success = useLocal success = useLocal
? await tauri.deleteLocationElement(elementId!, currentEntityId, deletedAt) ? await tauri.deleteLocationElement(elementId!, currentEntityId, deletedAt)
: await apiDelete<boolean>('location/element/delete', {elementId: elementId}, token, lang); : await apiDelete<boolean>('location/element/delete', {elementId: elementId}, token, lang);
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
addToQueue('delete_location_element', {elementId, bookId: currentEntityId, deletedAt});
}
} }
if (!success) { if (!success) {
errorMessage(t('locationComponent.errorUnknownDeleteElement')); errorMessage(t('locationComponent.errorUnknownDeleteElement'));
@@ -417,6 +437,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
success = useLocal success = useLocal
? await tauri.deleteLocationSubElement(subElementId!, currentEntityId, deletedAt) ? await tauri.deleteLocationSubElement(subElementId!, currentEntityId, deletedAt)
: await apiDelete<boolean>('location/sub-element/delete', {subElementId: subElementId}, token, lang); : await apiDelete<boolean>('location/sub-element/delete', {subElementId: subElementId}, token, lang);
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
addToQueue('delete_location_subelement', {subElementId, bookId: currentEntityId, deletedAt});
}
} }
if (!success) { if (!success) {
errorMessage(t('locationComponent.errorUnknownDeleteSubElement')); errorMessage(t('locationComponent.errorUnknownDeleteSubElement'));
@@ -447,6 +470,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
success = useLocal success = useLocal
? await tauri.deleteLocationSection(sectionId, currentEntityId, deletedAt) ? await tauri.deleteLocationSection(sectionId, currentEntityId, deletedAt)
: await apiDelete<boolean>('location/delete', {locationId: sectionId}, token, lang); : await apiDelete<boolean>('location/delete', {locationId: sectionId}, token, lang);
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
addToQueue('delete_location', {locationId: sectionId, bookId: currentEntityId, deletedAt});
}
} }
if (!success) { if (!success) {
errorMessage(t('locationComponent.errorUnknownDeleteSection')); errorMessage(t('locationComponent.errorUnknownDeleteSection'));
@@ -468,6 +494,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
const response: boolean = useLocal const response: boolean = useLocal
? await tauri.updateLocations(sections) as boolean ? await tauri.updateLocations(sections) as boolean
: await apiPost<boolean>(`location/update`, {locations: sections}, token, lang); : await apiPost<boolean>(`location/update`, {locations: sections}, token, lang);
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
addToQueue('update_locations', {locations: sections});
}
if (!response) { if (!response) {
errorMessage(t('locationComponent.errorUnknownSave')); errorMessage(t('locationComponent.errorUnknownSave'));
return; return;

View File

@@ -6,6 +6,9 @@ import {apiDelete, apiPost} from '@/lib/api/client';
import {isDesktop} from '@/lib/configs'; import {isDesktop} from '@/lib/configs';
import * as tauri from '@/lib/tauri'; import * as tauri from '@/lib/tauri';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
import {SyncedBook} from '@/lib/types/synced-book';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
import {BookContext, BookContextProps} from '@/context/BookContext'; import {BookContext, BookContextProps} from '@/context/BookContext';
import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext';
@@ -30,6 +33,8 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const bookId: string | undefined = book?.bookId; const bookId: string | undefined = book?.bookId;
const token: string = session.accessToken; const token: string = session.accessToken;
@@ -72,10 +77,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
incidentId = await tauri.addIncident(bookId ?? '', newIncidentTitle); incidentId = await tauri.addIncident(bookId ?? '', newIncidentTitle);
} else { } else {
incidentId = await apiPost<string>('book/incident/new', { const addData = {bookId, name: newIncidentTitle};
bookId, incidentId = await apiPost<string>('book/incident/new', addData, token, lang);
name: newIncidentTitle,
}, token, lang); if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
addToQueue('add_incident', addData);
}
} }
if (!incidentId) { if (!incidentId) {
errorMessage(t('errorAddIncident')); errorMessage(t('errorAddIncident'));
@@ -114,10 +121,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
response = await tauri.removeIncident(bookId ?? '', incidentId, Date.now()); response = await tauri.removeIncident(bookId ?? '', incidentId, Date.now());
} else { } else {
response = await apiDelete<boolean>('book/incident/remove', { const deleteData = {bookId, incidentId};
bookId, response = await apiDelete<boolean>('book/incident/remove', deleteData, token, lang);
incidentId,
}, token, lang); if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
addToQueue('remove_incident', {...deleteData, deletedAt: Date.now()});
}
} }
if (!response) { if (!response) {
errorMessage(t('errorDeleteIncident')); errorMessage(t('errorDeleteIncident'));
@@ -151,11 +160,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
plotId = await tauri.addPlotPoint(bookId ?? '', newPlotPointTitle, selectedIncidentId); plotId = await tauri.addPlotPoint(bookId ?? '', newPlotPointTitle, selectedIncidentId);
} else { } else {
plotId = await apiPost<string>('book/plot/new', { const plotData = {bookId, name: newPlotPointTitle, incidentId: selectedIncidentId};
bookId, plotId = await apiPost<string>('book/plot/new', plotData, token, lang);
name: newPlotPointTitle,
incidentId: selectedIncidentId, if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
}, token, lang); addToQueue('add_plot_point', plotData);
}
} }
if (!plotId) { if (!plotId) {
errorMessage(t('errorAddPlotPoint')); errorMessage(t('errorAddPlotPoint'));
@@ -195,9 +205,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
response = await tauri.removePlotPoint(plotPointId, bookId ?? '', Date.now()); response = await tauri.removePlotPoint(plotPointId, bookId ?? '', Date.now());
} else { } else {
response = await apiDelete<boolean>('book/plot/remove', { const deleteData = {plotId: plotPointId};
plotId: plotPointId, response = await apiDelete<boolean>('book/plot/remove', deleteData, token, lang);
}, token, lang);
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
addToQueue('remove_plot_point', {...deleteData, bookId, deletedAt: Date.now()});
}
} }
if (!response) { if (!response) {
errorMessage(t('errorDeletePlotPoint')); errorMessage(t('errorDeletePlotPoint'));
@@ -246,13 +259,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
incidentId: destination === 'incident' ? itemId : undefined, incidentId: destination === 'incident' ? itemId : undefined,
}); });
} else { } else {
linkId = await apiPost<string>('chapter/resume/add', { const linkData = {bookId, chapterId, actId, plotId: destination === 'plotPoint' ? itemId : null, incidentId: destination === 'incident' ? itemId : null};
bookId, linkId = await apiPost<string>('chapter/resume/add', linkData, token, lang);
chapterId: chapterId,
actId: actId, if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
plotId: destination === 'plotPoint' ? itemId : null, addToQueue('add_chapter_information', linkData);
incidentId: destination === 'incident' ? itemId : null, }
}, token, lang);
} }
if (!linkId) { if (!linkId) {
errorMessage(t('errorLinkChapter')); errorMessage(t('errorLinkChapter'));
@@ -332,9 +344,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
response = await tauri.removeChapterInformation(chapterInfoId, bookId ?? '', Date.now()); response = await tauri.removeChapterInformation(chapterInfoId, bookId ?? '', Date.now());
} else { } else {
response = await apiDelete<boolean>('chapter/resume/remove', { const removeData = {chapterInfoId};
chapterInfoId, response = await apiDelete<boolean>('chapter/resume/remove', removeData, token, lang);
}, token, lang);
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
addToQueue('remove_chapter_information', {...removeData, bookId, deletedAt: Date.now()});
}
} }
if (!response) { if (!response) {
errorMessage(t('errorUnlinkChapter')); errorMessage(t('errorUnlinkChapter'));

View File

@@ -6,6 +6,9 @@ import {apiDelete, apiPost} from '@/lib/api/client';
import {isDesktop} from '@/lib/configs'; import {isDesktop} from '@/lib/configs';
import * as tauri from '@/lib/tauri'; import * as tauri from '@/lib/tauri';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
import {SyncedBook} from '@/lib/types/synced-book';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
import {BookContext, BookContextProps} from '@/context/BookContext'; import {BookContext, BookContextProps} from '@/context/BookContext';
import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext';
@@ -27,6 +30,8 @@ export default function Issues({issues, setIssues}: IssuesProps) {
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const bookId: string | undefined = book?.bookId; const bookId: string | undefined = book?.bookId;
const token: string = session.accessToken; const token: string = session.accessToken;
@@ -43,10 +48,12 @@ export default function Issues({issues, setIssues}: IssuesProps) {
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
issueId = await tauri.addIssue(bookId ?? '', newIssueName); issueId = await tauri.addIssue(bookId ?? '', newIssueName);
} else { } else {
issueId = await apiPost<string>('book/issue/add', { const addData = {bookId, name: newIssueName};
bookId, issueId = await apiPost<string>('book/issue/add', addData, token, lang);
name: newIssueName,
}, token, lang); if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
addToQueue('add_issue', addData);
}
} }
if (!issueId) { if (!issueId) {
errorMessage(t("issues.errorAdd")); errorMessage(t("issues.errorAdd"));
@@ -79,15 +86,12 @@ export default function Issues({issues, setIssues}: IssuesProps) {
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
response = await tauri.removeIssue(bookId ?? '', issueId, Date.now()); response = await tauri.removeIssue(bookId ?? '', issueId, Date.now());
} else { } else {
response = await apiDelete<boolean>( const deleteData = {bookId, issueId};
'book/issue/remove', response = await apiDelete<boolean>('book/issue/remove', deleteData, token, lang);
{
bookId, if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
issueId, addToQueue('remove_issue', {...deleteData, deletedAt: Date.now()});
}, }
token,
lang
);
} }
if (response) { if (response) {
const updatedIssues: Issue[] = issues.filter((issue: Issue): boolean => issue.id !== issueId,); const updatedIssues: Issue[] = issues.filter((issue: Issue): boolean => issue.id !== issueId,);

View File

@@ -7,6 +7,9 @@ import {apiDelete, apiPost} from '@/lib/api/client';
import {isDesktop} from '@/lib/configs'; import {isDesktop} from '@/lib/configs';
import * as tauri from '@/lib/tauri'; import * as tauri from '@/lib/tauri';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
import {SyncedBook} from '@/lib/types/synced-book';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
import {BookContext, BookContextProps} from '@/context/BookContext'; import {BookContext, BookContextProps} from '@/context/BookContext';
import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext';
@@ -30,6 +33,8 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) {
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const bookId: string | undefined = book?.bookId; const bookId: string | undefined = book?.bookId;
const token: string = session.accessToken; const token: string = session.accessToken;
@@ -88,15 +93,12 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) {
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
response = await tauri.removeChapter(chapterIdToRemove, bookId ?? '', Date.now()); response = await tauri.removeChapter(chapterIdToRemove, bookId ?? '', Date.now());
} else { } else {
response = await apiDelete<boolean>( const deleteData = {bookId, chapterId: chapterIdToRemove};
'chapter/remove', response = await apiDelete<boolean>('chapter/remove', deleteData, token, lang);
{
bookId, if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
chapterId: chapterIdToRemove, addToQueue('remove_chapter', {...deleteData, deletedAt: Date.now()});
}, }
token,
lang,
);
} }
if (!response) { if (!response) {
errorMessage(t("mainChapter.errorDelete")); errorMessage(t("mainChapter.errorDelete"));
@@ -126,16 +128,12 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) {
chapterOrder: newChapterOrder ? newChapterOrder : 0, chapterOrder: newChapterOrder ? newChapterOrder : 0,
}); });
} else { } else {
responseId = await apiPost<string>( const addData = {bookId, wordsCount: 0, chapterOrder: newChapterOrder ? newChapterOrder : 0, title: newChapterTitle};
'chapter/add', responseId = await apiPost<string>('chapter/add', addData, token);
{
bookId: bookId, if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
wordsCount: 0, addToQueue('add_chapter', addData);
chapterOrder: newChapterOrder ? newChapterOrder : 0, }
title: newChapterTitle,
},
token,
);
} }
if (!responseId) { if (!responseId) {
errorMessage(t("mainChapter.errorAdd")); errorMessage(t("mainChapter.errorAdd"));

View File

@@ -8,6 +8,9 @@ import {apiGet, apiPost} from '@/lib/api/client';
import {isDesktop} from '@/lib/configs'; import {isDesktop} from '@/lib/configs';
import * as tauri from '@/lib/tauri'; import * as tauri from '@/lib/tauri';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
import {SyncedBook} from '@/lib/types/synced-book';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
import {Act as ActType, Incident, Issue, PlotPoint} from '@/lib/types/book'; import {Act as ActType, Incident, Issue, PlotPoint} from '@/lib/types/book';
import {ActChapter, ChapterListProps} from '@/lib/types/chapter'; import {ActChapter, ChapterListProps} from '@/lib/types/chapter';
import MainChapter from "@/components/book/settings/story/MainChapter"; import MainChapter from "@/components/book/settings/story/MainChapter";
@@ -51,6 +54,8 @@ export function Story(_props: object, ref: React.ForwardedRef<SettingRef>): Reac
const userToken: string = session.accessToken; const userToken: string = session.accessToken;
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const [acts, setActs] = useState<ActType[]>([]); const [acts, setActs] = useState<ActType[]>([]);
const [issues, setIssues] = useState<Issue[]>([]); const [issues, setIssues] = useState<Issue[]>([]);
@@ -138,12 +143,12 @@ export function Story(_props: object, ref: React.ForwardedRef<SettingRef>): Reac
issues, issues,
}); });
} else { } else {
response = await apiPost<boolean>('book/story', { const storyData = {bookId, acts, mainChapters, issues};
bookId, response = await apiPost<boolean>('book/story', storyData, userToken, lang);
acts,
mainChapters, if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
issues, addToQueue('update_book_story', storyData);
}, userToken, lang); }
} }
if (!response) { if (!response) {
errorMessage(t("story.errorSave")) errorMessage(t("story.errorSave"))

View File

@@ -11,6 +11,9 @@ import {apiDelete, apiPost} from "@/lib/api/client";
import {isDesktop} from '@/lib/configs'; import {isDesktop} from '@/lib/configs';
import * as tauri from '@/lib/tauri'; import * as tauri from '@/lib/tauri';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
import {SyncedBook} from '@/lib/types/synced-book';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
import {BookContext, BookContextProps} from '@/context/BookContext'; import {BookContext, BookContextProps} from '@/context/BookContext';
import InputField from "@/components/form/InputField"; import InputField from "@/components/form/InputField";
import {useTranslations} from '@/lib/i18n'; import {useTranslations} from '@/lib/i18n';
@@ -52,6 +55,8 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {book}: BookContextProps = useContext<BookContextProps>(BookContext); const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const [newElementName, setNewElementName] = useState<string>(''); const [newElementName, setNewElementName] = useState<string>('');
@@ -69,6 +74,9 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
response = await apiDelete<boolean>(endpoint, { response = await apiDelete<boolean>(endpoint, {
elementId: elements[index].id, elementId: elements[index].id,
}, session.accessToken, lang); }, session.accessToken, lang);
if (!isSeriesMode && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
addToQueue('remove_world_element', {elementId: elements[index].id, bookId: book?.bookId, deletedAt: Date.now()});
}
} }
if (!response) { if (!response) {
errorMessage(t("worldSetting.unknownError")) errorMessage(t("worldSetting.unknownError"))
@@ -119,6 +127,9 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
worldId: worlds[selectedWorldIndex].id, worldId: worlds[selectedWorldIndex].id,
elementName: newElementName, elementName: newElementName,
}, session.accessToken, lang); }, session.accessToken, lang);
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
addToQueue('add_world_element', {worldId: worlds[selectedWorldIndex].id, elementId, elementType: section, elementName: newElementName});
}
if (!elementId) { if (!elementId) {
errorMessage(t("worldSetting.unknownError")) errorMessage(t("worldSetting.unknownError"))
return; return;

View File

@@ -9,6 +9,9 @@ import {apiGet, apiPatch, apiPost} from "@/lib/api/client";
import {isDesktop} from '@/lib/configs'; import {isDesktop} from '@/lib/configs';
import * as tauri from '@/lib/tauri'; import * as tauri from '@/lib/tauri';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
import {SyncedBook} from '@/lib/types/synced-book';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
import {ElementSection, WorldListResponse, WorldProps, WorldTextField} from "@/lib/types/world"; import {ElementSection, WorldListResponse, WorldProps, WorldTextField} from "@/lib/types/world";
import {SettingRef} from "@/lib/types/settings"; import {SettingRef} from "@/lib/types/settings";
import {elementSections} from "@/lib/constants/world"; import {elementSections} from "@/lib/constants/world";
@@ -39,6 +42,8 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef<S
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext); const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext);
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const currentEntityId: string = entityId || book?.bookId || ''; const currentEntityId: string = entityId || book?.bookId || '';
const isSeriesMode: boolean = entityType === 'series'; const isSeriesMode: boolean = entityType === 'series';
@@ -96,6 +101,9 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef<S
toolName: 'worlds', toolName: 'worlds',
enabled: enabled enabled: enabled
}, session.accessToken, lang); }, session.accessToken, lang);
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
addToQueue('update_book_tool_setting', {bookId: currentEntityId, toolName: 'worlds', enabled});
}
} }
if (response && setBook && book) { if (response && setBook && book) {
setToolEnabled(enabled); setToolEnabled(enabled);
@@ -238,6 +246,9 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef<S
worldName: newWorldName, worldName: newWorldName,
bookId: currentEntityId, bookId: currentEntityId,
}, session.accessToken, lang); }, session.accessToken, lang);
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
addToQueue('add_world', {worldName: newWorldName, bookId: currentEntityId, worldId});
}
if (!worldId) { if (!worldId) {
errorMessage(t("worldSetting.addWorldError")); errorMessage(t("worldSetting.addWorldError"));
return; return;
@@ -310,6 +321,9 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef<S
world: currentWorld, world: currentWorld,
bookId: currentEntityId, bookId: currentEntityId,
}, session.accessToken, lang); }, session.accessToken, lang);
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
addToQueue('update_world', {world: currentWorld, bookId: currentEntityId});
}
if (!response) { if (!response) {
errorMessage(t("worldSetting.updateWorldError")); errorMessage(t("worldSetting.updateWorldError"));
return; return;
@@ -393,7 +407,10 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef<S
errorMessage(t("worldSetting.importError")); errorMessage(t("worldSetting.importError"));
return; return;
} }
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
addToQueue('add_world', {worldName: seriesWorld.name, bookId: currentEntityId, worldId, seriesWorldId});
}
const newWorld: WorldProps = { const newWorld: WorldProps = {
id: worldId, id: worldId,
name: seriesWorld.name, name: seriesWorld.name,

View File

@@ -26,6 +26,9 @@ import {apiPost} from '@/lib/api/client';
import {isDesktop} from '@/lib/configs'; import {isDesktop} from '@/lib/configs';
import * as tauri from '@/lib/tauri'; import * as tauri from '@/lib/tauri';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
import {SyncedBook} from '@/lib/types/synced-book';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext';
import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {SessionContext, SessionContextProps} from "@/context/SessionContext";
import DraftCompanion from "@/components/editor/DraftCompanion"; import DraftCompanion from "@/components/editor/DraftCompanion";
@@ -144,7 +147,9 @@ export default function TextEditor() {
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const [mainTimer, setMainTimer] = useState<number>(0); const [mainTimer, setMainTimer] = useState<number>(0);
const [showDraftCompanion, setShowDraftCompanion] = useState<boolean>(false); const [showDraftCompanion, setShowDraftCompanion] = useState<boolean>(false);
const [showGhostWriter, setShowGhostWriter] = useState<boolean>(false); const [showGhostWriter, setShowGhostWriter] = useState<boolean>(false);
@@ -291,13 +296,18 @@ export default function TextEditor() {
contentId: '', contentId: '',
}); });
} else { } else {
response = await apiPost<boolean>(`chapter/content`, { const saveData = {
chapterId, chapterId,
version, version,
content, content,
totalWordCount: editor.getText().length, totalWordCount: editor.getText().length,
currentTime: mainTimer currentTime: mainTimer
}, session?.accessToken ?? ''); };
response = await apiPost<boolean>(`chapter/content`, saveData, session?.accessToken ?? '');
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
addToQueue('save_chapter_content', saveData);
}
} }
if (!response) { if (!response) {
errorMessage(t('editor.error.savedFailed')); errorMessage(t('editor.error.savedFailed'));
@@ -315,7 +325,7 @@ export default function TextEditor() {
} }
setIsSaving(false); setIsSaving(false);
} }
}, [editor, chapter, mainTimer, session?.accessToken, successMessage, errorMessage]); }, [editor, chapter, mainTimer, session?.accessToken, successMessage, errorMessage, addToQueue, book?.localBook, isCurrentlyOffline]);
const handleShowDraftCompanion: () => void = useCallback((): void => { const handleShowDraftCompanion: () => void = useCallback((): void => {
setShowDraftCompanion((prev: boolean): boolean => !prev); setShowDraftCompanion((prev: boolean): boolean => !prev);
@@ -454,7 +464,7 @@ export default function TextEditor() {
onClick={handleShowUserSettings} onClick={handleShowUserSettings}
tooltip={t("textEditor.preferences")} tooltip={t("textEditor.preferences")}
/> />
{chapter?.chapterContent.version === 2 && book?.quillsenseEnabled !== false && ( {chapter?.chapterContent.version === 2 && !isCurrentlyOffline() && !book?.localBook && book?.quillsenseEnabled !== false && (
<IconButton <IconButton
icon={Ghost} icon={Ghost}
variant="ghost" variant="ghost"

View File

@@ -6,6 +6,13 @@ import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext';
import {LangContext, LangContextProps} from '@/context/LangContext'; import {LangContext, LangContextProps} from '@/context/LangContext';
import {apiPost} from '@/lib/api/client'; import {apiPost} from '@/lib/api/client';
import {isDesktop} from '@/lib/configs';
import * as tauri from '@/lib/tauri';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BookContext, BookContextProps} from '@/context/BookContext';
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
import {SyncedBook} from '@/lib/types/synced-book';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
import IconButton from '@/components/ui/IconButton'; import IconButton from '@/components/ui/IconButton';
export type SyncElementType = 'character' | 'world' | 'location' | 'spell'; export type SyncElementType = 'character' | 'world' | 'location' | 'spell';
@@ -42,7 +49,11 @@ export default function SyncFieldWrapper({
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext); const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
const [isUploading, setIsUploading] = useState<boolean>(false); const [isUploading, setIsUploading] = useState<boolean>(false);
const isLinkedToSeries: boolean = !!seriesElementId; const isLinkedToSeries: boolean = !!seriesElementId;
@@ -53,17 +64,17 @@ export default function SyncFieldWrapper({
setIsUploading(true); setIsUploading(true);
try { try {
const response: SeriesSyncUploadResponse = await apiPost<SeriesSyncUploadResponse>( const requestData = {type: elementType, bookElementId, field, value: currentValue};
'series/propagate', let response: SeriesSyncUploadResponse;
{
type: elementType, if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
bookElementId: bookElementId, response = await tauri.invoke<SeriesSyncUploadResponse>('series_sync_upload', {data: requestData});
field: field, } else {
value: currentValue response = await apiPost<SeriesSyncUploadResponse>('series/propagate', requestData, session.accessToken, lang);
}, if (isDesktop && book?.bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book.bookId)) {
session.accessToken, addToQueue('series_sync_upload', requestData);
lang }
); }
if (response.success) { if (response.success) {
successMessage(t('syncField.uploadSuccess', {count: response.updatedCount})); successMessage(t('syncField.uploadSuccess', {count: response.updatedCount}));
if (onSyncComplete) { if (onSyncComplete) {

View File

@@ -4,6 +4,9 @@ import {apiDelete, apiGet, apiPost} from '@/lib/api/client';
import {isDesktop} from '@/lib/configs'; import {isDesktop} from '@/lib/configs';
import * as tauri from '@/lib/tauri'; import * as tauri from '@/lib/tauri';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
import {SyncedBook} from '@/lib/types/synced-book';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
import {BookContext, BookContextProps} from "@/context/BookContext"; import {BookContext, BookContextProps} from "@/context/BookContext";
import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import {ChapterContext, ChapterContextProps} from "@/context/ChapterContext"; import {ChapterContext, ChapterContextProps} from "@/context/ChapterContext";
@@ -26,6 +29,8 @@ export default function ScribeChapterComponent() {
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const userToken: string = session?.accessToken ? session?.accessToken : ''; const userToken: string = session?.accessToken ? session?.accessToken : '';
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const router = useRouter(); const router = useRouter();
const [chapters, setChapters] = useState<ChapterListProps[]>([]) const [chapters, setChapters] = useState<ChapterListProps[]>([])
@@ -107,11 +112,12 @@ export default function ScribeChapterComponent() {
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
response = await tauri.updateChapter(chapterId, title, chapterOrder); response = await tauri.updateChapter(chapterId, title, chapterOrder);
} else { } else {
response = await apiPost<boolean>('chapter/update', { const updateData = {chapterId, chapterOrder, title};
chapterId: chapterId, response = await apiPost<boolean>('chapter/update', updateData, userToken, lang);
chapterOrder: chapterOrder,
title: title, if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
}, userToken, lang); addToQueue('update_chapter', updateData);
}
} }
if (!response) { if (!response) {
errorMessage(t("scribeChapterComponent.errorChapterUpdate")); errorMessage(t("scribeChapterComponent.errorChapterUpdate"));
@@ -148,9 +154,12 @@ export default function ScribeChapterComponent() {
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
response = await tauri.removeChapter(removeChapterId, book?.bookId ?? '', Date.now()); response = await tauri.removeChapter(removeChapterId, book?.bookId ?? '', Date.now());
} else { } else {
response = await apiDelete<boolean>('chapter/remove', { const deleteData = {bookId: book?.bookId, chapterId: removeChapterId};
chapterId: removeChapterId, response = await apiDelete<boolean>('chapter/remove', deleteData, userToken, lang);
}, userToken, lang);
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
addToQueue('remove_chapter', {...deleteData, deletedAt: Date.now()});
}
} }
if (!response) { if (!response) {
errorMessage(t("scribeChapterComponent.errorChapterDelete")); errorMessage(t("scribeChapterComponent.errorChapterDelete"));
@@ -184,11 +193,12 @@ export default function ScribeChapterComponent() {
chapterOrder: chapterOrder, chapterOrder: chapterOrder,
}); });
} else { } else {
chapterId = await apiPost<string>('chapter/add', { const addData = {bookId: book?.bookId, chapterOrder, title: chapterTitle};
bookId: book?.bookId, chapterId = await apiPost<string>('chapter/add', addData, userToken, lang);
chapterOrder: chapterOrder,
title: chapterTitle if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
}, userToken, lang); addToQueue('add_chapter', addData);
}
} }
if (!chapterId) { if (!chapterId) {
errorMessage(t("scribeChapterComponent.errorChapterSubmit", {chapterName: newChapterName})); errorMessage(t("scribeChapterComponent.errorChapterSubmit", {chapterName: newChapterName}));

View File

@@ -30,8 +30,10 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
const {isCurrentlyOffline} = useContext(OfflineContext); const {isCurrentlyOffline} = useContext(OfflineContext);
const { const {
serverSyncedBooks, serverSyncedBooks,
setServerSyncedBooks setServerSyncedBooks,
localSyncedBooks
}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext); }: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const useLocal: boolean = isDesktop && isCurrentlyOffline();
const [name, setName] = useState<string>(''); const [name, setName] = useState<string>('');
const [description, setDescription] = useState<string>(''); const [description, setDescription] = useState<string>('');
const [selectedBookIds, setSelectedBookIds] = useState<string[]>([]); const [selectedBookIds, setSelectedBookIds] = useState<string[]>([]);
@@ -83,6 +85,8 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
: book : book
) )
); );
} else {
setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => [...prev]);
} }
if (onSeriesCreated) { if (onSeriesCreated) {
@@ -150,7 +154,7 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
)} )}
</div> </div>
{serverSyncedBooks.length === 0 ? ( {(useLocal ? localSyncedBooks : serverSyncedBooks).length === 0 ? (
<div className="text-center py-6 text-muted"> <div className="text-center py-6 text-muted">
<Book className="w-8 h-8 mb-2 opacity-50" strokeWidth={1.75}/> <Book className="w-8 h-8 mb-2 opacity-50" strokeWidth={1.75}/>
<p>{t("addNewSeriesForm.noBooks")}</p> <p>{t("addNewSeriesForm.noBooks")}</p>
@@ -158,7 +162,7 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
) : ( ) : (
<div <div
className="max-h-48 overflow-y-auto rounded-xl border border-secondary bg-tertiary"> className="max-h-48 overflow-y-auto rounded-xl border border-secondary bg-tertiary">
{serverSyncedBooks.map((book: SyncedBook) => { {(useLocal ? localSyncedBooks : serverSyncedBooks).map((book: SyncedBook) => {
const isSelected: boolean = selectedBookIds.includes(book.id); const isSelected: boolean = selectedBookIds.includes(book.id);
return ( return (
<button <button

View File

@@ -10,6 +10,10 @@ import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {isDesktop} from '@/lib/configs'; import {isDesktop} from '@/lib/configs';
import {apiDelete} from '@/lib/api/client'; import {apiDelete} from '@/lib/api/client';
import {deleteSeries} from '@/lib/tauri'; import {deleteSeries} from '@/lib/tauri';
import {SeriesContext, SeriesContextProps} from '@/context/SeriesContext';
import {SeriesSyncContext, SeriesSyncContextProps} from '@/context/SeriesSyncContext';
import {SyncedSeries} from '@/lib/types/synced-series';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
interface SeriesSettingOption { interface SeriesSettingOption {
id: string; id: string;
@@ -36,6 +40,8 @@ export default function SeriesSettingSidebar(
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext); const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline} = useContext(OfflineContext); const {isCurrentlyOffline} = useContext(OfflineContext);
const {localSyncedSeries}: SeriesSyncContextProps = useContext<SeriesSyncContextProps>(SeriesSyncContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const userToken: string = session?.accessToken ? session?.accessToken : ''; const userToken: string = session?.accessToken ? session?.accessToken : '';
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
@@ -44,9 +50,15 @@ export default function SeriesSettingSidebar(
try { try {
const useLocal: boolean = isDesktop && isCurrentlyOffline(); const useLocal: boolean = isDesktop && isCurrentlyOffline();
const deletedAt: number = Math.floor(Date.now() / 1000); const deletedAt: number = Math.floor(Date.now() / 1000);
const success: boolean = useLocal let success: boolean;
? await deleteSeries(seriesId, deletedAt) if (useLocal) {
: await apiDelete<boolean>('series/delete', {seriesId: seriesId}, userToken, lang); success = await deleteSeries(seriesId, deletedAt);
} else {
success = await apiDelete<boolean>('series/delete', {seriesId: seriesId}, userToken, lang);
if (isDesktop && localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
addToQueue('delete_series', {seriesId, deletedAt});
}
}
if (success) { if (success) {
successMessage(t('seriesSetting.deleteSuccess')); successMessage(t('seriesSetting.deleteSuccess'));
onClose(); onClose();

View File

@@ -14,6 +14,9 @@ import {LangContext, LangContextProps} from "@/context/LangContext";
import {SeriesContext, SeriesContextProps} from "@/context/SeriesContext"; import {SeriesContext, SeriesContextProps} from "@/context/SeriesContext";
import {SeriesDetailResponse, SeriesUpdateResponse} from "@/lib/types/series"; import {SeriesDetailResponse, SeriesUpdateResponse} from "@/lib/types/series";
import PulseLoader from '@/components/ui/PulseLoader'; import PulseLoader from '@/components/ui/PulseLoader';
import {SeriesSyncContext, SeriesSyncContextProps} from '@/context/SeriesSyncContext';
import {SyncedSeries} from '@/lib/types/synced-series';
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () => Promise<void> }>) { function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () => Promise<void> }>) {
const t = useTranslations(); const t = useTranslations();
@@ -24,6 +27,8 @@ function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () =
const userToken: string = session?.accessToken ? session?.accessToken : ''; const userToken: string = session?.accessToken ? session?.accessToken : '';
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline} = useContext(OfflineContext); const {isCurrentlyOffline} = useContext(OfflineContext);
const {localSyncedSeries}: SeriesSyncContextProps = useContext<SeriesSyncContextProps>(SeriesSyncContext);
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const useLocal: boolean = isDesktop && isCurrentlyOffline(); const useLocal: boolean = isDesktop && isCurrentlyOffline();
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
@@ -69,9 +74,16 @@ function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () =
return; return;
} }
try { try {
const response: SeriesUpdateResponse = useLocal let response: SeriesUpdateResponse;
? {success: await updateSeries({seriesId, name, description})} as SeriesUpdateResponse const updateData = {seriesId, name, description};
: await apiPut<SeriesUpdateResponse>('series/update', {seriesId, name, description}, userToken, lang); if (useLocal) {
response = {success: await updateSeries(updateData)} as SeriesUpdateResponse;
} else {
response = await apiPut<SeriesUpdateResponse>('series/update', updateData, userToken, lang);
if (isDesktop && localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
addToQueue('update_series', updateData);
}
}
if (!response.success) { if (!response.success) {
errorMessage(t('seriesBasicInformation.error.update')); errorMessage(t('seriesBasicInformation.error.update'));
return; return;

View File

@@ -29,7 +29,8 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
const {seriesId}: SeriesContextProps = useContext<SeriesContextProps>(SeriesContext); const {seriesId}: SeriesContextProps = useContext<SeriesContextProps>(SeriesContext);
const { const {
serverSyncedBooks, serverSyncedBooks,
setServerSyncedBooks setServerSyncedBooks,
localSyncedBooks
}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext); }: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const userToken: string = session?.accessToken ? session?.accessToken : ''; const userToken: string = session?.accessToken ? session?.accessToken : '';
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
@@ -49,11 +50,12 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
useEffect(function () { useEffect(function () {
const booksInSeries: string[] = seriesBooks.map((book: SeriesBookProps) => book.bookId); const booksInSeries: string[] = seriesBooks.map((book: SeriesBookProps) => book.bookId);
const filteredBooks: SyncedBook[] = serverSyncedBooks.filter( const allBooks: SyncedBook[] = useLocal ? localSyncedBooks : serverSyncedBooks;
const filteredBooks: SyncedBook[] = allBooks.filter(
(book: SyncedBook) => !booksInSeries.includes(book.id) (book: SyncedBook) => !booksInSeries.includes(book.id)
); );
setAvailableBooks(filteredBooks); setAvailableBooks(filteredBooks);
}, [seriesBooks, serverSyncedBooks]); }, [seriesBooks, serverSyncedBooks, localSyncedBooks, useLocal]);
async function loadSeriesBooks(): Promise<void> { async function loadSeriesBooks(): Promise<void> {
setIsLoading(true); setIsLoading(true);
@@ -97,7 +99,8 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
: await apiPost<boolean>('series/book/add', {seriesId: seriesId, bookId: selectedBookToAdd}, userToken, lang); : await apiPost<boolean>('series/book/add', {seriesId: seriesId, bookId: selectedBookToAdd}, userToken, lang);
if (response) { if (response) {
const addedBook: SyncedBook | undefined = serverSyncedBooks.find( const allBooks: SyncedBook[] = useLocal ? localSyncedBooks : serverSyncedBooks;
const addedBook: SyncedBook | undefined = allBooks.find(
(book: SyncedBook) => book.id === selectedBookToAdd (book: SyncedBook) => book.id === selectedBookToAdd
); );
if (addedBook) { if (addedBook) {

View File

@@ -44,6 +44,9 @@ const KEYRING_USER: &str = "vault-key";
/// Falls back to the old derivation method if the keyring is unavailable, /// Falls back to the old derivation method if the keyring is unavailable,
/// and attempts to migrate the key into the keyring for next time. /// and attempts to migrate the key into the keyring for next time.
fn get_vault_key() -> [u8; 32] { fn get_vault_key() -> [u8; 32] {
if cfg!(debug_assertions) {
return derive_machine_key_legacy();
}
let entry = keyring::Entry::new(SERVICE_NAME, KEYRING_USER); let entry = keyring::Entry::new(SERVICE_NAME, KEYRING_USER);
if let Ok(entry) = &entry { if let Ok(entry) = &entry {
if let Ok(stored) = entry.get_password() { if let Ok(stored) = entry.get_password() {
@@ -56,7 +59,6 @@ fn get_vault_key() -> [u8; 32] {
} }
} }
} }
// No key in keyring yet — generate a random one
let mut key = [0u8; 32]; let mut key = [0u8; 32];
rand::rng().fill_bytes(&mut key); rand::rng().fill_bytes(&mut key);
let encoded = BASE64.encode(key); let encoded = BASE64.encode(key);
@@ -120,7 +122,6 @@ fn read_vault() -> AppResult<SecureVault> {
let raw = BASE64.decode(content.trim()) let raw = BASE64.decode(content.trim())
.map_err(|e| AppError::Keyring(format!("Vault corrupted (base64): {}", e)))?; .map_err(|e| AppError::Keyring(format!("Vault corrupted (base64): {}", e)))?;
// Try the new keyring-backed key first
let key = get_vault_key(); let key = get_vault_key();
if let Ok(decrypted) = decrypt_vault(&raw, &key) { if let Ok(decrypted) = decrypt_vault(&raw, &key) {
if let Ok(vault) = serde_json::from_slice::<SecureVault>(&decrypted) { if let Ok(vault) = serde_json::from_slice::<SecureVault>(&decrypted) {
@@ -128,16 +129,15 @@ fn read_vault() -> AppResult<SecureVault> {
} }
} }
// Fallback: try legacy key and migrate if successful
let legacy_key = derive_machine_key_legacy(); let legacy_key = derive_machine_key_legacy();
let decrypted = decrypt_vault(&raw, &legacy_key) if let Ok(decrypted) = decrypt_vault(&raw, &legacy_key) {
.map_err(|_| AppError::Keyring("Vault corrupted: unable to decrypt with any key.".to_string()))?; if let Ok(vault) = serde_json::from_slice::<SecureVault>(&decrypted) {
let vault: SecureVault = serde_json::from_slice(&decrypted) let _ = write_vault_with_key(&vault, &key);
.map_err(|e| AppError::Keyring(format!("Vault corrupted (json): {}", e)))?; return Ok(vault);
}
}
// Migrate: re-encrypt with the new keyring key Ok(SecureVault::default())
let _ = write_vault_with_key(&vault, &key);
Ok(vault)
} }
fn write_vault_with_key(vault: &SecureVault, key: &[u8; 32]) -> AppResult<()> { fn write_vault_with_key(vault: &SecureVault, key: &[u8; 32]) -> AppResult<()> {

View File

@@ -404,29 +404,6 @@ pub fn fetch_book_chapters(conn: &Connection, user_id: &str, book_id: &str, lang
Ok(rows) Ok(rows)
} }
/// Retrieves chapter information for a specific chapter.
pub fn fetch_book_chapter_infos(conn: &Connection, user_id: &str, chapter_id: &str, lang: Lang) -> AppResult<Vec<BookChapterInfosTable>> {
let mut statement = conn
.prepare("SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update FROM book_chapter_infos WHERE author_id=?1 AND chapter_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?;
let rows = statement
.query_map(params![user_id, chapter_id], |query_row| {
Ok(BookChapterInfosTable {
_chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
_act_id: query_row.get(2)?, _incident_id: query_row.get(3)?,
_plot_point_id: query_row.get(4)?, _book_id: query_row.get(5)?,
author_id: query_row.get(6)?, summary: query_row.get(7)?,
goal: query_row.get(8)?, last_update: query_row.get(9)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?;
Ok(rows)
}
pub fn fetch_all_chapter_infos_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookChapterInfosTable>> { pub fn fetch_all_chapter_infos_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookChapterInfosTable>> {
let mut statement = conn let mut statement = conn
.prepare("SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update FROM book_chapter_infos WHERE author_id=?1 AND book_id=?2") .prepare("SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update FROM book_chapter_infos WHERE author_id=?1 AND book_id=?2")

View File

@@ -172,33 +172,6 @@ pub fn is_chapter_content_exist(conn: &Connection, user_id: &str, content_id: &s
Ok(exists) Ok(exists)
} }
/// Fetches all chapter contents for a specific chapter belonging to a user.
/// * `conn` - Database connection
/// * `user_id` - The ID of the user/author
/// * `chapter_id` - The ID of the chapter
/// * `lang` - The language for error messages
/// Returns an array of book chapter content records.
pub fn fetch_book_chapter_contents(conn: &Connection, user_id: &str, chapter_id: &str, lang: Lang) -> AppResult<Vec<BookChapterContentTable>> {
let mut statement = conn
.prepare("SELECT content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update FROM book_chapter_content WHERE author_id=?1 AND chapter_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?;
let rows = statement
.query_map(params![user_id, chapter_id], |query_row| {
Ok(BookChapterContentTable {
content_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
author_id: query_row.get(2)?, version: query_row.get(3)?,
content: query_row.get(4)?, _words_count: query_row.get(5)?,
_time_on_it: query_row.get(6)?, last_update: query_row.get(7)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?;
Ok(rows)
}
pub fn fetch_all_chapter_contents_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookChapterContentTable>> { pub fn fetch_all_chapter_contents_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookChapterContentTable>> {
let mut statement = conn let mut statement = conn
.prepare("SELECT bcc.content_id, bcc.chapter_id, bcc.author_id, bcc.version, bcc.content, bcc.words_count, bcc.time_on_it, bcc.last_update FROM book_chapter_content bcc INNER JOIN book_chapters bc ON bcc.chapter_id = bc.chapter_id WHERE bcc.author_id=?1 AND bc.book_id=?2") .prepare("SELECT bcc.content_id, bcc.chapter_id, bcc.author_id, bcc.version, bcc.content, bcc.words_count, bcc.time_on_it, bcc.last_update FROM book_chapter_content bcc INNER JOIN book_chapters bc ON bcc.chapter_id = bc.chapter_id WHERE bcc.author_id=?1 AND bc.book_id=?2")

View File

@@ -416,32 +416,6 @@ pub fn fetch_book_characters(conn: &Connection, user_id: &str, book_id: &str, la
Ok(characters) Ok(characters)
} }
/// Fetches all attributes for a specific character.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `character_id` - The unique identifier of the character
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of character attributes.
pub fn fetch_book_characters_attributes(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<Vec<BookCharactersAttributesTable>> {
let mut statement = conn
.prepare("SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM book_characters_attributes WHERE user_id=?1 AND character_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?;
let attributes = statement
.query_map(params![user_id, character_id], |query_row| {
Ok(BookCharactersAttributesTable {
attr_id: query_row.get(0)?, character_id: query_row.get(1)?,
user_id: query_row.get(2)?, attribute_name: query_row.get(3)?,
attribute_value: query_row.get(4)?, last_update: query_row.get(5)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?;
Ok(attributes)
}
pub fn fetch_all_character_attributes_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookCharactersAttributesTable>> { pub fn fetch_all_character_attributes_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookCharactersAttributesTable>> {
let mut statement = conn let mut statement = conn
.prepare("SELECT bca.attr_id, bca.character_id, bca.user_id, bca.attribute_name, bca.attribute_value, bca.last_update FROM book_characters_attributes bca INNER JOIN book_characters bc ON bca.character_id = bc.character_id WHERE bca.user_id=?1 AND bc.book_id=?2") .prepare("SELECT bca.attr_id, bca.character_id, bca.user_id, bca.attribute_name, bca.attribute_value, bca.last_update FROM book_characters_attributes bca INNER JOIN book_characters bc ON bca.character_id = bc.character_id WHERE bca.user_id=?1 AND bc.book_id=?2")

View File

@@ -425,33 +425,6 @@ pub fn fetch_book_locations(conn: &Connection, user_id: &str, book_id: &str, lan
Ok(book_locations) Ok(book_locations)
} }
/// Fetches all elements for a specific location.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `location_id` - The location's unique identifier
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of location element records.
pub fn fetch_location_elements(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult<Vec<LocationElementTable>> {
let mut statement = conn
.prepare("SELECT element_id, location, user_id, element_name, original_name, element_description, last_update FROM location_element WHERE user_id = ?1 AND location = ?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?;
let location_elements = statement
.query_map(params![user_id, location_id], |query_row| {
Ok(LocationElementTable {
element_id: query_row.get(0)?, location: query_row.get(1)?,
user_id: query_row.get(2)?, element_name: query_row.get(3)?,
original_name: query_row.get(4)?, element_description: query_row.get(5)?,
last_update: query_row.get(6)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?;
Ok(location_elements)
}
pub fn fetch_all_location_elements_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<LocationElementTable>> { pub fn fetch_all_location_elements_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<LocationElementTable>> {
let mut statement = conn let mut statement = conn
.prepare("SELECT e.element_id, e.location, e.user_id, e.element_name, e.original_name, e.element_description, e.last_update FROM location_element e INNER JOIN book_location l ON e.location = l.loc_id AND e.user_id = l.user_id WHERE e.user_id = ?1 AND l.book_id = ?2") .prepare("SELECT e.element_id, e.location, e.user_id, e.element_name, e.original_name, e.element_description, e.last_update FROM location_element e INNER JOIN book_location l ON e.location = l.loc_id AND e.user_id = l.user_id WHERE e.user_id = ?1 AND l.book_id = ?2")
@@ -494,33 +467,6 @@ pub fn fetch_all_location_sub_elements_by_book(conn: &Connection, user_id: &str,
Ok(location_sub_elements) Ok(location_sub_elements)
} }
/// Fetches all sub-elements for a specific location element.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `element_id` - The element's unique identifier
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of location sub-element records.
pub fn fetch_location_sub_elements(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult<Vec<LocationSubElementTable>> {
let mut statement = conn
.prepare("SELECT sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update FROM location_sub_element WHERE user_id = ?1 AND element_id = ?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?;
let location_sub_elements = statement
.query_map(params![user_id, element_id], |query_row| {
Ok(LocationSubElementTable {
sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?,
user_id: query_row.get(2)?, sub_elem_name: query_row.get(3)?,
original_name: query_row.get(4)?, sub_elem_description: query_row.get(5)?,
last_update: query_row.get(6)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?;
Ok(location_sub_elements)
}
/// Fetches all synced locations for a user (used for synchronization). /// Fetches all synced locations for a user (used for synchronization).
/// * `conn` - Database connection /// * `conn` - Database connection
/// * `user_id` - The user's unique identifier /// * `user_id` - The user's unique identifier

View File

@@ -281,33 +281,6 @@ pub fn fetch_book_worlds(conn: &Connection, user_id: &str, book_id: &str, lang:
Ok(worlds) Ok(worlds)
} }
/// Fetches all elements for a specific world.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `world_id` - The unique identifier of the world
/// * `lang` - The language for error messages ("fr" or "en")
/// Returns an array of book world elements table records.
pub fn fetch_book_world_elements(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult<Vec<BookWorldElementsTable>> {
let mut statement = conn
.prepare("SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM book_world_elements WHERE user_id=?1 AND world_id=?2")
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les \u{00e9}l\u{00e9}ments du monde.".to_string() } else { "Unable to retrieve world elements.".to_string() }))?;
let elements = statement
.query_map(params![user_id, world_id], |query_row| {
Ok(BookWorldElementsTable {
element_id: query_row.get(0)?, world_id: query_row.get(1)?,
user_id: query_row.get(2)?, element_type: query_row.get(3)?,
name: query_row.get(4)?, original_name: query_row.get(5)?,
description: query_row.get(6)?, last_update: query_row.get(7)?,
})
})
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les \u{00e9}l\u{00e9}ments du monde.".to_string() } else { "Unable to retrieve world elements.".to_string() }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les \u{00e9}l\u{00e9}ments du monde.".to_string() } else { "Unable to retrieve world elements.".to_string() }))?;
Ok(elements)
}
pub fn fetch_all_world_elements_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookWorldElementsTable>> { pub fn fetch_all_world_elements_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookWorldElementsTable>> {
let mut statement = conn let mut statement = conn
.prepare("SELECT e.element_id, e.world_id, e.user_id, e.element_type, e.name, e.original_name, e.description, e.last_update FROM book_world_elements e INNER JOIN book_world w ON e.world_id = w.world_id AND e.user_id = w.author_id WHERE e.user_id=?1 AND w.book_id=?2") .prepare("SELECT e.element_id, e.world_id, e.user_id, e.element_type, e.name, e.original_name, e.description, e.last_update FROM book_world_elements e INNER JOIN book_world w ON e.world_id = w.world_id AND e.user_id = w.author_id WHERE e.user_id=?1 AND w.book_id=?2")