Enhance synchronization logic and offline handling

- Refactor components to support conditional offline and online CRUD operations.
- Introduce `addToQueue` mechanism for syncing offline changes to the server.
- Add `isChapterContentExist` method and related existence checks in repositories.
- Consolidate data structures and streamline book, chapter, character, and guideline synchronization workflows.
- Encrypt additional character fields and adjust repository inserts for offline data.
This commit is contained in:
natreex
2026-01-07 20:43:34 -05:00
parent fa05d6dbae
commit 8eab6fd771
21 changed files with 557 additions and 578 deletions

View File

@@ -21,6 +21,9 @@ import ActPlotPoints from '@/components/book/settings/story/act/ActPlotPoints';
import {useTranslations} from 'next-intl';
import {LangContext, LangContextProps} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook";
interface ActProps {
acts: ActType[];
@@ -32,6 +35,8 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
const t = useTranslations('actComponent');
const {lang} = useContext<LangContextProps>(LangContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {book} = useContext(BookContext);
const {session} = useContext(SessionContext);
const {errorMessage, successMessage} = useContext(AlertContext);
@@ -74,22 +79,23 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
try {
let incidentId: string;
if (isCurrentlyOffline()) {
if (isCurrentlyOffline() || book?.localBook) {
incidentId = await window.electron.invoke<string>('db:book:incident:add', {
bookId,
name: newIncidentTitle,
});
} else {
if (book?.localBook) {
incidentId = await window.electron.invoke<string>('db:book:incident:add', {
incidentId = await System.authPostToServer<string>('book/incident/new', {
bookId,
name: newIncidentTitle,
}, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:book:incident:add', {
bookId,
incidentId,
name: newIncidentTitle,
});
} else {
incidentId = await System.authPostToServer<string>('book/incident/new', {
bookId,
name: newIncidentTitle,
}, token, lang);
}
}
if (!incidentId) {
@@ -104,7 +110,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
summary: '',
chapters: [],
};
return {
...act,
incidents: [...(act.incidents || []), newIncident],
@@ -126,22 +132,14 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
async function deleteIncident(actId: number, incidentId: string): Promise<void> {
try {
let response: boolean;
if (isCurrentlyOffline()) {
response = await window.electron.invoke<boolean>('db:book:incident:remove', {
bookId,
incidentId,
});
const deleteData = { bookId, incidentId };
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:incident:remove', deleteData);
} else {
if (book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:incident:remove', {
bookId,
incidentId,
});
} else {
response = await System.authDeleteToServer<boolean>('book/incident/remove', {
bookId,
incidentId,
}, token, lang);
response = await System.authDeleteToServer<boolean>('book/incident/remove', deleteData, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:book:incident:remove', deleteData);
}
}
if (!response) {
@@ -173,25 +171,21 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
if (newPlotPointTitle.trim() === '') return;
try {
let plotId: string;
if (isCurrentlyOffline()) {
plotId = await window.electron.invoke<string>('db:book:plot:add', {
bookId,
name: newPlotPointTitle,
incidentId: selectedIncidentId,
});
const plotData = {
bookId,
name: newPlotPointTitle,
incidentId: selectedIncidentId,
};
if (isCurrentlyOffline() || book?.localBook) {
plotId = await window.electron.invoke<string>('db:book:plot:add', plotData);
} else {
if (book?.localBook) {
plotId = await window.electron.invoke<string>('db:book:plot:add', {
bookId,
name: newPlotPointTitle,
incidentId: selectedIncidentId,
plotId = await System.authPostToServer<string>('book/plot/new', plotData, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:book:plot:add', {
...plotData,
plotId,
});
} else {
plotId = await System.authPostToServer<string>('book/plot/new', {
bookId,
name: newPlotPointTitle,
incidentId: selectedIncidentId,
}, token, lang);
}
}
if (!plotId) {
@@ -229,19 +223,14 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
async function deletePlotPoint(actId: number, plotPointId: string): Promise<void> {
try {
let response: boolean;
if (isCurrentlyOffline()) {
response = await window.electron.invoke<boolean>('db:book:plot:remove', {
plotId: plotPointId,
});
const deleteData = { plotId: plotPointId };
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:plot:remove', deleteData);
} else {
if (book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:plot:remove', {
plotId: plotPointId,
});
} else {
response = await System.authDeleteToServer<boolean>('book/plot/remove', {
plotId: plotPointId,
}, token, lang);
response = await System.authDeleteToServer<boolean>('book/plot/remove', deleteData, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:book:plot:remove', deleteData);
}
}
if (!response) {
@@ -289,13 +278,16 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
plotId: destination === 'plotPoint' ? itemId : null,
incidentId: destination === 'incident' ? itemId : null,
};
if (isCurrentlyOffline()) {
if (isCurrentlyOffline() || book?.localBook) {
linkId = await window.electron.invoke<string>('db:chapter:information:add', linkData);
} else {
if (book?.localBook) {
linkId = await window.electron.invoke<string>('db:chapter:information:add', linkData);
} else {
linkId = await System.authPostToServer<string>('chapter/resume/add', linkData, token, lang);
linkId = await System.authPostToServer<string>('chapter/resume/add', linkData, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:chapter:information:add', {
...linkData,
chapterInfoId: linkId,
});
}
}
if (!linkId) {
@@ -373,19 +365,14 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
): Promise<void> {
try {
let response: boolean;
if (isCurrentlyOffline()) {
response = await window.electron.invoke<boolean>('db:chapter:information:remove', {
chapterInfoId,
});
const unlinkData = { chapterInfoId };
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:chapter:information:remove', unlinkData);
} else {
if (book?.localBook) {
response = await window.electron.invoke<boolean>('db:chapter:information:remove', {
chapterInfoId,
});
} else {
response = await System.authDeleteToServer<boolean>('chapter/resume/remove', {
chapterInfoId,
}, token, lang);
response = await System.authDeleteToServer<boolean>('chapter/resume/remove', unlinkData, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:chapter:information:remove', unlinkData);
}
}
if (!response) {

View File

@@ -10,6 +10,9 @@ import CollapsableArea from "@/components/CollapsableArea";
import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook";
interface IssuesProps {
issues: Issue[];
@@ -20,6 +23,8 @@ export default function Issues({issues, setIssues}: IssuesProps) {
const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {book} = useContext(BookContext);
const {session} = useContext(SessionContext);
const {errorMessage} = useContext(AlertContext);
@@ -36,22 +41,23 @@ export default function Issues({issues, setIssues}: IssuesProps) {
}
try {
let issueId: string;
if (isCurrentlyOffline()) {
if (isCurrentlyOffline() || book?.localBook) {
issueId = await window.electron.invoke<string>('db:book:issue:add', {
bookId,
name: newIssueName,
});
} else {
if (book?.localBook) {
issueId = await window.electron.invoke<string>('db:book:issue:add', {
issueId = await System.authPostToServer<string>('book/issue/add', {
bookId,
name: newIssueName,
}, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:book:issue:add', {
bookId,
issueId,
name: newIssueName,
});
} else {
issueId = await System.authPostToServer<string>('book/issue/add', {
bookId,
name: newIssueName,
}, token, lang);
}
}
if (!issueId) {
@@ -62,7 +68,7 @@ export default function Issues({issues, setIssues}: IssuesProps) {
name: newIssueName,
id: issueId,
};
setIssues([...issues, newIssue]);
setNewIssueName('');
} catch (e: unknown) {
@@ -77,32 +83,32 @@ export default function Issues({issues, setIssues}: IssuesProps) {
async function deleteIssue(issueId: string): Promise<void> {
if (issueId === undefined) {
errorMessage(t("issues.errorInvalidId"));
return;
}
try {
let response: boolean;
if (isCurrentlyOffline()) {
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:issue:remove', {
bookId,
issueId,
});
} else {
if (book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:issue:remove', {
response = await System.authDeleteToServer<boolean>(
'book/issue/remove',
{
bookId,
issueId,
},
token,
lang
);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:book:issue:remove', {
bookId,
issueId,
});
} else {
response = await System.authDeleteToServer<boolean>(
'book/issue/remove',
{
bookId,
issueId,
},
token,
lang
);
}
}
if (response) {

View File

@@ -13,6 +13,9 @@ import CollapsableArea from "@/components/CollapsableArea";
import {useTranslations} from "next-intl";
import {LangContext} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook";
interface MainChapterProps {
chapters: ChapterListProps[];
@@ -23,6 +26,8 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) {
const t = useTranslations();
const {lang} = useContext(LangContext)
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {book} = useContext(BookContext);
const {session} = useContext(SessionContext);
const {errorMessage, successMessage} = useContext(AlertContext);
@@ -85,13 +90,13 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) {
bookId,
chapterId: chapterIdToRemove,
};
if (isCurrentlyOffline()) {
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:chapter:remove', deleteData);
} else {
if (book?.localBook) {
response = await window.electron.invoke<boolean>('db:chapter:remove', deleteData);
} else {
response = await System.authDeleteToServer<boolean>('chapter/remove', deleteData, token, lang);
response = await System.authDeleteToServer<boolean>('chapter/remove', deleteData, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:chapter:remove', deleteData);
}
}
if (!response) {
@@ -121,13 +126,16 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) {
chapterOrder: newChapterOrder ? newChapterOrder : 0,
title: newChapterTitle,
};
if (isCurrentlyOffline()) {
if (isCurrentlyOffline() || book?.localBook) {
responseId = await window.electron.invoke<string>('db:chapter:add', chapterData);
} else {
if (book?.localBook) {
responseId = await window.electron.invoke<string>('db:chapter:add', chapterData);
} else {
responseId = await System.authPostToServer<string>('chapter/add', chapterData, token);
responseId = await System.authPostToServer<string>('chapter/add', chapterData, token);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:chapter:add', {
...chapterData,
chapterId: responseId,
});
}
}
if (!responseId) {

View File

@@ -13,6 +13,9 @@ import Act from "@/components/book/settings/story/Act";
import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook";
export const StoryContext = createContext<{
acts: ActType[];
@@ -43,6 +46,8 @@ export function Story(props: any, ref: any) {
const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {book} = useContext(BookContext);
const bookId: string = book?.bookId ? book.bookId.toString() : '';
const {session} = useContext(SessionContext);
@@ -137,13 +142,13 @@ export function Story(props: any, ref: any) {
mainChapters,
issues,
};
if (isCurrentlyOffline()) {
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:story:update', storyData);
} else {
if (book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:story:update', storyData);
} else {
response = await System.authPostToServer<boolean>('book/story', storyData, userToken, lang);
response = await System.authPostToServer<boolean>('book/story', storyData, userToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:book:story:update', storyData);
}
}
if (!response) {