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

@@ -35,12 +35,36 @@ import OfflinePinSetup from "@/components/offline/OfflinePinSetup";
import OfflinePinVerify from "@/components/offline/OfflinePinVerify"; import OfflinePinVerify from "@/components/offline/OfflinePinVerify";
import {SyncedBook, BookSyncCompare, compareBookSyncs} from "@/lib/models/SyncedBook"; import {SyncedBook, BookSyncCompare, compareBookSyncs} from "@/lib/models/SyncedBook";
import {BooksSyncContext} from "@/context/BooksSyncContext"; import {BooksSyncContext} from "@/context/BooksSyncContext";
import useSyncBooks from "@/hooks/useSyncBooks";
import {LocalSyncQueueContext, LocalSyncOperation} from "@/context/SyncQueueContext";
const messagesMap = { const messagesMap = {
fr: frMessages, fr: frMessages,
en: enMessages en: enMessages
}; };
function AutoSyncOnReconnect() {
const {offlineMode} = useContext(OfflineContext);
const {syncAllToServer, refreshBooks, booksToSyncToServer} = useSyncBooks();
const [pendingSync, setPendingSync] = useState<boolean>(false);
useEffect((): void => {
if (!offlineMode.isOffline) {
setPendingSync(true);
refreshBooks();
}
}, [offlineMode.isOffline]);
useEffect((): void => {
if (pendingSync && booksToSyncToServer.length > 0) {
syncAllToServer();
setPendingSync(false);
}
}, [booksToSyncToServer, pendingSync]);
return null;
}
function ScribeContent() { function ScribeContent() {
const t = useTranslations(); const t = useTranslations();
const {lang: locale} = useContext(LangContext); const {lang: locale} = useContext(LangContext);
@@ -79,7 +103,48 @@ function ScribeContent() {
const [homeStepsGuide, setHomeStepsGuide] = useState<boolean>(false); const [homeStepsGuide, setHomeStepsGuide] = useState<boolean>(false);
const [showPinSetup, setShowPinSetup] = useState<boolean>(false); const [showPinSetup, setShowPinSetup] = useState<boolean>(false);
const [showPinVerify, setShowPinVerify] = useState<boolean>(false); const [showPinVerify, setShowPinVerify] = useState<boolean>(false);
const [localSyncQueue, setLocalSyncQueue] = useState<LocalSyncOperation[]>([]);
const [isQueueProcessing, setIsQueueProcessing] = useState<boolean>(false);
function addToLocalSyncQueue(channel: string, data: Record<string, unknown>): void {
const operation: LocalSyncOperation = {
id: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
channel,
data,
timestamp: Date.now(),
};
setLocalSyncQueue((prev: LocalSyncOperation[]): LocalSyncOperation[] => [...prev, operation]);
}
useEffect((): void => {
if (localSyncQueue.length === 0 || isQueueProcessing) {
return;
}
async function processQueue(): Promise<void> {
setIsQueueProcessing(true);
const queueCopy: LocalSyncOperation[] = [...localSyncQueue];
for (const operation of queueCopy) {
try {
await window.electron.invoke(operation.channel, operation.data);
setLocalSyncQueue((prev: LocalSyncOperation[]): LocalSyncOperation[] =>
prev.filter((op: LocalSyncOperation): boolean => op.id !== operation.id)
);
} catch (error) {
console.error(`[LocalSyncQueue] Failed to process operation ${operation.channel}:`, error);
}
}
setIsQueueProcessing(false);
}
processQueue().then();
}, [localSyncQueue, isQueueProcessing]);
const homeSteps: GuideStep[] = [ const homeSteps: GuideStep[] = [
{ {
id: 0, id: 0,
@@ -212,6 +277,7 @@ function ScribeContent() {
console.log('bookSyncDiffsFromServer', bookSyncDiffsFromServer); console.log('bookSyncDiffsFromServer', bookSyncDiffsFromServer);
console.log('bookSyncDiffsToServer', bookSyncDiffsToServer); console.log('bookSyncDiffsToServer', bookSyncDiffsToServer);
}, [localSyncedBooks, serverSyncedBooks,localOnlyBooks, bookSyncDiffsFromServer, bookSyncDiffsToServer]); }, [localSyncedBooks, serverSyncedBooks,localOnlyBooks, bookSyncDiffsFromServer, bookSyncDiffsToServer]);
async function getBooks(): Promise<void> { async function getBooks(): Promise<void> {
try { try {
@@ -313,7 +379,6 @@ function ScribeContent() {
setHomeStepsGuide(false); setHomeStepsGuide(false);
} }
} else { } else {
// Mode offline: stocker dans localStorage
const completedGuides = JSON.parse(localStorage.getItem('completedGuides') || '[]'); const completedGuides = JSON.parse(localStorage.getItem('completedGuides') || '[]');
if (!completedGuides.includes('home-basic')) { if (!completedGuides.includes('home-basic')) {
completedGuides.push('home-basic'); completedGuides.push('home-basic');
@@ -535,10 +600,17 @@ function ScribeContent() {
return ( return (
<SessionContext.Provider value={{session: session, setSession: setSession}}> <SessionContext.Provider value={{session: session, setSession: setSession}}>
<BooksSyncContext.Provider value={{serverSyncedBooks, localSyncedBooks, booksToSyncFromServer:bookSyncDiffsFromServer, booksToSyncToServer:bookSyncDiffsToServer, setServerOnlyBooks, setLocalOnlyBooks, serverOnlyBooks, localOnlyBooks}}> <LocalSyncQueueContext.Provider value={{
<BookContext.Provider value={{book: currentBook, setBook: setCurrentBook}}> queue: localSyncQueue,
<ChapterContext.Provider value={{chapter: currentChapter, setChapter: setCurrentChapter}}> setQueue: setLocalSyncQueue,
<AIUsageContext.Provider value={{ addToQueue: addToLocalSyncQueue,
isProcessing: isQueueProcessing,
}}>
<BooksSyncContext.Provider value={{serverSyncedBooks, localSyncedBooks, booksToSyncFromServer:bookSyncDiffsFromServer, booksToSyncToServer:bookSyncDiffsToServer, setServerSyncedBooks, setLocalSyncedBooks, setServerOnlyBooks, setLocalOnlyBooks, serverOnlyBooks, localOnlyBooks}}>
<AutoSyncOnReconnect/>
<BookContext.Provider value={{book: currentBook, setBook: setCurrentBook}}>
<ChapterContext.Provider value={{chapter: currentChapter, setChapter: setCurrentChapter}}>
<AIUsageContext.Provider value={{
totalCredits: currentCredits, totalCredits: currentCredits,
setTotalCredits: setCurrentCredits, setTotalCredits: setCurrentCredits,
totalPrice: amountSpent, totalPrice: amountSpent,
@@ -586,10 +658,11 @@ function ScribeContent() {
/> />
) )
} }
</AIUsageContext.Provider> </AIUsageContext.Provider>
</ChapterContext.Provider> </ChapterContext.Provider>
</BookContext.Provider> </BookContext.Provider>
</BooksSyncContext.Provider> </BooksSyncContext.Provider>
</LocalSyncQueueContext.Provider>
</SessionContext.Provider> </SessionContext.Provider>
); );
} }

View File

@@ -26,7 +26,6 @@ export default function ScribeFooterBar() {
}, [editor?.state.doc.textContent]); }, [editor?.state.doc.textContent]);
function getWordCount(): void { function getWordCount(): void {
console.log(editor)
if (editor) { if (editor) {
try { try {
const content: string = editor?.state.doc.textContent; const content: string = editor?.state.doc.textContent;

View File

@@ -3,13 +3,8 @@ import {faCloud, faCloudArrowDown, faCloudArrowUp, faSpinner} from "@fortawesome
import {useTranslations} from "next-intl"; import {useTranslations} from "next-intl";
import {useState, useContext} from "react"; import {useState, useContext} from "react";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import System from "@/lib/models/System"; import {SyncType} from "@/context/BooksSyncContext";
import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import useSyncBooks from "@/hooks/useSyncBooks";
import {LangContext} from "@/context/LangContext";
import {CompleteBook} from "@/lib/models/Book";
import {BooksSyncContext, BooksSyncContextProps, SyncType} from "@/context/BooksSyncContext";
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import {BookSyncCompare, SyncedBook} from "@/lib/models/SyncedBook";
interface SyncBookProps { interface SyncBookProps {
bookId: string; bookId: string;
@@ -18,148 +13,43 @@ interface SyncBookProps {
export default function SyncBook({bookId, status}: SyncBookProps) { export default function SyncBook({bookId, status}: SyncBookProps) {
const t = useTranslations(); const t = useTranslations();
const {session} = useContext<SessionContextProps>(SessionContext);
const {lang} = useContext(LangContext);
const {errorMessage} = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [currentStatus, setCurrentStatus] = useState<SyncType>(status); const [currentStatus, setCurrentStatus] = useState<SyncType>(status);
const {booksToSyncToServer, booksToSyncFromServer,serverSyncedBooks,localSyncedBooks,setLocalOnlyBooks, setServerOnlyBooks} = useContext<BooksSyncContextProps>(BooksSyncContext) const {upload: hookUpload, download: hookDownload, syncFromServer: hookSyncFromServer, syncToServer: hookSyncToServer} = useSyncBooks();
const isOffline: boolean = isCurrentlyOffline(); const isOffline: boolean = isCurrentlyOffline();
async function upload(): Promise<void> { async function upload(): Promise<void> {
if (isOffline) { if (isOffline) return;
return;
}
setIsLoading(true); setIsLoading(true);
try { const success = await hookUpload(bookId);
const bookToSync: CompleteBook = await window.electron.invoke<CompleteBook>('db:book:uploadToServer', bookId); if (success) setCurrentStatus('synced');
if (!bookToSync) { setIsLoading(false);
errorMessage(t("bookCard.uploadError"));
return;
}
const response: boolean = await System.authPostToServer('book/sync/upload', {
book: bookToSync
}, session.accessToken, lang);
if (!response) {
errorMessage(t("bookCard.uploadError"));
return;
}
setCurrentStatus('synced');
setLocalOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => {
return prevBooks.filter((book: SyncedBook): boolean => book.id !== bookId)
});
} catch (e:unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("bookCard.uploadError"));
}
} finally {
setIsLoading(false);
}
} }
async function download(): Promise<void> { async function download(): Promise<void> {
if (isOffline) { if (isOffline) return;
return;
}
setIsLoading(true); setIsLoading(true);
try { const success = await hookDownload(bookId);
const response: CompleteBook = await System.authGetQueryToServer('book/sync/download', session.accessToken, lang, {bookId}); if (success) setCurrentStatus('synced');
if (!response) { setIsLoading(false);
errorMessage(t("bookCard.downloadError"));
return;
}
const syncStatus:boolean = await window.electron.invoke<boolean>('db:book:syncSave', response);
if (!syncStatus) {
errorMessage(t("bookCard.downloadError"));
return;
}
setCurrentStatus('synced');
setServerOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => {
return prevBooks.filter((book: SyncedBook): boolean => book.id !== bookId)
});
} catch (e:unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("bookCard.downloadError"));
}
} finally {
setIsLoading(false);
}
} }
async function syncFromServer(): Promise<void> { async function syncFromServer(): Promise<void> {
if (isOffline) { if (isOffline) return;
return;
}
setIsLoading(true); setIsLoading(true);
try { const success = await hookSyncFromServer(bookId);
const bookToFetch:BookSyncCompare|undefined = booksToSyncFromServer.find((book:BookSyncCompare):boolean => book.id === bookId); if (success) setCurrentStatus('synced');
if (!bookToFetch) { setIsLoading(false);
errorMessage(t("bookCard.syncFromServerError"));
return;
}
const response: CompleteBook = await System.authPostToServer('book/sync/server-to-client', {
bookToSync: bookToFetch
}, session.accessToken, lang);
if (!response) {
errorMessage(t("bookCard.syncFromServerError"));
return;
}
const syncStatus:boolean = await window.electron.invoke<boolean>('db:book:sync:toClient', response);
if (!syncStatus) {
errorMessage(t("bookCard.syncFromServerError"));
return;
}
setCurrentStatus('synced');
} catch (e:unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("bookCard.syncFromServerError"));
}
} finally {
setIsLoading(false);
}
} }
async function syncToServer(): Promise<void> { async function syncToServer(): Promise<void> {
if (isOffline) { if (isOffline) return;
return;
}
setIsLoading(true); setIsLoading(true);
try { const success = await hookSyncToServer(bookId);
const bookToFetch:BookSyncCompare|undefined = booksToSyncToServer.find((book:BookSyncCompare):boolean => book.id === bookId); if (success) setCurrentStatus('synced');
if (!bookToFetch) { setIsLoading(false);
errorMessage(t("bookCard.syncToServerError"));
return;
}
const bookToSync: CompleteBook = await window.electron.invoke<CompleteBook>('db:book:sync:toServer', bookToFetch);
if (!bookToSync) {
errorMessage(t("bookCard.syncToServerError"));
return;
}
const response: boolean = await System.authPatchToServer('book/sync/client-to-server', {
book: bookToSync
}, session.accessToken, lang);
if (!response) {
errorMessage(t("bookCard.syncToServerError"));
return;
}
setCurrentStatus('synced');
} catch (e:unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("bookCard.syncToServerError"));
}
} finally {
setIsLoading(false);
}
} }
if (isLoading) { if (isLoading) {

View File

@@ -138,19 +138,11 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<
}; };
let bookId: string; let bookId: string;
if (!isCurrentlyOffline()) { if (isCurrentlyOffline()) {
// Online - call API server
bookId = await System.authPostToServer<string>('book/add', {
title: title,
subTitle: subtitle,
type: selectedBookType,
summary: summary,
serie: 0,
publicationDate: publicationDate,
desiredWordCount: wordCount,
}, token, lang);
} else {
bookId = await window.electron.invoke<string>('db:book:create', bookData); bookId = await window.electron.invoke<string>('db:book:create', bookData);
} else {
// Online - call API server
bookId = await System.authPostToServer<string>('book/add', bookData, token, lang);
} }
if (!bookId) { if (!bookId) {

View File

@@ -17,11 +17,16 @@ import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext"; import {LangContext, LangContextProps} from "@/context/LangContext";
import {BookProps} from "@/lib/models/Book"; import {BookProps} from "@/lib/models/Book";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook";
function BasicInformationSetting(props: any, ref: any) { function BasicInformationSetting(props: any, ref: any) {
const t = useTranslations(); const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext) const {lang} = useContext<LangContextProps>(LangContext)
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {session} = useContext(SessionContext); const {session} = useContext(SessionContext);
const {book, setBook} = useContext(BookContext); const {book, setBook} = useContext(BookContext);
@@ -117,34 +122,21 @@ function BasicInformationSetting(props: any, ref: any) {
} }
try { try {
let response: boolean; let response: boolean;
if (isCurrentlyOffline()) { const basicInfoData = {
response = await window.electron.invoke<boolean>('db:book:updateBasicInformation', { title: title,
title: title, subTitle: subTitle,
subTitle: subTitle, summary: summary,
summary: summary, publicationDate: publicationDate,
publicationDate: publicationDate, wordCount: wordCount,
wordCount: wordCount, bookId: bookId
bookId: bookId };
}); if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:updateBasicInformation', basicInfoData);
} else { } else {
if (book?.localBook) { response = await System.authPostToServer<boolean>('book/basic-information', basicInfoData, userToken, lang);
response = await window.electron.invoke<boolean>('db:book:updateBasicInformation', {
title: title, if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
subTitle: subTitle, addToQueue('db:book:updateBasicInformation', basicInfoData);
summary: summary,
publicationDate: publicationDate,
wordCount: wordCount,
bookId: bookId
});
} else {
response = await System.authPostToServer<boolean>('book/basic-information', {
title: title,
subTitle: subTitle,
summary: summary,
publicationDate: publicationDate,
wordCount: wordCount,
bookId: bookId
}, userToken, lang);
} }
} }
if (!response) { if (!response) {

View File

@@ -7,7 +7,6 @@ import {LangContext, LangContextProps} from "@/context/LangContext";
import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import AlertBox from "@/components/AlertBox"; import AlertBox from "@/components/AlertBox";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {BookContext} from "@/context/BookContext";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook"; import {SyncedBook} from "@/lib/models/SyncedBook";
@@ -19,7 +18,6 @@ export default function DeleteBook({bookId}: DeleteBookProps) {
const {session} = useContext(SessionContext); const {session} = useContext(SessionContext);
const {lang} = useContext<LangContextProps>(LangContext) const {lang} = useContext<LangContextProps>(LangContext)
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {book} = useContext(BookContext);
const [showConfirmBox, setShowConfirmBox] = useState<boolean>(false); const [showConfirmBox, setShowConfirmBox] = useState<boolean>(false);
const {errorMessage} = useContext<AlertContextProps>(AlertContext) const {errorMessage} = useContext<AlertContextProps>(AlertContext)
const {serverOnlyBooks,setServerOnlyBooks,localOnlyBooks,setLocalOnlyBooks} = useContext<BooksSyncContextProps>(BooksSyncContext); const {serverOnlyBooks,setServerOnlyBooks,localOnlyBooks,setLocalOnlyBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
@@ -31,38 +29,26 @@ export default function DeleteBook({bookId}: DeleteBookProps) {
async function handleDeleteBook(): Promise<void> { async function handleDeleteBook(): Promise<void> {
try { try {
let response: boolean; let response: boolean;
if (isCurrentlyOffline()) { const deleteData = { id: bookId };
response = await window.electron.invoke<boolean>('db:book:delete', { const ifLocalBook: SyncedBook | undefined = localOnlyBooks.find((book: SyncedBook): boolean => book.id === bookId);
id: bookId,
}); if (isCurrentlyOffline() || ifLocalBook) {
response = await window.electron.invoke<boolean>('db:book:delete', deleteData);
} else { } else {
const ifLocalBook:SyncedBook|undefined = localOnlyBooks.find((book: SyncedBook):boolean => book.id === bookId); response = await System.authDeleteToServer<boolean>(
if (ifLocalBook) { `book/delete`,
response = await window.electron.invoke<boolean>('db:book:delete', { deleteData,
id: bookId, session.accessToken,
}); lang
} else { );
response = await window.electron.invoke<boolean>('db:book:delete', {
id: bookId,
});
response = await System.authDeleteToServer<boolean>(
`book/delete`,
{
id: bookId,
},
session.accessToken,
lang
);
}
} }
if (response) { if (response) {
setShowConfirmBox(false); setShowConfirmBox(false);
if (book?.localBook){ if (ifLocalBook) {
setLocalOnlyBooks(localOnlyBooks.filter((book:SyncedBook):boolean => book.id !== bookId)); setLocalOnlyBooks(localOnlyBooks.filter((b: SyncedBook): boolean => b.id !== bookId));
return; } else {
setServerOnlyBooks(serverOnlyBooks.filter((b: SyncedBook): boolean => b.id !== bookId));
} }
setServerOnlyBooks(serverOnlyBooks.filter((book:SyncedBook):boolean => book.id !== bookId));
setShowConfirmBox(false);
} }
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {

View File

@@ -10,6 +10,9 @@ import CharacterDetail from "@/components/book/settings/characters/CharacterDeta
import {useTranslations} from "next-intl"; import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext"; import {LangContext, LangContextProps} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; 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 CharacterDetailProps { interface CharacterDetailProps {
selectedCharacter: CharacterProps | null; selectedCharacter: CharacterProps | null;
@@ -48,6 +51,8 @@ export function CharacterComponent(props: any, ref: any) {
const t = useTranslations(); const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext) const {lang} = useContext<LangContextProps>(LangContext)
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {session} = useContext(SessionContext); const {session} = useContext(SessionContext);
const {book} = useContext(BookContext); const {book} = useContext(BookContext);
const {errorMessage, successMessage} = useContext(AlertContext); const {errorMessage, successMessage} = useContext(AlertContext);
@@ -120,22 +125,23 @@ export function CharacterComponent(props: any, ref: any) {
} }
try { try {
let characterId: string; let characterId: string;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
characterId = await window.electron.invoke<string>('db:character:create', { characterId = await window.electron.invoke<string>('db:character:create', {
bookId: book?.bookId, bookId: book?.bookId,
character: updatedCharacter, character: updatedCharacter,
}); });
} else { } else {
if (book?.localBook) { characterId = await System.authPostToServer<string>(`character/add`, {
characterId = await window.electron.invoke<string>('db:character:create', { bookId: book?.bookId,
character: updatedCharacter,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:character:create', {
bookId: book?.bookId, bookId: book?.bookId,
characterId,
character: updatedCharacter, character: updatedCharacter,
}); });
} else {
characterId = await System.authPostToServer<string>(`character/add`, {
bookId: book?.bookId,
character: updatedCharacter,
}, session.accessToken, lang);
} }
} }
if (!characterId) { if (!characterId) {
@@ -157,19 +163,19 @@ export function CharacterComponent(props: any, ref: any) {
async function updateCharacter(updatedCharacter: CharacterProps,): Promise<void> { async function updateCharacter(updatedCharacter: CharacterProps,): Promise<void> {
try { try {
let response: boolean; let response: boolean;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:character:update', { response = await window.electron.invoke<boolean>('db:character:update', {
character: updatedCharacter, character: updatedCharacter,
}); });
} else { } else {
if (book?.localBook) { response = await System.authPostToServer<boolean>(`character/update`, {
response = await window.electron.invoke<boolean>('db:character:update', { character: updatedCharacter,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:character:update', {
character: updatedCharacter, character: updatedCharacter,
}); });
} else {
response = await System.authPostToServer<boolean>(`character/update`, {
character: updatedCharacter,
}, session.accessToken, lang);
} }
} }
if (!response) { if (!response) {
@@ -215,25 +221,26 @@ export function CharacterComponent(props: any, ref: any) {
} else { } else {
try { try {
let attributeId: string; let attributeId: string;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
attributeId = await window.electron.invoke<string>('db:character:attribute:add', { attributeId = await window.electron.invoke<string>('db:character:attribute:add', {
characterId: selectedCharacter.id, characterId: selectedCharacter.id,
type: section, type: section,
name: value.name, name: value.name,
}); });
} else { } else {
if (book?.localBook) { attributeId = await System.authPostToServer<string>(`character/attribute/add`, {
attributeId = await window.electron.invoke<string>('db:character:attribute:add', { characterId: selectedCharacter.id,
type: section,
name: value.name,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:character:attribute:add', {
characterId: selectedCharacter.id, characterId: selectedCharacter.id,
attributeId,
type: section, type: section,
name: value.name, name: value.name,
}); });
} else {
attributeId = await System.authPostToServer<string>(`character/attribute/add`, {
characterId: selectedCharacter.id,
type: section,
name: value.name,
}, session.accessToken, lang);
} }
} }
if (!attributeId) { if (!attributeId) {
@@ -271,19 +278,19 @@ export function CharacterComponent(props: any, ref: any) {
} else { } else {
try { try {
let response: boolean; let response: boolean;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:character:attribute:delete', { response = await window.electron.invoke<boolean>('db:character:attribute:delete', {
attributeId: attrId, attributeId: attrId,
}); });
} else { } else {
if (book?.localBook) { response = await System.authDeleteToServer<boolean>(`character/attribute/delete`, {
response = await window.electron.invoke<boolean>('db:character:attribute:delete', { attributeId: attrId,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:character:attribute:delete', {
attributeId: attrId, attributeId: attrId,
}); });
} else {
response = await System.authDeleteToServer<boolean>(`character/attribute/delete`, {
attributeId: attrId,
}, session.accessToken, lang);
} }
} }
if (!response) { if (!response) {

View File

@@ -6,6 +6,7 @@ import SelectBox from "@/components/form/SelectBox";
import {AlertContext} from "@/context/AlertContext"; import {AlertContext} from "@/context/AlertContext";
import {SessionContext} from "@/context/SessionContext"; import {SessionContext} from "@/context/SessionContext";
import { import {
Attribute,
CharacterAttribute, CharacterAttribute,
characterCategories, characterCategories,
CharacterElement, CharacterElement,
@@ -32,6 +33,8 @@ import {LangContext} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {BookContext} from "@/context/BookContext"; import {BookContext} from "@/context/BookContext";
type AttributeResponse = { type: string; values: Attribute[] }[];
interface CharacterDetailProps { interface CharacterDetailProps {
selectedCharacter: CharacterProps | null; selectedCharacter: CharacterProps | null;
setSelectedCharacter: Dispatch<SetStateAction<CharacterProps | null>>; setSelectedCharacter: Dispatch<SetStateAction<CharacterProps | null>>;
@@ -70,23 +73,31 @@ export default function CharacterDetail(
async function getAttributes(): Promise<void> { async function getAttributes(): Promise<void> {
try { try {
let response: CharacterAttribute; let response: AttributeResponse;
if (isCurrentlyOffline()) { if (isCurrentlyOffline()) {
response = await window.electron.invoke<CharacterAttribute>('db:character:attributes', { response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {
characterId: selectedCharacter?.id, characterId: selectedCharacter?.id,
}); });
} else { } else {
if (book?.localBook) { if (book?.localBook) {
response = await window.electron.invoke<CharacterAttribute>('db:character:attributes', { response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {
characterId: selectedCharacter?.id, characterId: selectedCharacter?.id,
}); });
} else { } else {
response = await System.authGetQueryToServer<CharacterAttribute>(`character/attribute`, session.accessToken, lang, { response = await System.authGetQueryToServer<AttributeResponse>(`character/attribute`, session.accessToken, lang, {
characterId: selectedCharacter?.id, characterId: selectedCharacter?.id,
}); });
} }
} }
if (response) { if (response) {
const attributes: CharacterAttribute = {};
response.forEach((item: {
type: string
values: Attribute[]
}):void => {
attributes[item.type] = item.values;
});
setSelectedCharacter({ setSelectedCharacter({
id: selectedCharacter?.id ?? '', id: selectedCharacter?.id ?? '',
name: selectedCharacter?.name ?? '', name: selectedCharacter?.name ?? '',
@@ -97,14 +108,14 @@ export default function CharacterDetail(
biography: selectedCharacter?.biography, biography: selectedCharacter?.biography,
history: selectedCharacter?.history, history: selectedCharacter?.history,
role: selectedCharacter?.role ?? '', role: selectedCharacter?.role ?? '',
physical: response.physical ?? [], physical: attributes.physical ?? [],
psychological: response.psychological ?? [], psychological: attributes.psychological ?? [],
relations: response.relations ?? [], relations: attributes.relations ?? [],
skills: response.skills ?? [], skills: attributes.skills ?? [],
weaknesses: response.weaknesses ?? [], weaknesses: attributes.weaknesses ?? [],
strengths: response.strengths ?? [], strengths: attributes.strengths ?? [],
goals: response.goals ?? [], goals: attributes.goals ?? [],
motivations: response.motivations ?? [], motivations: attributes.motivations ?? [],
}); });
} }
} catch (e: unknown) { } catch (e: unknown) {

View File

@@ -22,11 +22,16 @@ import {
import {useTranslations} from "next-intl"; import {useTranslations} from "next-intl";
import {LangContext} from "@/context/LangContext"; import {LangContext} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook";
function GuideLineSetting(props: any, ref: any) { function GuideLineSetting(props: any, ref: any) {
const t = useTranslations(); const t = useTranslations();
const {lang} = useContext(LangContext); const {lang} = useContext(LangContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {book} = useContext(BookContext); const {book} = useContext(BookContext);
const {session} = useContext(SessionContext); const {session} = useContext(SessionContext);
const userToken: string = session?.accessToken ? session?.accessToken : ''; const userToken: string = session?.accessToken ? session?.accessToken : '';
@@ -159,18 +164,18 @@ function GuideLineSetting(props: any, ref: any) {
intendedAudience: intendedAudience, intendedAudience: intendedAudience,
keyMessages: keyMessages, keyMessages: keyMessages,
}; };
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:guideline:update', guidelineData); response = await window.electron.invoke<boolean>('db:book:guideline:update', guidelineData);
} else { } else {
if (book?.localBook) { response = await System.authPostToServer<boolean>(
response = await window.electron.invoke<boolean>('db:book:guideline:update', guidelineData); 'book/guide-line',
} else { guidelineData,
response = await System.authPostToServer<boolean>( userToken,
'book/guide-line', lang,
guidelineData, );
userToken,
lang, if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
); addToQueue('db:book:guideline:update', guidelineData);
} }
} }
if (!response) { if (!response) {
@@ -190,45 +195,28 @@ function GuideLineSetting(props: any, ref: any) {
async function saveQuillSense(): Promise<void> { async function saveQuillSense(): Promise<void> {
try { try {
let response: boolean; let response: boolean;
if (isCurrentlyOffline()) { const aiGuidelineData = {
response = await window.electron.invoke<boolean>('db:book:guideline:ai:update', { bookId: bookId,
bookId: bookId, plotSummary: plotSummary,
plotSummary: plotSummary, verbTense: verbTense,
verbTense: verbTense, narrativeType: narrativeType,
narrativeType: narrativeType, dialogueType: dialogueType,
dialogueType: dialogueType, toneAtmosphere: toneAtmosphere,
toneAtmosphere: toneAtmosphere, language: language,
language: language, themes: themes,
themes: themes, };
}); if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:guideline:ai:update', aiGuidelineData);
} else { } else {
if (book?.localBook) { response = await System.authPostToServer<boolean>(
response = await window.electron.invoke<boolean>('db:book:guideline:ai:update', { 'quillsense/book/guide-line',
bookId: bookId, aiGuidelineData,
plotSummary: plotSummary, userToken,
verbTense: verbTense, lang,
narrativeType: narrativeType, );
dialogueType: dialogueType,
toneAtmosphere: toneAtmosphere, if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
language: language, addToQueue('db:book:guideline:ai:update', aiGuidelineData);
themes: themes,
});
} else {
response = await System.authPostToServer<boolean>(
'quillsense/book/guide-line',
{
bookId: bookId,
plotSummary: plotSummary,
verbTense: verbTense,
narrativeType: narrativeType,
dialogueType: dialogueType,
toneAtmosphere: toneAtmosphere,
language: language,
themes: themes,
},
userToken,
lang,
);
} }
} }
if (response) { if (response) {

View File

@@ -12,6 +12,9 @@ import TexteAreaInput from "@/components/form/TexteAreaInput";
import {useTranslations} from "next-intl"; import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext"; import {LangContext, LangContextProps} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; 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 SubElement { interface SubElement {
id: string; id: string;
@@ -36,6 +39,8 @@ export function LocationComponent(props: any, ref: any) {
const t = useTranslations(); const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext); const {lang} = useContext<LangContextProps>(LangContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {session} = useContext(SessionContext); const {session} = useContext(SessionContext);
const {successMessage, errorMessage} = useContext(AlertContext); const {successMessage, errorMessage} = useContext(AlertContext);
const {book} = useContext(BookContext); const {book} = useContext(BookContext);
@@ -91,22 +96,23 @@ export function LocationComponent(props: any, ref: any) {
} }
try { try {
let sectionId: string; let sectionId: string;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
sectionId = await window.electron.invoke<string>('db:location:section:add', { sectionId = await window.electron.invoke<string>('db:location:section:add', {
bookId: bookId, bookId: bookId,
locationName: newSectionName, locationName: newSectionName,
}); });
} else { } else {
if (book?.localBook) { sectionId = await System.authPostToServer<string>(`location/section/add`, {
sectionId = await window.electron.invoke<string>('db:location:section:add', { bookId: bookId,
locationName: newSectionName,
}, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:location:section:add', {
bookId: bookId, bookId: bookId,
sectionId,
locationName: newSectionName, locationName: newSectionName,
}); });
} else {
sectionId = await System.authPostToServer<string>(`location/section/add`, {
bookId: bookId,
locationName: newSectionName,
}, token, lang);
} }
} }
if (!sectionId) { if (!sectionId) {
@@ -136,26 +142,27 @@ export function LocationComponent(props: any, ref: any) {
} }
try { try {
let elementId: string; let elementId: string;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
elementId = await window.electron.invoke<string>('db:location:element:add', { elementId = await window.electron.invoke<string>('db:location:element:add', {
bookId: bookId, bookId: bookId,
locationId: sectionId, locationId: sectionId,
elementName: newElementNames[sectionId], elementName: newElementNames[sectionId],
}); });
} else { } else {
if (book?.localBook) { elementId = await System.authPostToServer<string>(`location/element/add`, {
elementId = await window.electron.invoke<string>('db:location:element:add', {
bookId: bookId, bookId: bookId,
locationId: sectionId, locationId: sectionId,
elementName: newElementNames[sectionId], elementName: newElementNames[sectionId],
},
token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:location:element:add', {
bookId: bookId,
locationId: sectionId,
elementId,
elementName: newElementNames[sectionId],
}); });
} else {
elementId = await System.authPostToServer<string>(`location/element/add`, {
bookId: bookId,
locationId: sectionId,
elementName: newElementNames[sectionId],
},
token, lang);
} }
} }
if (!elementId) { if (!elementId) {
@@ -211,22 +218,24 @@ export function LocationComponent(props: any, ref: any) {
); );
try { try {
let subElementId: string; let subElementId: string;
if (isCurrentlyOffline()) { const elementId = sections[sectionIndex].elements[elementIndex].id;
if (isCurrentlyOffline() || book?.localBook) {
subElementId = await window.electron.invoke<string>('db:location:subelement:add', { subElementId = await window.electron.invoke<string>('db:location:subelement:add', {
elementId: sections[sectionIndex].elements[elementIndex].id, elementId: elementId,
subElementName: newSubElementNames[elementIndex], subElementName: newSubElementNames[elementIndex],
}); });
} else { } else {
if (book?.localBook) { subElementId = await System.authPostToServer<string>(`location/sub-element/add`, {
subElementId = await window.electron.invoke<string>('db:location:subelement:add', { elementId: elementId,
elementId: sections[sectionIndex].elements[elementIndex].id, subElementName: newSubElementNames[elementIndex],
}, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:location:subelement:add', {
elementId: elementId,
subElementId,
subElementName: newSubElementNames[elementIndex], subElementName: newSubElementNames[elementIndex],
}); });
} else {
subElementId = await System.authPostToServer<string>(`location/sub-element/add`, {
elementId: sections[sectionIndex].elements[elementIndex].id,
subElementName: newSubElementNames[elementIndex],
}, token, lang);
} }
} }
if (!subElementId) { if (!subElementId) {
@@ -275,19 +284,19 @@ export function LocationComponent(props: any, ref: any) {
let response: boolean; let response: boolean;
const elementId = sections.find((section: LocationProps): boolean => section.id === sectionId) const elementId = sections.find((section: LocationProps): boolean => section.id === sectionId)
?.elements[elementIndex].id; ?.elements[elementIndex].id;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:location:element:delete', { response = await window.electron.invoke<boolean>('db:location:element:delete', {
elementId: elementId, elementId: elementId,
}); });
} else { } else {
if (book?.localBook) { response = await System.authDeleteToServer<boolean>(`location/element/delete`, {
response = await window.electron.invoke<boolean>('db:location:element:delete', { elementId: elementId,
}, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:location:element:delete', {
elementId: elementId, elementId: elementId,
}); });
} else {
response = await System.authDeleteToServer<boolean>(`location/element/delete`, {
elementId: elementId,
}, token, lang);
} }
} }
if (!response) { if (!response) {
@@ -315,19 +324,19 @@ export function LocationComponent(props: any, ref: any) {
try { try {
let response: boolean; let response: boolean;
const subElementId = sections.find((section: LocationProps): boolean => section.id === sectionId)?.elements[elementIndex].subElements[subElementIndex].id; const subElementId = sections.find((section: LocationProps): boolean => section.id === sectionId)?.elements[elementIndex].subElements[subElementIndex].id;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:location:subelement:delete', { response = await window.electron.invoke<boolean>('db:location:subelement:delete', {
subElementId: subElementId, subElementId: subElementId,
}); });
} else { } else {
if (book?.localBook) { response = await System.authDeleteToServer<boolean>(`location/sub-element/delete`, {
response = await window.electron.invoke<boolean>('db:location:subelement:delete', { subElementId: subElementId,
}, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:location:subelement:delete', {
subElementId: subElementId, subElementId: subElementId,
}); });
} else {
response = await System.authDeleteToServer<boolean>(`location/sub-element/delete`, {
subElementId: subElementId,
}, token, lang);
} }
} }
if (!response) { if (!response) {
@@ -350,19 +359,19 @@ export function LocationComponent(props: any, ref: any) {
async function handleRemoveSection(sectionId: string): Promise<void> { async function handleRemoveSection(sectionId: string): Promise<void> {
try { try {
let response: boolean; let response: boolean;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:location:delete', { response = await window.electron.invoke<boolean>('db:location:delete', {
locationId: sectionId, locationId: sectionId,
}); });
} else { } else {
if (book?.localBook) { response = await System.authDeleteToServer<boolean>(`location/delete`, {
response = await window.electron.invoke<boolean>('db:location:delete', { locationId: sectionId,
}, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:location:delete', {
locationId: sectionId, locationId: sectionId,
}); });
} else {
response = await System.authDeleteToServer<boolean>(`location/delete`, {
locationId: sectionId,
}, token, lang);
} }
} }
if (!response) { if (!response) {
@@ -383,19 +392,19 @@ export function LocationComponent(props: any, ref: any) {
async function handleSave(): Promise<void> { async function handleSave(): Promise<void> {
try { try {
let response: boolean; let response: boolean;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:location:update', { response = await window.electron.invoke<boolean>('db:location:update', {
locations: sections, locations: sections,
}); });
} else { } else {
if (book?.localBook) { response = await System.authPostToServer<boolean>(`location/update`, {
response = await window.electron.invoke<boolean>('db:location:update', { locations: sections,
}, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:location:update', {
locations: sections, locations: sections,
}); });
} else {
response = await System.authPostToServer<boolean>(`location/update`, {
locations: sections,
}, token, lang);
} }
} }
if (!response) { if (!response) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,9 @@ import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext"; import {LangContext, LangContextProps} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {BookContext} from "@/context/BookContext"; import {BookContext} from "@/context/BookContext";
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook";
interface WorldElementInputProps { interface WorldElementInputProps {
sectionLabel: string; sectionLabel: string;
@@ -23,6 +26,8 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
const t = useTranslations(); const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext); const {lang} = useContext<LangContextProps>(LangContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {book} = useContext(BookContext); const {book} = useContext(BookContext);
const {worlds, setWorlds, selectedWorldIndex} = useContext(WorldContext); const {worlds, setWorlds, selectedWorldIndex} = useContext(WorldContext);
const {errorMessage, successMessage} = useContext(AlertContext); const {errorMessage, successMessage} = useContext(AlertContext);
@@ -37,19 +42,19 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
try { try {
let response: boolean; let response: boolean;
const elementId = (worlds[selectedWorldIndex][section] as WorldElement[])[index].id; const elementId = (worlds[selectedWorldIndex][section] as WorldElement[])[index].id;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:world:element:remove', { response = await window.electron.invoke<boolean>('db:book:world:element:remove', {
elementId: elementId, elementId: elementId,
}); });
} else { } else {
if (book?.localBook) { response = await System.authDeleteToServer<boolean>('book/world/element/delete', {
response = await window.electron.invoke<boolean>('db:book:world:element:remove', { elementId: elementId,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:book:world:element:remove', {
elementId: elementId, elementId: elementId,
}); });
} else {
response = await System.authDeleteToServer<boolean>('book/world/element/delete', {
elementId: elementId,
}, session.accessToken, lang);
} }
} }
if (!response) { if (!response) {
@@ -77,25 +82,26 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
} }
try { try {
let elementId: string; let elementId: string;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
elementId = await window.electron.invoke<string>('db:book:world:element:add', { elementId = await window.electron.invoke<string>('db:book:world:element:add', {
elementType: section, elementType: section,
worldId: worlds[selectedWorldIndex].id, worldId: worlds[selectedWorldIndex].id,
elementName: newElementName, elementName: newElementName,
}); });
} else { } else {
if (book?.localBook) { elementId = await System.authPostToServer('book/world/element/add', {
elementId = await window.electron.invoke<string>('db:book:world:element:add', { elementType: section,
worldId: worlds[selectedWorldIndex].id,
elementName: newElementName,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:book:world:element:add', {
elementType: section, elementType: section,
worldId: worlds[selectedWorldIndex].id, worldId: worlds[selectedWorldIndex].id,
elementId,
elementName: newElementName, elementName: newElementName,
}); });
} else {
elementId = await System.authPostToServer('book/world/element/add', {
elementType: section,
worldId: worlds[selectedWorldIndex].id,
elementName: newElementName,
}, session.accessToken, lang);
} }
} }
if (!elementId) { if (!elementId) {

View File

@@ -17,6 +17,9 @@ import SelectBox from "@/components/form/SelectBox";
import {useTranslations} from "next-intl"; import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext"; import {LangContext, LangContextProps} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; 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 interface ElementSection { export interface ElementSection {
title: string; title: string;
@@ -28,6 +31,8 @@ export function WorldSetting(props: any, ref: any) {
const t = useTranslations(); const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext); const {lang} = useContext<LangContextProps>(LangContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {errorMessage, successMessage} = useContext(AlertContext); const {errorMessage, successMessage} = useContext(AlertContext);
const {session} = useContext(SessionContext); const {session} = useContext(SessionContext);
const {book} = useContext(BookContext); const {book} = useContext(BookContext);
@@ -89,22 +94,23 @@ export function WorldSetting(props: any, ref: any) {
} }
try { try {
let worldId: string; let worldId: string;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
worldId = await window.electron.invoke<string>('db:book:world:add', { worldId = await window.electron.invoke<string>('db:book:world:add', {
worldName: newWorldName, worldName: newWorldName,
bookId: bookId, bookId: bookId,
}); });
} else { } else {
if (book?.localBook) { worldId = await System.authPostToServer<string>('book/world/add', {
worldId = await window.electron.invoke<string>('db:book:world:add', { worldName: newWorldName,
bookId: bookId,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
addToQueue('db:book:world:add', {
worldName: newWorldName, worldName: newWorldName,
worldId,
bookId: bookId, bookId: bookId,
}); });
} else {
worldId = await System.authPostToServer<string>('book/world/add', {
worldName: newWorldName,
bookId: bookId,
}, session.accessToken, lang);
} }
} }
if (!worldId) { if (!worldId) {
@@ -152,22 +158,17 @@ export function WorldSetting(props: any, ref: any) {
async function handleUpdateWorld(): Promise<void> { async function handleUpdateWorld(): Promise<void> {
try { try {
let response: boolean; let response: boolean;
if (isCurrentlyOffline()) { const worldData = {
response = await window.electron.invoke<boolean>('db:book:world:update', { world: worlds[selectedWorldIndex],
world: worlds[selectedWorldIndex], bookId: bookId,
bookId: bookId, };
}); if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:world:update', worldData);
} else { } else {
if (book?.localBook) { response = await System.authPutToServer<boolean>('book/world/update', worldData, session.accessToken, lang);
response = await window.electron.invoke<boolean>('db:book:world:update', {
world: worlds[selectedWorldIndex], if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
bookId: bookId, addToQueue('db:book:world:update', worldData);
});
} else {
response = await System.authPutToServer<boolean>('book/world/update', {
world: worlds[selectedWorldIndex],
bookId: bookId,
}, session.accessToken, lang);
} }
} }
if (!response) { if (!response) {

View File

@@ -33,6 +33,9 @@ import UserEditorSettings, {EditorDisplaySettings} from "@/components/editor/Use
import {useTranslations} from "next-intl"; import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext"; import {LangContext, LangContextProps} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; 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 ToolbarButton { interface ToolbarButton {
action: () => void; action: () => void;
@@ -141,7 +144,9 @@ export default function TextEditor() {
const {errorMessage, successMessage} = useContext(AlertContext); const {errorMessage, successMessage} = useContext(AlertContext);
const {session} = useContext(SessionContext); const {session} = useContext(SessionContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = 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);
@@ -281,39 +286,28 @@ export default function TextEditor() {
const saveContent: () => Promise<void> = useCallback(async (): Promise<void> => { const saveContent: () => Promise<void> = useCallback(async (): Promise<void> => {
if (!editor || !chapter) return; if (!editor || !chapter) return;
setIsSaving(true); setIsSaving(true);
const content = editor.state.doc.toJSON(); const content = editor.state.doc.toJSON();
const chapterId: string = chapter.chapterId || ''; const chapterId: string = chapter.chapterId || '';
const version: number = chapter.chapterContent.version || 0; const version: number = chapter.chapterContent.version || 0;
try { try {
let response: boolean; let response: boolean;
if (isCurrentlyOffline()){ const saveData = {
response = await window.electron.invoke<boolean>('db:chapter:content:save',{ chapterId,
chapterId, version,
version, content,
content, totalWordCount: editor.getText().length,
totalWordCount: editor.getText().length, currentTime: mainTimer
currentTime: mainTimer };
}) if (isCurrentlyOffline() || book?.localBook){
response = await window.electron.invoke<boolean>('db:chapter:content:save', saveData);
} else { } else {
if (book?.localBook) { response = await System.authPostToServer<boolean>(`chapter/content`, saveData, session?.accessToken, lang);
response = await window.electron.invoke<boolean>('db:chapter:content:save',{
chapterId, if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
version, addToQueue('db:chapter:content:save', saveData);
content,
totalWordCount: editor.getText().length,
currentTime: mainTimer
})
} else {
response = await System.authPostToServer<boolean>(`chapter/content`, {
chapterId,
version,
content,
totalWordCount: editor.getText().length,
currentTime: mainTimer
}, session?.accessToken, lang);
} }
} }
if (!response) { if (!response) {
@@ -332,7 +326,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);

View File

@@ -12,11 +12,16 @@ import {useTranslations} from "next-intl";
import InlineAddInput from "@/components/form/InlineAddInput"; import InlineAddInput from "@/components/form/InlineAddInput";
import {LangContext} from "@/context/LangContext"; import {LangContext} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; 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 default function ScribeChapterComponent() { export default function ScribeChapterComponent() {
const t = useTranslations(); const t = useTranslations();
const {lang} = useContext(LangContext); const {lang} = useContext(LangContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext); const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {book} = useContext(BookContext); const {book} = useContext(BookContext);
const {chapter, setChapter} = useContext(ChapterContext); const {chapter, setChapter} = useContext(ChapterContext);
@@ -137,25 +142,18 @@ export default function ScribeChapterComponent() {
async function handleChapterUpdate(chapterId: string, title: string, chapterOrder: number): Promise<void> { async function handleChapterUpdate(chapterId: string, title: string, chapterOrder: number): Promise<void> {
try { try {
let response: boolean; let response: boolean;
if (isCurrentlyOffline()) { const updateData = {
response = await window.electron.invoke<boolean>('db:chapter:update',{ chapterId: chapterId,
chapterId: chapterId, chapterOrder: chapterOrder,
chapterOrder: chapterOrder, title: title,
title: title, };
}) if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:chapter:update', updateData);
} else { } else {
if (book?.localBook){ response = await System.authPostToServer<boolean>('chapter/update', updateData, userToken, lang);
response = await window.electron.invoke<boolean>('db:chapter:update',{
chapterId: chapterId, if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
chapterOrder: chapterOrder, addToQueue('db:chapter:update', updateData);
title: title,
})
} else {
response = await System.authPostToServer<boolean>('chapter/update', {
chapterId: chapterId,
chapterOrder: chapterOrder,
title: title,
}, userToken, lang);
} }
} }
if (!response) { if (!response) {
@@ -190,15 +188,17 @@ export default function ScribeChapterComponent() {
try { try {
setDeleteConfirmationMessage(false); setDeleteConfirmationMessage(false);
let response:boolean = false; let response:boolean = false;
if (isCurrentlyOffline()) { if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:chapter:remove', removeChapterId) response = await window.electron.invoke<boolean>('db:chapter:remove', removeChapterId);
} else { } else {
if (book?.localBook){ response = await System.authDeleteToServer<boolean>('chapter/remove', {
response = await window.electron.invoke<boolean>('db:chapter:remove', removeChapterId) chapterId: removeChapterId,
} else { }, userToken, lang);
response = await System.authDeleteToServer<boolean>('chapter/remove', {
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:chapter:remove', {
chapterId: removeChapterId, chapterId: removeChapterId,
}, userToken, lang); });
} }
} }
if (!response) { if (!response) {
@@ -226,25 +226,21 @@ export default function ScribeChapterComponent() {
const chapterTitle: string = chapterOrder >= 0 ? newChapterName : book?.title as string; const chapterTitle: string = chapterOrder >= 0 ? newChapterName : book?.title as string;
try { try {
let chapterId:string|null = null; let chapterId:string|null = null;
if (isCurrentlyOffline()){ const addData = {
chapterId = await window.electron.invoke<string>('db:chapter:add', { bookId: book?.bookId,
bookId: book?.bookId, chapterOrder: chapterOrder,
chapterOrder: chapterOrder, title: chapterTitle
title: chapterTitle };
}) if (isCurrentlyOffline() || book?.localBook){
chapterId = await window.electron.invoke<string>('db:chapter:add', addData);
} else { } else {
if (book?.localBook){ chapterId = await System.authPostToServer<string>('chapter/add', addData, userToken, lang);
chapterId = await window.electron.invoke<string>('db:chapter:add', {
bookId: book?.bookId, if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
chapterOrder: chapterOrder, addToQueue('db:chapter:add', {
title: chapterTitle ...addData,
}) chapterId,
} else { });
chapterId = await System.authPostToServer<string>('chapter/add', {
bookId: book?.bookId,
chapterOrder: chapterOrder,
title: chapterTitle
}, userToken, lang);
} }
} }
if (!chapterId) { if (!chapterId) {

View File

@@ -8,6 +8,8 @@ export interface BooksSyncContextProps {
localSyncedBooks:SyncedBook[]; localSyncedBooks:SyncedBook[];
booksToSyncFromServer:BookSyncCompare[]; booksToSyncFromServer:BookSyncCompare[];
booksToSyncToServer:BookSyncCompare[]; booksToSyncToServer:BookSyncCompare[];
setServerSyncedBooks:Dispatch<SetStateAction<SyncedBook[]>>;
setLocalSyncedBooks:Dispatch<SetStateAction<SyncedBook[]>>;
setServerOnlyBooks:Dispatch<SetStateAction<SyncedBook[]>>; setServerOnlyBooks:Dispatch<SetStateAction<SyncedBook[]>>;
setLocalOnlyBooks:Dispatch<SetStateAction<SyncedBook[]>>; setLocalOnlyBooks:Dispatch<SetStateAction<SyncedBook[]>>;
serverOnlyBooks:SyncedBook[]; serverOnlyBooks:SyncedBook[];
@@ -19,6 +21,8 @@ export const BooksSyncContext:Context<BooksSyncContextProps> = createContext<Boo
localSyncedBooks:[], localSyncedBooks:[],
booksToSyncFromServer:[], booksToSyncFromServer:[],
booksToSyncToServer:[], booksToSyncToServer:[],
setServerSyncedBooks:():void => {},
setLocalSyncedBooks:():void => {},
setServerOnlyBooks:():void => {}, setServerOnlyBooks:():void => {},
setLocalOnlyBooks:():void => {}, setLocalOnlyBooks:():void => {},
serverOnlyBooks:[], serverOnlyBooks:[],

View File

@@ -1611,10 +1611,11 @@ export default class Book {
const encryptedLastName: string | null = character.last_name ? System.encryptDataWithUserKey(character.last_name, userKey) : null; const encryptedLastName: string | null = character.last_name ? System.encryptDataWithUserKey(character.last_name, userKey) : null;
const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey); const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey);
const encryptedCharTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userKey) : null; const encryptedCharTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userKey) : null;
const encryptedImage: string | null = character.image ? System.encryptDataWithUserKey(character.image, userKey) : null;
const encryptedRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userKey) : null; const encryptedRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userKey) : null;
const encryptedBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userKey) : null; const encryptedBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userKey) : null;
const encryptedCharHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userKey) : null; const encryptedCharHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userKey) : null;
return BookRepo.insertSyncCharacter(character.character_id, character.book_id, userId, encryptedFirstName, encryptedLastName, encryptedCategory, encryptedCharTitle, character.image, encryptedRole, encryptedBiography, encryptedCharHistory, character.last_update, lang); return BookRepo.insertSyncCharacter(character.character_id, character.book_id, userId, encryptedFirstName, encryptedLastName, encryptedCategory, encryptedCharTitle, encryptedImage, encryptedRole, encryptedBiography, encryptedCharHistory, character.last_update, lang);
}); });
if (!charactersInserted) return false; if (!charactersInserted) return false;
@@ -2064,10 +2065,18 @@ export default class Book {
if (chapterContents && chapterContents.length > 0) { if (chapterContents && chapterContents.length > 0) {
for (const chapterContent of chapterContents) { for (const chapterContent of chapterContents) {
const isExist: boolean = ChapterRepo.isChapterContentExist(userId, chapterContent.content_id, lang);
const content: string = System.encryptDataWithUserKey(chapterContent.content ? JSON.stringify(chapterContent.content) : '', userKey); const content: string = System.encryptDataWithUserKey(chapterContent.content ? JSON.stringify(chapterContent.content) : '', userKey);
const updated: boolean = ChapterRepo.updateChapterContent(userId, chapterContent.chapter_id, chapterContent.version, content, chapterContent.words_count, chapterContent.last_update); if (isExist) {
if (!updated) { const updated: boolean = ChapterRepo.updateChapterContent(userId, chapterContent.chapter_id, chapterContent.version, content, chapterContent.words_count, chapterContent.last_update);
return false; if (!updated) {
return false;
}
} else {
const insert: boolean = BookRepo.insertSyncChapterContent(chapterContent.content_id, chapterContent.chapter_id, userId, chapterContent.version, content, chapterContent.words_count, chapterContent.time_on_it, chapterContent.last_update, lang);
if (!insert) {
return false;
}
} }
} }
} }
@@ -2083,7 +2092,7 @@ export default class Book {
return false; return false;
} }
} else { } else {
const insert: boolean = BookRepo.insertSyncChapterInfo(chapterInfo.chapter_info_id, chapterInfo.chapter_id, chapterInfo.act_id, chapterInfo.incident_id, chapterInfo.plot_point_id, bookId, chapterInfo.author_id, chapterInfo.summary, chapterInfo.goal, chapterInfo.last_update,lang); const insert: boolean = BookRepo.insertSyncChapterInfo(chapterInfo.chapter_info_id, chapterInfo.chapter_id, chapterInfo.act_id, chapterInfo.incident_id, chapterInfo.plot_point_id, bookId, chapterInfo.author_id, summary, goal, chapterInfo.last_update,lang);
if (!insert) { if (!insert) {
return false; return false;
} }

View File

@@ -441,4 +441,20 @@ export default class ChapterRepo{
} }
} }
} }
static isChapterContentExist(userId: string, content_id: string, lang: "fr" | "en"): boolean {
try {
const db: Database = System.getDb();
const result: QueryResult | null = db.get('SELECT 1 FROM `book_chapter_content` WHERE `content_id`=? AND `author_id`=?', [content_id, userId]) || null;
return result !== null;
} catch (e: unknown) {
if (e instanceof Error) {
console.error(`DB Error: ${e.message}`);
throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du contenu du chapitre.` : `Unable to check chapter content existence.`);
} else {
console.error("An unknown error occurred.");
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
} }