Refactor character, chapter, and story components to support offline mode

- Add `OfflineContext` and `BookContext` to components for offline state management.
- Introduce conditional logic to toggle between server API requests and offline IPC handlers for CRUD operations.
- Refine `TextEditor`, `DraftCompanion`, and other components to disable actions or features unavailable in offline mode.
- Improve error handling and user feedback in both online and offline scenarios.
This commit is contained in:
natreex
2025-12-19 15:42:35 -05:00
parent 43c7ef375c
commit ff530f3442
16 changed files with 454 additions and 157 deletions

View File

@@ -32,6 +32,7 @@ import {LangContext, LangContextProps} from "@/context/LangContext";
import {BookTags} from "@/lib/models/Book";
import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext";
import {configs} from "@/lib/configs";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
interface CompanionContent {
version: number;
@@ -43,6 +44,7 @@ export default function DraftCompanion() {
const t = useTranslations();
const {setTotalPrice, setTotalCredits} = useContext<AIUsageContextProps>(AIUsageContext)
const {lang} = useContext<LangContextProps>(LangContext)
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const mainEditor: Editor | null = useEditor({
extensions: [
@@ -95,7 +97,7 @@ export default function DraftCompanion() {
const isGPTEnabled: boolean = QuillSense.isOpenAIEnabled(session);
const isSubTierTree: boolean = QuillSense.getSubLevel(session) === 3;
const hasAccess: boolean = isGPTEnabled || isSubTierTree;
const hasAccess: boolean = (isGPTEnabled || isSubTierTree) && !isCurrentlyOffline() && !book?.localBook;
useEffect((): void => {
getDraftContent().then();
@@ -106,11 +108,28 @@ export default function DraftCompanion() {
async function getDraftContent(): Promise<void> {
try {
const response: CompanionContent = await System.authGetQueryToServer<CompanionContent>(`chapter/content/companion`, session.accessToken, lang, {
bookid: book?.bookId,
chapterid: chapter?.chapterId,
version: chapter?.chapterContent.version,
});
let response: CompanionContent | null;
if (isCurrentlyOffline()) {
response = await window.electron.invoke<CompanionContent>('db:chapter:content:companion', {
bookid: book?.bookId,
chapterid: chapter?.chapterId,
version: chapter?.chapterContent.version,
});
} else {
if (book?.localBook) {
response = await window.electron.invoke<CompanionContent>('db:chapter:content:companion', {
bookid: book?.bookId,
chapterid: chapter?.chapterId,
version: chapter?.chapterContent.version,
});
} else {
response = await System.authGetQueryToServer<CompanionContent>(`chapter/content/companion`, session.accessToken, lang, {
bookid: book?.bookId,
chapterid: chapter?.chapterId,
version: chapter?.chapterContent.version,
});
}
}
if (response && mainEditor) {
mainEditor.commands.setContent(JSON.parse(response.content));
setDraftVersion(response.version);
@@ -145,9 +164,18 @@ export default function DraftCompanion() {
async function fetchTags(): Promise<void> {
try {
const responseTags: BookTags = await System.authGetQueryToServer<BookTags>(`book/tags`, session.accessToken, lang, {
bookId: book?.bookId
});
let responseTags: BookTags | null;
if (isCurrentlyOffline()) {
responseTags = await window.electron.invoke<BookTags>('db:book:tags', book?.bookId);
} else {
if (book?.localBook) {
responseTags = await window.electron.invoke<BookTags>('db:book:tags', book?.bookId);
} else {
responseTags = await System.authGetQueryToServer<BookTags>(`book/tags`, session.accessToken, lang, {
bookId: book?.bookId
});
}
}
if (responseTags) {
setCharacters(responseTags.characters);
setLocations(responseTags.locations);

View File

@@ -22,6 +22,7 @@ import {ChapterContext} from '@/context/ChapterContext';
import System from '@/lib/models/System';
import {AlertContext} from '@/context/AlertContext';
import {SessionContext} from "@/context/SessionContext";
import {BookContext} from '@/context/BookContext';
import DraftCompanion from "@/components/editor/DraftCompanion";
import GhostWriter from "@/components/ghostwriter/GhostWriter";
import SubmitButtonWLoading from "@/components/form/SubmitButtonWLoading";
@@ -135,6 +136,7 @@ export default function TextEditor() {
const {lang} = useContext<LangContextProps>(LangContext)
const {editor} = useContext(EditorContext);
const {chapter} = useContext(ChapterContext);
const {book} = useContext(BookContext);
const {errorMessage, successMessage} = useContext(AlertContext);
const {session} = useContext(SessionContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
@@ -294,13 +296,23 @@ export default function TextEditor() {
currentTime: mainTimer
})
} else {
response = await System.authPostToServer<boolean>(`chapter/content`, {
chapterId,
version,
content,
totalWordCount: editor.getText().length,
currentTime: mainTimer
}, session?.accessToken, lang);
if (book?.localBook) {
response = await window.electron.invoke<boolean>('db:chapter:content:save',{
chapterId,
version,
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) {
errorMessage(t('editor.error.savedFailed'));
@@ -467,7 +479,7 @@ export default function TextEditor() {
onClick={handleShowUserSettings}
icon={faCog}
/>
{chapter?.chapterContent.version === 2 && !isCurrentlyOffline() && (
{chapter?.chapterContent.version === 2 && !isCurrentlyOffline() && !book?.localBook && (
<CollapsableButton
showCollapsable={showGhostWriter}
text={t("textEditor.ghostWriter")}