Refactor and extend offline synchronization logic across components and services
- Integrated sync queue mechanisms with `LocalSyncQueueContext` for offline data handling. - Updated key sync-related services (e.g., book, chapter, series) to support offline-first functionality. - Removed redundant database fetch methods to optimize repository logic and improve maintainability. - Enhanced Tauri IPC usage for sync operations and removed legacy methods in Rust services.
This commit is contained in:
@@ -20,6 +20,9 @@ import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
import {BookProps} from "@/lib/types/book";
|
||||
import {SettingRef} from "@/lib/types/settings";
|
||||
import ImageDropZone from "@/components/form/ImageDropZone";
|
||||
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
||||
import {SyncedBook} from "@/lib/types/synced-book";
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||
|
||||
function BasicInformationSetting(_props: object, ref: React.ForwardedRef<SettingRef>): React.JSX.Element {
|
||||
const t = useTranslations();
|
||||
@@ -30,6 +33,8 @@ function BasicInformationSetting(_props: object, ref: React.ForwardedRef<Setting
|
||||
const userToken: string = session?.accessToken ? session?.accessToken : '';
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
const bookId: string = book?.bookId ? book?.bookId.toString() : '';
|
||||
|
||||
const [currentImage, setCurrentImage] = useState<string>(book?.coverImage ?? '');
|
||||
@@ -120,14 +125,14 @@ function BasicInformationSetting(_props: object, ref: React.ForwardedRef<Setting
|
||||
wordCount: wordCount,
|
||||
});
|
||||
} else {
|
||||
response = await apiPost<boolean>('book/basic-information', {
|
||||
title: title,
|
||||
subTitle: subTitle,
|
||||
summary: summary,
|
||||
publicationDate: publicationDate,
|
||||
wordCount: wordCount,
|
||||
bookId: bookId
|
||||
}, userToken, lang);
|
||||
const basicInfoData = {
|
||||
title, subTitle, summary, publicationDate, wordCount, bookId
|
||||
};
|
||||
response = await apiPost<boolean>('book/basic-information', basicInfoData, userToken, lang);
|
||||
|
||||
if (isDesktop && localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('update_book_basic_info', basicInfoData);
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t('basicInformationSetting.error.update'));
|
||||
|
||||
@@ -22,31 +22,44 @@ export default function DeleteBook({bookId}: DeleteBookProps) {
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext)
|
||||
const [showConfirmBox, setShowConfirmBox] = useState<boolean>(false);
|
||||
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext)
|
||||
const {setServerSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext)
|
||||
const {serverOnlyBooks, setServerOnlyBooks, localOnlyBooks, setLocalOnlyBooks, localSyncedBooks, setLocalSyncedBooks, setServerSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext)
|
||||
const [deleteLocalToo, setDeleteLocalToo] = useState<boolean>(false);
|
||||
const ifLocalOnlyBook: SyncedBook | undefined = localOnlyBooks.find((b: SyncedBook): boolean => b.id === bookId);
|
||||
const ifSyncedBook: SyncedBook | undefined = localSyncedBooks.find((b: SyncedBook): boolean => b.id === bookId);
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
|
||||
function handleConfirmation(): void {
|
||||
setDeleteLocalToo(false);
|
||||
setShowConfirmBox(true);
|
||||
}
|
||||
|
||||
async function handleDeleteBook(): Promise<void> {
|
||||
try {
|
||||
let response: boolean;
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
response = await tauri.deleteBook(bookId, Date.now());
|
||||
const deletedAt: number = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (isDesktop && (isCurrentlyOffline() || ifLocalOnlyBook)) {
|
||||
response = await tauri.deleteBook(bookId, deletedAt);
|
||||
} else {
|
||||
response = await apiDelete<boolean>('book/delete', {
|
||||
id: bookId,
|
||||
}, session.accessToken, lang);
|
||||
response = await apiDelete<boolean>('book/delete', {id: bookId, deletedAt}, session.accessToken, lang);
|
||||
if (response && isDesktop && ifSyncedBook && deleteLocalToo) {
|
||||
await tauri.deleteBook(bookId, deletedAt);
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
setShowConfirmBox(false);
|
||||
if (!response) {
|
||||
errorMessage("Une erreur est survenue lors de la suppression du livre.");
|
||||
return;
|
||||
if (ifLocalOnlyBook) {
|
||||
setLocalOnlyBooks(localOnlyBooks.filter((b: SyncedBook): boolean => b.id !== bookId));
|
||||
} else if (ifSyncedBook) {
|
||||
setLocalSyncedBooks(localSyncedBooks.filter((b: SyncedBook): boolean => b.id !== bookId));
|
||||
setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => prev.filter((b: SyncedBook): boolean => b.id !== bookId));
|
||||
if (!deleteLocalToo) {
|
||||
setLocalOnlyBooks([...localOnlyBooks, ifSyncedBook]);
|
||||
}
|
||||
} else {
|
||||
setServerOnlyBooks(serverOnlyBooks.filter((b: SyncedBook): boolean => b.id !== bookId));
|
||||
}
|
||||
setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => prev.filter((book: SyncedBook): boolean => book.id !== bookId))
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
|
||||
@@ -5,6 +5,9 @@ import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||
import {BookContext, BookContextProps} from "@/context/BookContext";
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
|
||||
import {SyncedBook} from '@/lib/types/synced-book';
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import {apiDelete, apiGet, apiPatch, apiPost} from '@/lib/api/client';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
@@ -57,6 +60,8 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
const {successMessage, errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
|
||||
const currentEntityId: string = entityId || book?.bookId || '';
|
||||
const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook);
|
||||
@@ -120,6 +125,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
toolName: 'locations',
|
||||
enabled: enabled
|
||||
}, token, lang);
|
||||
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
|
||||
addToQueue('update_book_tool_setting', {bookId: currentEntityId, toolName: 'locations', enabled});
|
||||
}
|
||||
if (response && setBook && book) {
|
||||
setToolEnabled(enabled);
|
||||
setBook({
|
||||
@@ -218,6 +226,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
sectionId = useLocal
|
||||
? await tauri.addLocationSection(newSectionName, currentEntityId)
|
||||
: await apiPost<string>('location/section/add', {bookId: currentEntityId, locationName: newSectionName}, token, lang);
|
||||
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
|
||||
addToQueue('add_location_section', {bookId: currentEntityId, sectionId, locationName: newSectionName});
|
||||
}
|
||||
if (!sectionId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSection'));
|
||||
return;
|
||||
@@ -258,6 +269,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
elementId = useLocal
|
||||
? await tauri.addLocationElement(sectionId, newElementNames[sectionId])
|
||||
: await apiPost<string>('location/element/add', {bookId: currentEntityId, locationId: sectionId, elementName: newElementNames[sectionId]}, token, lang);
|
||||
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
|
||||
addToQueue('add_location_element', {bookId: currentEntityId, locationId: sectionId, elementId, elementName: newElementNames[sectionId]});
|
||||
}
|
||||
if (!elementId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddElement'));
|
||||
return;
|
||||
@@ -325,6 +339,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
subElementId = useLocal
|
||||
? await tauri.addLocationSubElement(parentElementId, newSubElementNames[elementIndex])
|
||||
: await apiPost<string>('location/sub-element/add', {elementId: parentElementId, subElementName: newSubElementNames[elementIndex]}, token, lang);
|
||||
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
|
||||
addToQueue('add_location_subelement', {elementId: parentElementId, subElementId, subElementName: newSubElementNames[elementIndex]});
|
||||
}
|
||||
if (!subElementId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSubElement'));
|
||||
return;
|
||||
@@ -381,6 +398,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
success = useLocal
|
||||
? await tauri.deleteLocationElement(elementId!, currentEntityId, deletedAt)
|
||||
: await apiDelete<boolean>('location/element/delete', {elementId: elementId}, token, lang);
|
||||
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
|
||||
addToQueue('delete_location_element', {elementId, bookId: currentEntityId, deletedAt});
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteElement'));
|
||||
@@ -417,6 +437,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
success = useLocal
|
||||
? await tauri.deleteLocationSubElement(subElementId!, currentEntityId, deletedAt)
|
||||
: await apiDelete<boolean>('location/sub-element/delete', {subElementId: subElementId}, token, lang);
|
||||
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
|
||||
addToQueue('delete_location_subelement', {subElementId, bookId: currentEntityId, deletedAt});
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteSubElement'));
|
||||
@@ -447,6 +470,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
success = useLocal
|
||||
? await tauri.deleteLocationSection(sectionId, currentEntityId, deletedAt)
|
||||
: await apiDelete<boolean>('location/delete', {locationId: sectionId}, token, lang);
|
||||
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
|
||||
addToQueue('delete_location', {locationId: sectionId, bookId: currentEntityId, deletedAt});
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteSection'));
|
||||
@@ -468,6 +494,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
const response: boolean = useLocal
|
||||
? await tauri.updateLocations(sections) as boolean
|
||||
: await apiPost<boolean>(`location/update`, {locations: sections}, token, lang);
|
||||
if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
|
||||
addToQueue('update_locations', {locations: sections});
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t('locationComponent.errorUnknownSave'));
|
||||
return;
|
||||
|
||||
@@ -6,6 +6,9 @@ import {apiDelete, apiPost} from '@/lib/api/client';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
|
||||
import {SyncedBook} from '@/lib/types/synced-book';
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
|
||||
import {BookContext, BookContextProps} from '@/context/BookContext';
|
||||
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
@@ -30,6 +33,8 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
|
||||
const bookId: string | undefined = book?.bookId;
|
||||
const token: string = session.accessToken;
|
||||
@@ -72,10 +77,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
incidentId = await tauri.addIncident(bookId ?? '', newIncidentTitle);
|
||||
} else {
|
||||
incidentId = await apiPost<string>('book/incident/new', {
|
||||
bookId,
|
||||
name: newIncidentTitle,
|
||||
}, token, lang);
|
||||
const addData = {bookId, name: newIncidentTitle};
|
||||
incidentId = await apiPost<string>('book/incident/new', addData, token, lang);
|
||||
|
||||
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
||||
addToQueue('add_incident', addData);
|
||||
}
|
||||
}
|
||||
if (!incidentId) {
|
||||
errorMessage(t('errorAddIncident'));
|
||||
@@ -114,10 +121,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
response = await tauri.removeIncident(bookId ?? '', incidentId, Date.now());
|
||||
} else {
|
||||
response = await apiDelete<boolean>('book/incident/remove', {
|
||||
bookId,
|
||||
incidentId,
|
||||
}, token, lang);
|
||||
const deleteData = {bookId, incidentId};
|
||||
response = await apiDelete<boolean>('book/incident/remove', deleteData, token, lang);
|
||||
|
||||
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
||||
addToQueue('remove_incident', {...deleteData, deletedAt: Date.now()});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t('errorDeleteIncident'));
|
||||
@@ -151,11 +160,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
plotId = await tauri.addPlotPoint(bookId ?? '', newPlotPointTitle, selectedIncidentId);
|
||||
} else {
|
||||
plotId = await apiPost<string>('book/plot/new', {
|
||||
bookId,
|
||||
name: newPlotPointTitle,
|
||||
incidentId: selectedIncidentId,
|
||||
}, token, lang);
|
||||
const plotData = {bookId, name: newPlotPointTitle, incidentId: selectedIncidentId};
|
||||
plotId = await apiPost<string>('book/plot/new', plotData, token, lang);
|
||||
|
||||
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
||||
addToQueue('add_plot_point', plotData);
|
||||
}
|
||||
}
|
||||
if (!plotId) {
|
||||
errorMessage(t('errorAddPlotPoint'));
|
||||
@@ -195,9 +205,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
response = await tauri.removePlotPoint(plotPointId, bookId ?? '', Date.now());
|
||||
} else {
|
||||
response = await apiDelete<boolean>('book/plot/remove', {
|
||||
plotId: plotPointId,
|
||||
}, token, lang);
|
||||
const deleteData = {plotId: plotPointId};
|
||||
response = await apiDelete<boolean>('book/plot/remove', deleteData, token, lang);
|
||||
|
||||
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
||||
addToQueue('remove_plot_point', {...deleteData, bookId, deletedAt: Date.now()});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t('errorDeletePlotPoint'));
|
||||
@@ -246,13 +259,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
incidentId: destination === 'incident' ? itemId : undefined,
|
||||
});
|
||||
} else {
|
||||
linkId = await apiPost<string>('chapter/resume/add', {
|
||||
bookId,
|
||||
chapterId: chapterId,
|
||||
actId: actId,
|
||||
plotId: destination === 'plotPoint' ? itemId : null,
|
||||
incidentId: destination === 'incident' ? itemId : null,
|
||||
}, token, lang);
|
||||
const linkData = {bookId, chapterId, actId, plotId: destination === 'plotPoint' ? itemId : null, incidentId: destination === 'incident' ? itemId : null};
|
||||
linkId = await apiPost<string>('chapter/resume/add', linkData, token, lang);
|
||||
|
||||
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
||||
addToQueue('add_chapter_information', linkData);
|
||||
}
|
||||
}
|
||||
if (!linkId) {
|
||||
errorMessage(t('errorLinkChapter'));
|
||||
@@ -332,9 +344,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
response = await tauri.removeChapterInformation(chapterInfoId, bookId ?? '', Date.now());
|
||||
} else {
|
||||
response = await apiDelete<boolean>('chapter/resume/remove', {
|
||||
chapterInfoId,
|
||||
}, token, lang);
|
||||
const removeData = {chapterInfoId};
|
||||
response = await apiDelete<boolean>('chapter/resume/remove', removeData, token, lang);
|
||||
|
||||
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
||||
addToQueue('remove_chapter_information', {...removeData, bookId, deletedAt: Date.now()});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t('errorUnlinkChapter'));
|
||||
|
||||
@@ -6,6 +6,9 @@ import {apiDelete, apiPost} from '@/lib/api/client';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
|
||||
import {SyncedBook} from '@/lib/types/synced-book';
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
|
||||
import {BookContext, BookContextProps} from '@/context/BookContext';
|
||||
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
@@ -27,6 +30,8 @@ export default function Issues({issues, setIssues}: IssuesProps) {
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
|
||||
const bookId: string | undefined = book?.bookId;
|
||||
const token: string = session.accessToken;
|
||||
@@ -43,10 +48,12 @@ export default function Issues({issues, setIssues}: IssuesProps) {
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
issueId = await tauri.addIssue(bookId ?? '', newIssueName);
|
||||
} else {
|
||||
issueId = await apiPost<string>('book/issue/add', {
|
||||
bookId,
|
||||
name: newIssueName,
|
||||
}, token, lang);
|
||||
const addData = {bookId, name: newIssueName};
|
||||
issueId = await apiPost<string>('book/issue/add', addData, token, lang);
|
||||
|
||||
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
||||
addToQueue('add_issue', addData);
|
||||
}
|
||||
}
|
||||
if (!issueId) {
|
||||
errorMessage(t("issues.errorAdd"));
|
||||
@@ -79,15 +86,12 @@ export default function Issues({issues, setIssues}: IssuesProps) {
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
response = await tauri.removeIssue(bookId ?? '', issueId, Date.now());
|
||||
} else {
|
||||
response = await apiDelete<boolean>(
|
||||
'book/issue/remove',
|
||||
{
|
||||
bookId,
|
||||
issueId,
|
||||
},
|
||||
token,
|
||||
lang
|
||||
);
|
||||
const deleteData = {bookId, issueId};
|
||||
response = await apiDelete<boolean>('book/issue/remove', deleteData, token, lang);
|
||||
|
||||
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
||||
addToQueue('remove_issue', {...deleteData, deletedAt: Date.now()});
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
const updatedIssues: Issue[] = issues.filter((issue: Issue): boolean => issue.id !== issueId,);
|
||||
|
||||
@@ -7,6 +7,9 @@ import {apiDelete, apiPost} from '@/lib/api/client';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
|
||||
import {SyncedBook} from '@/lib/types/synced-book';
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
|
||||
import {BookContext, BookContextProps} from '@/context/BookContext';
|
||||
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
@@ -30,6 +33,8 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) {
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
|
||||
const bookId: string | undefined = book?.bookId;
|
||||
const token: string = session.accessToken;
|
||||
@@ -88,15 +93,12 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) {
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
response = await tauri.removeChapter(chapterIdToRemove, bookId ?? '', Date.now());
|
||||
} else {
|
||||
response = await apiDelete<boolean>(
|
||||
'chapter/remove',
|
||||
{
|
||||
bookId,
|
||||
chapterId: chapterIdToRemove,
|
||||
},
|
||||
token,
|
||||
lang,
|
||||
);
|
||||
const deleteData = {bookId, chapterId: chapterIdToRemove};
|
||||
response = await apiDelete<boolean>('chapter/remove', deleteData, token, lang);
|
||||
|
||||
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
||||
addToQueue('remove_chapter', {...deleteData, deletedAt: Date.now()});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t("mainChapter.errorDelete"));
|
||||
@@ -126,16 +128,12 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) {
|
||||
chapterOrder: newChapterOrder ? newChapterOrder : 0,
|
||||
});
|
||||
} else {
|
||||
responseId = await apiPost<string>(
|
||||
'chapter/add',
|
||||
{
|
||||
bookId: bookId,
|
||||
wordsCount: 0,
|
||||
chapterOrder: newChapterOrder ? newChapterOrder : 0,
|
||||
title: newChapterTitle,
|
||||
},
|
||||
token,
|
||||
);
|
||||
const addData = {bookId, wordsCount: 0, chapterOrder: newChapterOrder ? newChapterOrder : 0, title: newChapterTitle};
|
||||
responseId = await apiPost<string>('chapter/add', addData, token);
|
||||
|
||||
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
||||
addToQueue('add_chapter', addData);
|
||||
}
|
||||
}
|
||||
if (!responseId) {
|
||||
errorMessage(t("mainChapter.errorAdd"));
|
||||
|
||||
@@ -8,6 +8,9 @@ import {apiGet, apiPost} from '@/lib/api/client';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
|
||||
import {SyncedBook} from '@/lib/types/synced-book';
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
|
||||
import {Act as ActType, Incident, Issue, PlotPoint} from '@/lib/types/book';
|
||||
import {ActChapter, ChapterListProps} from '@/lib/types/chapter';
|
||||
import MainChapter from "@/components/book/settings/story/MainChapter";
|
||||
@@ -51,6 +54,8 @@ export function Story(_props: object, ref: React.ForwardedRef<SettingRef>): Reac
|
||||
const userToken: string = session.accessToken;
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
|
||||
const [acts, setActs] = useState<ActType[]>([]);
|
||||
const [issues, setIssues] = useState<Issue[]>([]);
|
||||
@@ -138,12 +143,12 @@ export function Story(_props: object, ref: React.ForwardedRef<SettingRef>): Reac
|
||||
issues,
|
||||
});
|
||||
} else {
|
||||
response = await apiPost<boolean>('book/story', {
|
||||
bookId,
|
||||
acts,
|
||||
mainChapters,
|
||||
issues,
|
||||
}, userToken, lang);
|
||||
const storyData = {bookId, acts, mainChapters, issues};
|
||||
response = await apiPost<boolean>('book/story', storyData, userToken, lang);
|
||||
|
||||
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
||||
addToQueue('update_book_story', storyData);
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t("story.errorSave"))
|
||||
|
||||
@@ -11,6 +11,9 @@ import {apiDelete, apiPost} from "@/lib/api/client";
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
|
||||
import {SyncedBook} from '@/lib/types/synced-book';
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
|
||||
import {BookContext, BookContextProps} from '@/context/BookContext';
|
||||
import InputField from "@/components/form/InputField";
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
@@ -52,6 +55,8 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
|
||||
const [newElementName, setNewElementName] = useState<string>('');
|
||||
|
||||
@@ -69,6 +74,9 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
|
||||
response = await apiDelete<boolean>(endpoint, {
|
||||
elementId: elements[index].id,
|
||||
}, session.accessToken, lang);
|
||||
if (!isSeriesMode && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
|
||||
addToQueue('remove_world_element', {elementId: elements[index].id, bookId: book?.bookId, deletedAt: Date.now()});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t("worldSetting.unknownError"))
|
||||
@@ -119,6 +127,9 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
|
||||
worldId: worlds[selectedWorldIndex].id,
|
||||
elementName: newElementName,
|
||||
}, session.accessToken, lang);
|
||||
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
|
||||
addToQueue('add_world_element', {worldId: worlds[selectedWorldIndex].id, elementId, elementType: section, elementName: newElementName});
|
||||
}
|
||||
if (!elementId) {
|
||||
errorMessage(t("worldSetting.unknownError"))
|
||||
return;
|
||||
|
||||
@@ -9,6 +9,9 @@ import {apiGet, apiPatch, apiPost} from "@/lib/api/client";
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
|
||||
import {SyncedBook} from '@/lib/types/synced-book';
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
|
||||
import {ElementSection, WorldListResponse, WorldProps, WorldTextField} from "@/lib/types/world";
|
||||
import {SettingRef} from "@/lib/types/settings";
|
||||
import {elementSections} from "@/lib/constants/world";
|
||||
@@ -39,6 +42,8 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef<S
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
|
||||
const currentEntityId: string = entityId || book?.bookId || '';
|
||||
const isSeriesMode: boolean = entityType === 'series';
|
||||
@@ -96,6 +101,9 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef<S
|
||||
toolName: 'worlds',
|
||||
enabled: enabled
|
||||
}, session.accessToken, lang);
|
||||
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
|
||||
addToQueue('update_book_tool_setting', {bookId: currentEntityId, toolName: 'worlds', enabled});
|
||||
}
|
||||
}
|
||||
if (response && setBook && book) {
|
||||
setToolEnabled(enabled);
|
||||
@@ -238,6 +246,9 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef<S
|
||||
worldName: newWorldName,
|
||||
bookId: currentEntityId,
|
||||
}, session.accessToken, lang);
|
||||
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
|
||||
addToQueue('add_world', {worldName: newWorldName, bookId: currentEntityId, worldId});
|
||||
}
|
||||
if (!worldId) {
|
||||
errorMessage(t("worldSetting.addWorldError"));
|
||||
return;
|
||||
@@ -310,6 +321,9 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef<S
|
||||
world: currentWorld,
|
||||
bookId: currentEntityId,
|
||||
}, session.accessToken, lang);
|
||||
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
|
||||
addToQueue('update_world', {world: currentWorld, bookId: currentEntityId});
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t("worldSetting.updateWorldError"));
|
||||
return;
|
||||
@@ -393,6 +407,9 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef<S
|
||||
errorMessage(t("worldSetting.importError"));
|
||||
return;
|
||||
}
|
||||
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) {
|
||||
addToQueue('add_world', {worldName: seriesWorld.name, bookId: currentEntityId, worldId, seriesWorldId});
|
||||
}
|
||||
|
||||
const newWorld: WorldProps = {
|
||||
id: worldId,
|
||||
|
||||
@@ -26,6 +26,9 @@ import {apiPost} from '@/lib/api/client';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
|
||||
import {SyncedBook} from '@/lib/types/synced-book';
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import DraftCompanion from "@/components/editor/DraftCompanion";
|
||||
@@ -144,6 +147,8 @@ export default function TextEditor() {
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
|
||||
const [mainTimer, setMainTimer] = useState<number>(0);
|
||||
const [showDraftCompanion, setShowDraftCompanion] = useState<boolean>(false);
|
||||
@@ -291,13 +296,18 @@ export default function TextEditor() {
|
||||
contentId: '',
|
||||
});
|
||||
} else {
|
||||
response = await apiPost<boolean>(`chapter/content`, {
|
||||
const saveData = {
|
||||
chapterId,
|
||||
version,
|
||||
content,
|
||||
totalWordCount: editor.getText().length,
|
||||
currentTime: mainTimer
|
||||
}, session?.accessToken ?? '');
|
||||
};
|
||||
response = await apiPost<boolean>(`chapter/content`, saveData, session?.accessToken ?? '');
|
||||
|
||||
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
|
||||
addToQueue('save_chapter_content', saveData);
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t('editor.error.savedFailed'));
|
||||
@@ -315,7 +325,7 @@ export default function TextEditor() {
|
||||
}
|
||||
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 => {
|
||||
setShowDraftCompanion((prev: boolean): boolean => !prev);
|
||||
@@ -454,7 +464,7 @@ export default function TextEditor() {
|
||||
onClick={handleShowUserSettings}
|
||||
tooltip={t("textEditor.preferences")}
|
||||
/>
|
||||
{chapter?.chapterContent.version === 2 && book?.quillsenseEnabled !== false && (
|
||||
{chapter?.chapterContent.version === 2 && !isCurrentlyOffline() && !book?.localBook && book?.quillsenseEnabled !== false && (
|
||||
<IconButton
|
||||
icon={Ghost}
|
||||
variant="ghost"
|
||||
|
||||
@@ -6,6 +6,13 @@ import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
import {LangContext, LangContextProps} from '@/context/LangContext';
|
||||
import {apiPost} from '@/lib/api/client';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BookContext, BookContextProps} from '@/context/BookContext';
|
||||
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
|
||||
import {SyncedBook} from '@/lib/types/synced-book';
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
|
||||
import IconButton from '@/components/ui/IconButton';
|
||||
|
||||
export type SyncElementType = 'character' | 'world' | 'location' | 'spell';
|
||||
@@ -42,6 +49,10 @@ export default function SyncFieldWrapper({
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
|
||||
const [isUploading, setIsUploading] = useState<boolean>(false);
|
||||
|
||||
@@ -53,17 +64,17 @@ export default function SyncFieldWrapper({
|
||||
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const response: SeriesSyncUploadResponse = await apiPost<SeriesSyncUploadResponse>(
|
||||
'series/propagate',
|
||||
{
|
||||
type: elementType,
|
||||
bookElementId: bookElementId,
|
||||
field: field,
|
||||
value: currentValue
|
||||
},
|
||||
session.accessToken,
|
||||
lang
|
||||
);
|
||||
const requestData = {type: elementType, bookElementId, field, value: currentValue};
|
||||
let response: SeriesSyncUploadResponse;
|
||||
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
response = await tauri.invoke<SeriesSyncUploadResponse>('series_sync_upload', {data: requestData});
|
||||
} else {
|
||||
response = await apiPost<SeriesSyncUploadResponse>('series/propagate', requestData, session.accessToken, lang);
|
||||
if (isDesktop && book?.bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book.bookId)) {
|
||||
addToQueue('series_sync_upload', requestData);
|
||||
}
|
||||
}
|
||||
if (response.success) {
|
||||
successMessage(t('syncField.uploadSuccess', {count: response.updatedCount}));
|
||||
if (onSyncComplete) {
|
||||
|
||||
@@ -4,6 +4,9 @@ import {apiDelete, apiGet, apiPost} from '@/lib/api/client';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
|
||||
import {SyncedBook} from '@/lib/types/synced-book';
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
|
||||
import {BookContext, BookContextProps} from "@/context/BookContext";
|
||||
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||
import {ChapterContext, ChapterContextProps} from "@/context/ChapterContext";
|
||||
@@ -26,6 +29,8 @@ export default function ScribeChapterComponent() {
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const userToken: string = session?.accessToken ? session?.accessToken : '';
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
const router = useRouter();
|
||||
|
||||
const [chapters, setChapters] = useState<ChapterListProps[]>([])
|
||||
@@ -107,11 +112,12 @@ export default function ScribeChapterComponent() {
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
response = await tauri.updateChapter(chapterId, title, chapterOrder);
|
||||
} else {
|
||||
response = await apiPost<boolean>('chapter/update', {
|
||||
chapterId: chapterId,
|
||||
chapterOrder: chapterOrder,
|
||||
title: title,
|
||||
}, userToken, lang);
|
||||
const updateData = {chapterId, chapterOrder, title};
|
||||
response = await apiPost<boolean>('chapter/update', updateData, userToken, lang);
|
||||
|
||||
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
|
||||
addToQueue('update_chapter', updateData);
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t("scribeChapterComponent.errorChapterUpdate"));
|
||||
@@ -148,9 +154,12 @@ export default function ScribeChapterComponent() {
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
response = await tauri.removeChapter(removeChapterId, book?.bookId ?? '', Date.now());
|
||||
} else {
|
||||
response = await apiDelete<boolean>('chapter/remove', {
|
||||
chapterId: removeChapterId,
|
||||
}, userToken, lang);
|
||||
const deleteData = {bookId: book?.bookId, chapterId: removeChapterId};
|
||||
response = await apiDelete<boolean>('chapter/remove', deleteData, userToken, lang);
|
||||
|
||||
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
|
||||
addToQueue('remove_chapter', {...deleteData, deletedAt: Date.now()});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t("scribeChapterComponent.errorChapterDelete"));
|
||||
@@ -184,11 +193,12 @@ export default function ScribeChapterComponent() {
|
||||
chapterOrder: chapterOrder,
|
||||
});
|
||||
} else {
|
||||
chapterId = await apiPost<string>('chapter/add', {
|
||||
bookId: book?.bookId,
|
||||
chapterOrder: chapterOrder,
|
||||
title: chapterTitle
|
||||
}, userToken, lang);
|
||||
const addData = {bookId: book?.bookId, chapterOrder, title: chapterTitle};
|
||||
chapterId = await apiPost<string>('chapter/add', addData, userToken, lang);
|
||||
|
||||
if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
|
||||
addToQueue('add_chapter', addData);
|
||||
}
|
||||
}
|
||||
if (!chapterId) {
|
||||
errorMessage(t("scribeChapterComponent.errorChapterSubmit", {chapterName: newChapterName}));
|
||||
|
||||
@@ -30,8 +30,10 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
const {
|
||||
serverSyncedBooks,
|
||||
setServerSyncedBooks
|
||||
setServerSyncedBooks,
|
||||
localSyncedBooks
|
||||
}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
const useLocal: boolean = isDesktop && isCurrentlyOffline();
|
||||
const [name, setName] = useState<string>('');
|
||||
const [description, setDescription] = useState<string>('');
|
||||
const [selectedBookIds, setSelectedBookIds] = useState<string[]>([]);
|
||||
@@ -83,6 +85,8 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
|
||||
: book
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => [...prev]);
|
||||
}
|
||||
|
||||
if (onSeriesCreated) {
|
||||
@@ -150,7 +154,7 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
|
||||
)}
|
||||
</div>
|
||||
|
||||
{serverSyncedBooks.length === 0 ? (
|
||||
{(useLocal ? localSyncedBooks : serverSyncedBooks).length === 0 ? (
|
||||
<div className="text-center py-6 text-muted">
|
||||
<Book className="w-8 h-8 mb-2 opacity-50" strokeWidth={1.75}/>
|
||||
<p>{t("addNewSeriesForm.noBooks")}</p>
|
||||
@@ -158,7 +162,7 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
|
||||
) : (
|
||||
<div
|
||||
className="max-h-48 overflow-y-auto rounded-xl border border-secondary bg-tertiary">
|
||||
{serverSyncedBooks.map((book: SyncedBook) => {
|
||||
{(useLocal ? localSyncedBooks : serverSyncedBooks).map((book: SyncedBook) => {
|
||||
const isSelected: boolean = selectedBookIds.includes(book.id);
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -10,6 +10,10 @@ import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import {apiDelete} from '@/lib/api/client';
|
||||
import {deleteSeries} from '@/lib/tauri';
|
||||
import {SeriesContext, SeriesContextProps} from '@/context/SeriesContext';
|
||||
import {SeriesSyncContext, SeriesSyncContextProps} from '@/context/SeriesSyncContext';
|
||||
import {SyncedSeries} from '@/lib/types/synced-series';
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
|
||||
|
||||
interface SeriesSettingOption {
|
||||
id: string;
|
||||
@@ -36,6 +40,8 @@ export default function SeriesSettingSidebar(
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
const {localSyncedSeries}: SeriesSyncContextProps = useContext<SeriesSyncContextProps>(SeriesSyncContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const userToken: string = session?.accessToken ? session?.accessToken : '';
|
||||
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
|
||||
@@ -44,9 +50,15 @@ export default function SeriesSettingSidebar(
|
||||
try {
|
||||
const useLocal: boolean = isDesktop && isCurrentlyOffline();
|
||||
const deletedAt: number = Math.floor(Date.now() / 1000);
|
||||
const success: boolean = useLocal
|
||||
? await deleteSeries(seriesId, deletedAt)
|
||||
: await apiDelete<boolean>('series/delete', {seriesId: seriesId}, userToken, lang);
|
||||
let success: boolean;
|
||||
if (useLocal) {
|
||||
success = await deleteSeries(seriesId, deletedAt);
|
||||
} else {
|
||||
success = await apiDelete<boolean>('series/delete', {seriesId: seriesId}, userToken, lang);
|
||||
if (isDesktop && localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('delete_series', {seriesId, deletedAt});
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
successMessage(t('seriesSetting.deleteSuccess'));
|
||||
onClose();
|
||||
|
||||
@@ -14,6 +14,9 @@ import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
import {SeriesContext, SeriesContextProps} from "@/context/SeriesContext";
|
||||
import {SeriesDetailResponse, SeriesUpdateResponse} from "@/lib/types/series";
|
||||
import PulseLoader from '@/components/ui/PulseLoader';
|
||||
import {SeriesSyncContext, SeriesSyncContextProps} from '@/context/SeriesSyncContext';
|
||||
import {SyncedSeries} from '@/lib/types/synced-series';
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext';
|
||||
|
||||
function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () => Promise<void> }>) {
|
||||
const t = useTranslations();
|
||||
@@ -24,6 +27,8 @@ function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () =
|
||||
const userToken: string = session?.accessToken ? session?.accessToken : '';
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
const {localSyncedSeries}: SeriesSyncContextProps = useContext<SeriesSyncContextProps>(SeriesSyncContext);
|
||||
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const useLocal: boolean = isDesktop && isCurrentlyOffline();
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
@@ -69,9 +74,16 @@ function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () =
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response: SeriesUpdateResponse = useLocal
|
||||
? {success: await updateSeries({seriesId, name, description})} as SeriesUpdateResponse
|
||||
: await apiPut<SeriesUpdateResponse>('series/update', {seriesId, name, description}, userToken, lang);
|
||||
let response: SeriesUpdateResponse;
|
||||
const updateData = {seriesId, name, description};
|
||||
if (useLocal) {
|
||||
response = {success: await updateSeries(updateData)} as SeriesUpdateResponse;
|
||||
} else {
|
||||
response = await apiPut<SeriesUpdateResponse>('series/update', updateData, userToken, lang);
|
||||
if (isDesktop && localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('update_series', updateData);
|
||||
}
|
||||
}
|
||||
if (!response.success) {
|
||||
errorMessage(t('seriesBasicInformation.error.update'));
|
||||
return;
|
||||
|
||||
@@ -29,7 +29,8 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
const {seriesId}: SeriesContextProps = useContext<SeriesContextProps>(SeriesContext);
|
||||
const {
|
||||
serverSyncedBooks,
|
||||
setServerSyncedBooks
|
||||
setServerSyncedBooks,
|
||||
localSyncedBooks
|
||||
}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
const userToken: string = session?.accessToken ? session?.accessToken : '';
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
@@ -49,11 +50,12 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
|
||||
useEffect(function () {
|
||||
const booksInSeries: string[] = seriesBooks.map((book: SeriesBookProps) => book.bookId);
|
||||
const filteredBooks: SyncedBook[] = serverSyncedBooks.filter(
|
||||
const allBooks: SyncedBook[] = useLocal ? localSyncedBooks : serverSyncedBooks;
|
||||
const filteredBooks: SyncedBook[] = allBooks.filter(
|
||||
(book: SyncedBook) => !booksInSeries.includes(book.id)
|
||||
);
|
||||
setAvailableBooks(filteredBooks);
|
||||
}, [seriesBooks, serverSyncedBooks]);
|
||||
}, [seriesBooks, serverSyncedBooks, localSyncedBooks, useLocal]);
|
||||
|
||||
async function loadSeriesBooks(): Promise<void> {
|
||||
setIsLoading(true);
|
||||
@@ -97,7 +99,8 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
: await apiPost<boolean>('series/book/add', {seriesId: seriesId, bookId: selectedBookToAdd}, userToken, lang);
|
||||
|
||||
if (response) {
|
||||
const addedBook: SyncedBook | undefined = serverSyncedBooks.find(
|
||||
const allBooks: SyncedBook[] = useLocal ? localSyncedBooks : serverSyncedBooks;
|
||||
const addedBook: SyncedBook | undefined = allBooks.find(
|
||||
(book: SyncedBook) => book.id === selectedBookToAdd
|
||||
);
|
||||
if (addedBook) {
|
||||
|
||||
@@ -44,6 +44,9 @@ const KEYRING_USER: &str = "vault-key";
|
||||
/// Falls back to the old derivation method if the keyring is unavailable,
|
||||
/// and attempts to migrate the key into the keyring for next time.
|
||||
fn get_vault_key() -> [u8; 32] {
|
||||
if cfg!(debug_assertions) {
|
||||
return derive_machine_key_legacy();
|
||||
}
|
||||
let entry = keyring::Entry::new(SERVICE_NAME, KEYRING_USER);
|
||||
if let Ok(entry) = &entry {
|
||||
if let Ok(stored) = entry.get_password() {
|
||||
@@ -56,7 +59,6 @@ fn get_vault_key() -> [u8; 32] {
|
||||
}
|
||||
}
|
||||
}
|
||||
// No key in keyring yet — generate a random one
|
||||
let mut key = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut key);
|
||||
let encoded = BASE64.encode(key);
|
||||
@@ -120,7 +122,6 @@ fn read_vault() -> AppResult<SecureVault> {
|
||||
let raw = BASE64.decode(content.trim())
|
||||
.map_err(|e| AppError::Keyring(format!("Vault corrupted (base64): {}", e)))?;
|
||||
|
||||
// Try the new keyring-backed key first
|
||||
let key = get_vault_key();
|
||||
if let Ok(decrypted) = decrypt_vault(&raw, &key) {
|
||||
if let Ok(vault) = serde_json::from_slice::<SecureVault>(&decrypted) {
|
||||
@@ -128,16 +129,15 @@ fn read_vault() -> AppResult<SecureVault> {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try legacy key and migrate if successful
|
||||
let legacy_key = derive_machine_key_legacy();
|
||||
let decrypted = decrypt_vault(&raw, &legacy_key)
|
||||
.map_err(|_| AppError::Keyring("Vault corrupted: unable to decrypt with any key.".to_string()))?;
|
||||
let vault: SecureVault = serde_json::from_slice(&decrypted)
|
||||
.map_err(|e| AppError::Keyring(format!("Vault corrupted (json): {}", e)))?;
|
||||
if let Ok(decrypted) = decrypt_vault(&raw, &legacy_key) {
|
||||
if let Ok(vault) = serde_json::from_slice::<SecureVault>(&decrypted) {
|
||||
let _ = write_vault_with_key(&vault, &key);
|
||||
return Ok(vault);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate: re-encrypt with the new keyring key
|
||||
let _ = write_vault_with_key(&vault, &key);
|
||||
Ok(vault)
|
||||
Ok(SecureVault::default())
|
||||
}
|
||||
|
||||
fn write_vault_with_key(vault: &SecureVault, key: &[u8; 32]) -> AppResult<()> {
|
||||
|
||||
@@ -404,29 +404,6 @@ pub fn fetch_book_chapters(conn: &Connection, user_id: &str, book_id: &str, lang
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Retrieves chapter information for a specific chapter.
|
||||
pub fn fetch_book_chapter_infos(conn: &Connection, user_id: &str, chapter_id: &str, lang: Lang) -> AppResult<Vec<BookChapterInfosTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update FROM book_chapter_infos WHERE author_id=?1 AND chapter_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id, chapter_id], |query_row| {
|
||||
Ok(BookChapterInfosTable {
|
||||
_chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
|
||||
_act_id: query_row.get(2)?, _incident_id: query_row.get(3)?,
|
||||
_plot_point_id: query_row.get(4)?, _book_id: query_row.get(5)?,
|
||||
author_id: query_row.get(6)?, summary: query_row.get(7)?,
|
||||
goal: query_row.get(8)?, last_update: query_row.get(9)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub fn fetch_all_chapter_infos_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookChapterInfosTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update FROM book_chapter_infos WHERE author_id=?1 AND book_id=?2")
|
||||
|
||||
@@ -172,33 +172,6 @@ pub fn is_chapter_content_exist(conn: &Connection, user_id: &str, content_id: &s
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Fetches all chapter contents for a specific chapter belonging to a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `chapter_id` - The ID of the chapter
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of book chapter content records.
|
||||
pub fn fetch_book_chapter_contents(conn: &Connection, user_id: &str, chapter_id: &str, lang: Lang) -> AppResult<Vec<BookChapterContentTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update FROM book_chapter_content WHERE author_id=?1 AND chapter_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id, chapter_id], |query_row| {
|
||||
Ok(BookChapterContentTable {
|
||||
content_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
|
||||
author_id: query_row.get(2)?, version: query_row.get(3)?,
|
||||
content: query_row.get(4)?, _words_count: query_row.get(5)?,
|
||||
_time_on_it: query_row.get(6)?, last_update: query_row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub fn fetch_all_chapter_contents_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookChapterContentTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT bcc.content_id, bcc.chapter_id, bcc.author_id, bcc.version, bcc.content, bcc.words_count, bcc.time_on_it, bcc.last_update FROM book_chapter_content bcc INNER JOIN book_chapters bc ON bcc.chapter_id = bc.chapter_id WHERE bcc.author_id=?1 AND bc.book_id=?2")
|
||||
|
||||
@@ -416,32 +416,6 @@ pub fn fetch_book_characters(conn: &Connection, user_id: &str, book_id: &str, la
|
||||
Ok(characters)
|
||||
}
|
||||
|
||||
/// Fetches all attributes for a specific character.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of character attributes.
|
||||
pub fn fetch_book_characters_attributes(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<Vec<BookCharactersAttributesTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM book_characters_attributes WHERE user_id=?1 AND character_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?;
|
||||
|
||||
let attributes = statement
|
||||
.query_map(params![user_id, character_id], |query_row| {
|
||||
Ok(BookCharactersAttributesTable {
|
||||
attr_id: query_row.get(0)?, character_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, attribute_name: query_row.get(3)?,
|
||||
attribute_value: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?;
|
||||
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
pub fn fetch_all_character_attributes_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookCharactersAttributesTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT bca.attr_id, bca.character_id, bca.user_id, bca.attribute_name, bca.attribute_value, bca.last_update FROM book_characters_attributes bca INNER JOIN book_characters bc ON bca.character_id = bc.character_id WHERE bca.user_id=?1 AND bc.book_id=?2")
|
||||
|
||||
@@ -425,33 +425,6 @@ pub fn fetch_book_locations(conn: &Connection, user_id: &str, book_id: &str, lan
|
||||
Ok(book_locations)
|
||||
}
|
||||
|
||||
/// Fetches all elements for a specific location.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `location_id` - The location's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of location element records.
|
||||
pub fn fetch_location_elements(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult<Vec<LocationElementTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT element_id, location, user_id, element_name, original_name, element_description, last_update FROM location_element WHERE user_id = ?1 AND location = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?;
|
||||
|
||||
let location_elements = statement
|
||||
.query_map(params![user_id, location_id], |query_row| {
|
||||
Ok(LocationElementTable {
|
||||
element_id: query_row.get(0)?, location: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, element_name: query_row.get(3)?,
|
||||
original_name: query_row.get(4)?, element_description: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?;
|
||||
|
||||
Ok(location_elements)
|
||||
}
|
||||
|
||||
pub fn fetch_all_location_elements_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<LocationElementTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT e.element_id, e.location, e.user_id, e.element_name, e.original_name, e.element_description, e.last_update FROM location_element e INNER JOIN book_location l ON e.location = l.loc_id AND e.user_id = l.user_id WHERE e.user_id = ?1 AND l.book_id = ?2")
|
||||
@@ -494,33 +467,6 @@ pub fn fetch_all_location_sub_elements_by_book(conn: &Connection, user_id: &str,
|
||||
Ok(location_sub_elements)
|
||||
}
|
||||
|
||||
/// Fetches all sub-elements for a specific location element.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `element_id` - The element's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of location sub-element records.
|
||||
pub fn fetch_location_sub_elements(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult<Vec<LocationSubElementTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update FROM location_sub_element WHERE user_id = ?1 AND element_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?;
|
||||
|
||||
let location_sub_elements = statement
|
||||
.query_map(params![user_id, element_id], |query_row| {
|
||||
Ok(LocationSubElementTable {
|
||||
sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, sub_elem_name: query_row.get(3)?,
|
||||
original_name: query_row.get(4)?, sub_elem_description: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?;
|
||||
|
||||
Ok(location_sub_elements)
|
||||
}
|
||||
|
||||
/// Fetches all synced locations for a user (used for synchronization).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
|
||||
@@ -281,33 +281,6 @@ pub fn fetch_book_worlds(conn: &Connection, user_id: &str, book_id: &str, lang:
|
||||
Ok(worlds)
|
||||
}
|
||||
|
||||
/// Fetches all elements for a specific world.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `world_id` - The unique identifier of the world
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of book world elements table records.
|
||||
pub fn fetch_book_world_elements(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult<Vec<BookWorldElementsTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM book_world_elements WHERE user_id=?1 AND world_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les \u{00e9}l\u{00e9}ments du monde.".to_string() } else { "Unable to retrieve world elements.".to_string() }))?;
|
||||
|
||||
let elements = statement
|
||||
.query_map(params![user_id, world_id], |query_row| {
|
||||
Ok(BookWorldElementsTable {
|
||||
element_id: query_row.get(0)?, world_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, element_type: query_row.get(3)?,
|
||||
name: query_row.get(4)?, original_name: query_row.get(5)?,
|
||||
description: query_row.get(6)?, last_update: query_row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les \u{00e9}l\u{00e9}ments du monde.".to_string() } else { "Unable to retrieve world elements.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les \u{00e9}l\u{00e9}ments du monde.".to_string() } else { "Unable to retrieve world elements.".to_string() }))?;
|
||||
|
||||
Ok(elements)
|
||||
}
|
||||
|
||||
pub fn fetch_all_world_elements_by_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookWorldElementsTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT e.element_id, e.world_id, e.user_id, e.element_type, e.name, e.original_name, e.description, e.last_update FROM book_world_elements e INNER JOIN book_world w ON e.world_id = w.world_id AND e.user_id = w.author_id WHERE e.user_id=?1 AND w.book_id=?2")
|
||||
|
||||
Reference in New Issue
Block a user