- Added offline detection logic with `OfflineContext` to improve app functionality in offline scenarios. - Integrated Tauri IPC functions to handle local tool settings and character attributes when offline. - Refined indentation logic in `TextEditor` for better compatibility with WebKit engines. - Removed unused `indent` property and related settings in editor components to simplify configuration. - Updated locale files with improved translation consistency and parameterized placeholders.
804 lines
34 KiB
TypeScript
804 lines
34 KiB
TypeScript
'use client'
|
|
import {useCallback, useContext, useEffect, useState} from 'react';
|
|
import {SeriesLocationElement, SeriesLocationItem, SeriesLocationSubElement} from '@/lib/types/series';
|
|
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
|
import {BookContext, BookContextProps} from '@/context/BookContext';
|
|
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
|
import {LangContext, LangContextProps} from '@/context/LangContext';
|
|
import {apiDelete, apiGet, apiPatch, apiPost} from '@/lib/api/client';
|
|
import {useTranslations} from '@/lib/i18n';
|
|
import {ViewMode} from '@/lib/types/settings';
|
|
import {isDesktop} from '@/lib/configs';
|
|
import * as tauri from '@/lib/tauri';
|
|
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
|
|
|
export interface SubElement {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
}
|
|
|
|
export interface Element {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
subElements: SubElement[];
|
|
}
|
|
|
|
export interface LocationProps {
|
|
id: string;
|
|
name: string;
|
|
elements: Element[];
|
|
seriesLocationId?: string | null;
|
|
}
|
|
|
|
interface LocationListResponse {
|
|
locations: LocationProps[];
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface UseLocationsConfig {
|
|
entityType: 'book' | 'series';
|
|
entityId: string;
|
|
}
|
|
|
|
export interface UseLocationsReturn {
|
|
// State
|
|
sections: LocationProps[];
|
|
seriesLocations: SeriesLocationItem[];
|
|
toolEnabled: boolean;
|
|
isLoading: boolean;
|
|
isSeriesMode: boolean;
|
|
bookSeriesId: string | null;
|
|
newSectionName: string;
|
|
newElementNames: { [key: string]: string };
|
|
newSubElementNames: { [key: string]: string };
|
|
|
|
// Navigation state
|
|
viewMode: ViewMode;
|
|
selectedSectionIndex: number;
|
|
sectionsBackup: LocationProps[] | null;
|
|
|
|
// Actions
|
|
addSection: () => Promise<void>;
|
|
addElement: (sectionId: string) => Promise<void>;
|
|
addSubElement: (sectionId: string, elementIndex: number) => Promise<void>;
|
|
removeSection: (sectionId: string) => Promise<void>;
|
|
removeElement: (sectionId: string, elementIndex: number) => Promise<void>;
|
|
removeSubElement: (sectionId: string, elementIndex: number, subElementIndex: number) => Promise<void>;
|
|
updateElement: (sectionId: string, elementIndex: number, field: keyof Element, value: string) => void;
|
|
updateSubElement: (sectionId: string, elementIndex: number, subElementIndex: number, field: keyof SubElement, value: string) => void;
|
|
saveLocations: () => Promise<void>;
|
|
toggleTool: (enabled: boolean) => Promise<void>;
|
|
importFromSeries: (seriesLocationId: string) => Promise<void>;
|
|
exportToSeries: (section: LocationProps) => Promise<void>;
|
|
refreshLocations: () => Promise<void>;
|
|
refreshSeriesLocations: () => Promise<void>;
|
|
setNewSectionName: (name: string) => void;
|
|
setNewElementNames: React.Dispatch<React.SetStateAction<{ [key: string]: string }>>;
|
|
setNewSubElementNames: React.Dispatch<React.SetStateAction<{ [key: string]: string }>>;
|
|
|
|
// Navigation actions
|
|
enterDetailMode: (sectionIndex: number) => void;
|
|
enterEditMode: () => void;
|
|
exitEditMode: (save: boolean) => Promise<void>;
|
|
backToList: () => void;
|
|
}
|
|
|
|
export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
|
const {entityType, entityId} = config;
|
|
const t = useTranslations();
|
|
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
|
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
|
const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext);
|
|
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
|
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
|
|
|
const [sections, setSections] = useState<LocationProps[]>([]);
|
|
const [seriesLocations, setSeriesLocations] = useState<SeriesLocationItem[]>([]);
|
|
const [toolEnabled, setToolEnabled] = useState<boolean>(entityType === 'series' || (book?.tools?.locations ?? false));
|
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
const [newSectionName, setNewSectionName] = useState<string>('');
|
|
const [newElementNames, setNewElementNames] = useState<{ [key: string]: string }>({});
|
|
const [newSubElementNames, setNewSubElementNames] = useState<{ [key: string]: string }>({});
|
|
|
|
// Navigation state
|
|
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
|
const [selectedSectionIndex, setSelectedSectionIndex] = useState<number>(-1);
|
|
const [sectionsBackup, setSectionsBackup] = useState<LocationProps[] | null>(null);
|
|
|
|
const isSeriesMode: boolean = entityType === 'series';
|
|
const bookSeriesId: string | null = book?.seriesId || null;
|
|
const userToken: string = session?.accessToken || '';
|
|
|
|
// Load locations on mount
|
|
useEffect(function (): void {
|
|
if (entityId) {
|
|
refreshLocations();
|
|
}
|
|
}, [entityId]);
|
|
|
|
// Load series locations for book mode
|
|
useEffect(function (): void {
|
|
if (bookSeriesId && !isSeriesMode) {
|
|
refreshSeriesLocations();
|
|
}
|
|
}, [bookSeriesId, isSeriesMode]);
|
|
|
|
const refreshSeriesLocations = useCallback(async function (): Promise<void> {
|
|
if (!bookSeriesId) return;
|
|
try {
|
|
const response: SeriesLocationItem[] = await apiGet<SeriesLocationItem[]>(
|
|
'series/location/list',
|
|
userToken,
|
|
lang,
|
|
{seriesid: bookSeriesId}
|
|
);
|
|
if (response) {
|
|
setSeriesLocations(response);
|
|
}
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
}
|
|
}
|
|
}, [bookSeriesId, userToken, lang]);
|
|
|
|
const refreshLocations = useCallback(async function (): Promise<void> {
|
|
setIsLoading(true);
|
|
try {
|
|
if (isSeriesMode) {
|
|
const response: SeriesLocationItem[] = await apiGet<SeriesLocationItem[]>(
|
|
'series/location/list',
|
|
userToken,
|
|
lang,
|
|
{seriesid: entityId}
|
|
);
|
|
if (response) {
|
|
const mappedLocations: LocationProps[] = response.map(function (loc: SeriesLocationItem): LocationProps {
|
|
return {
|
|
id: loc.id,
|
|
name: loc.name,
|
|
elements: loc.elements.map(function (elem: SeriesLocationElement): Element {
|
|
return {
|
|
id: elem.id,
|
|
name: elem.name,
|
|
description: elem.description,
|
|
subElements: elem.subElements.map(function (sub: SeriesLocationSubElement): SubElement {
|
|
return {
|
|
id: sub.id,
|
|
name: sub.name,
|
|
description: sub.description,
|
|
};
|
|
}),
|
|
};
|
|
}),
|
|
};
|
|
});
|
|
setSections(mappedLocations);
|
|
}
|
|
} else {
|
|
let response: LocationListResponse;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.getAllLocations(entityId, book?.tools?.locations ?? false) as LocationListResponse;
|
|
} else {
|
|
response = await apiGet<LocationListResponse>('location/all', userToken, lang, {
|
|
bookid: entityId
|
|
});
|
|
}
|
|
if (response) {
|
|
setSections(response.locations);
|
|
setToolEnabled(response.enabled);
|
|
if (setBook && book) {
|
|
setBook({
|
|
...book,
|
|
tools: {
|
|
characters: book.tools?.characters ?? false,
|
|
worlds: book.tools?.worlds ?? false,
|
|
locations: response.enabled,
|
|
spells: book.tools?.spells ?? false
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('locationComponent.errorUnknownFetchLocations'));
|
|
}
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [entityId, isSeriesMode, userToken, lang, book, setBook, errorMessage, t]);
|
|
|
|
const toggleTool = useCallback(async function (enabled: boolean): Promise<void> {
|
|
if (isSeriesMode) return;
|
|
try {
|
|
let response: boolean;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.updateBookToolSetting(book?.bookId ?? '', 'locations', enabled);
|
|
} else {
|
|
response = await apiPatch<boolean>('book/tool-setting', {
|
|
bookId: book?.bookId,
|
|
toolName: 'locations',
|
|
enabled: enabled
|
|
}, userToken, lang);
|
|
}
|
|
if (response && setBook && book) {
|
|
setToolEnabled(enabled);
|
|
setBook({
|
|
...book,
|
|
tools: {
|
|
characters: book.tools?.characters ?? false,
|
|
worlds: book.tools?.worlds ?? false,
|
|
locations: enabled,
|
|
spells: book.tools?.spells ?? false
|
|
}
|
|
});
|
|
}
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
}
|
|
}
|
|
}, [isSeriesMode, book, setBook, userToken, lang, errorMessage]);
|
|
|
|
const addSection = useCallback(async function (): Promise<void> {
|
|
if (!newSectionName.trim()) {
|
|
errorMessage(t('locationComponent.errorSectionNameEmpty'));
|
|
return;
|
|
}
|
|
try {
|
|
let sectionId: string;
|
|
if (isSeriesMode) {
|
|
sectionId = await apiPost<string>(
|
|
'series/location/section/add',
|
|
{
|
|
seriesId: entityId,
|
|
name: newSectionName,
|
|
},
|
|
userToken,
|
|
lang
|
|
);
|
|
if (!sectionId) {
|
|
errorMessage(t('locationComponent.errorUnknownAddSection'));
|
|
return;
|
|
}
|
|
} else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
sectionId = await tauri.addLocationSection(newSectionName, entityId);
|
|
if (!sectionId) {
|
|
errorMessage(t('locationComponent.errorUnknownAddSection'));
|
|
return;
|
|
}
|
|
} else {
|
|
sectionId = await apiPost<string>('location/section/add', {
|
|
bookId: entityId,
|
|
locationName: newSectionName,
|
|
}, userToken, lang);
|
|
if (!sectionId) {
|
|
errorMessage(t('locationComponent.errorUnknownAddSection'));
|
|
return;
|
|
}
|
|
}
|
|
const newLocation: LocationProps = {
|
|
id: sectionId,
|
|
name: newSectionName,
|
|
elements: [],
|
|
};
|
|
setSections(function (prev: LocationProps[]): LocationProps[] {
|
|
return [...prev, newLocation];
|
|
});
|
|
setNewSectionName('');
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('locationComponent.errorUnknownAddSection'));
|
|
}
|
|
}
|
|
}, [newSectionName, isSeriesMode, entityId, userToken, lang, errorMessage, t]);
|
|
|
|
const addElement = useCallback(async function (sectionId: string): Promise<void> {
|
|
if (!newElementNames[sectionId]?.trim()) {
|
|
errorMessage(t('locationComponent.errorElementNameEmpty'));
|
|
return;
|
|
}
|
|
try {
|
|
let elementId: string;
|
|
if (isSeriesMode) {
|
|
elementId = await apiPost<string>(
|
|
'series/location/element/add',
|
|
{
|
|
locationId: sectionId,
|
|
name: newElementNames[sectionId],
|
|
},
|
|
userToken,
|
|
lang
|
|
);
|
|
if (!elementId) {
|
|
errorMessage(t('locationComponent.errorUnknownAddElement'));
|
|
return;
|
|
}
|
|
} else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
elementId = await tauri.addLocationElement(sectionId, newElementNames[sectionId]);
|
|
if (!elementId) {
|
|
errorMessage(t('locationComponent.errorUnknownAddElement'));
|
|
return;
|
|
}
|
|
} else {
|
|
elementId = await apiPost<string>('location/element/add', {
|
|
bookId: entityId,
|
|
locationId: sectionId,
|
|
elementName: newElementNames[sectionId],
|
|
}, userToken, lang);
|
|
if (!elementId) {
|
|
errorMessage(t('locationComponent.errorUnknownAddElement'));
|
|
return;
|
|
}
|
|
}
|
|
setSections(function (prev: LocationProps[]): LocationProps[] {
|
|
return prev.map(function (section: LocationProps): LocationProps {
|
|
if (section.id !== sectionId) return section;
|
|
return {
|
|
...section,
|
|
elements: [...section.elements, {
|
|
id: elementId,
|
|
name: newElementNames[sectionId],
|
|
description: '',
|
|
subElements: [],
|
|
}],
|
|
};
|
|
});
|
|
});
|
|
setNewElementNames(function (prev: { [key: string]: string }): { [key: string]: string } {
|
|
return {...prev, [sectionId]: ''};
|
|
});
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('locationComponent.errorUnknownAddElement'));
|
|
}
|
|
}
|
|
}, [newElementNames, isSeriesMode, entityId, userToken, lang, errorMessage, t]);
|
|
|
|
const addSubElement = useCallback(async function (sectionId: string, elementIndex: number): Promise<void> {
|
|
if (!newSubElementNames[elementIndex]?.trim()) {
|
|
errorMessage(t('locationComponent.errorSubElementNameEmpty'));
|
|
return;
|
|
}
|
|
const sectionIndex: number = sections.findIndex(function (section: LocationProps): boolean {
|
|
return section.id === sectionId;
|
|
});
|
|
try {
|
|
let subElementId: string;
|
|
if (isSeriesMode) {
|
|
subElementId = await apiPost<string>(
|
|
'series/location/sub-element/add',
|
|
{
|
|
elementId: sections[sectionIndex].elements[elementIndex].id,
|
|
name: newSubElementNames[elementIndex],
|
|
},
|
|
userToken,
|
|
lang
|
|
);
|
|
if (!subElementId) {
|
|
errorMessage(t('locationComponent.errorUnknownAddSubElement'));
|
|
return;
|
|
}
|
|
} else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
subElementId = await tauri.addLocationSubElement(sections[sectionIndex].elements[elementIndex].id, newSubElementNames[elementIndex]);
|
|
if (!subElementId) {
|
|
errorMessage(t('locationComponent.errorUnknownAddSubElement'));
|
|
return;
|
|
}
|
|
} else {
|
|
subElementId = await apiPost<string>('location/sub-element/add', {
|
|
elementId: sections[sectionIndex].elements[elementIndex].id,
|
|
subElementName: newSubElementNames[elementIndex],
|
|
}, userToken, lang);
|
|
if (!subElementId) {
|
|
errorMessage(t('locationComponent.errorUnknownAddSubElement'));
|
|
return;
|
|
}
|
|
}
|
|
setSections(function (prev: LocationProps[]): LocationProps[] {
|
|
return prev.map(function (section: LocationProps, i: number): LocationProps {
|
|
if (i !== sectionIndex) return section;
|
|
return {
|
|
...section,
|
|
elements: section.elements.map(function (el, j: number) {
|
|
if (j !== elementIndex) return el;
|
|
return {
|
|
...el,
|
|
subElements: [...el.subElements, {
|
|
id: subElementId,
|
|
name: newSubElementNames[elementIndex],
|
|
description: '',
|
|
}],
|
|
};
|
|
}),
|
|
};
|
|
});
|
|
});
|
|
setNewSubElementNames(function (prev: { [key: string]: string }): { [key: string]: string } {
|
|
return {...prev, [elementIndex]: ''};
|
|
});
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('locationComponent.errorUnknownAddSubElement'));
|
|
}
|
|
}
|
|
}, [sections, newSubElementNames, isSeriesMode, userToken, lang, errorMessage, t]);
|
|
|
|
const removeSection = useCallback(async function (sectionId: string): Promise<void> {
|
|
try {
|
|
let success: boolean;
|
|
if (isSeriesMode) {
|
|
success = await apiDelete<boolean>('series/location/delete', {
|
|
locationId: sectionId
|
|
}, userToken, lang);
|
|
} else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
success = await tauri.deleteLocationSection(sectionId, book?.bookId ?? '', Date.now());
|
|
} else {
|
|
success = await apiDelete<boolean>('location/delete', {
|
|
locationId: sectionId,
|
|
}, userToken, lang);
|
|
}
|
|
if (!success) {
|
|
errorMessage(t('locationComponent.errorUnknownDeleteSection'));
|
|
return;
|
|
}
|
|
setSections(function (prev: LocationProps[]): LocationProps[] {
|
|
return prev.filter(function (section: LocationProps): boolean {
|
|
return section.id !== sectionId;
|
|
});
|
|
});
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('locationComponent.errorUnknownDeleteSection'));
|
|
}
|
|
}
|
|
}, [isSeriesMode, userToken, lang, errorMessage, t]);
|
|
|
|
const removeElement = useCallback(async function (sectionId: string, elementIndex: number): Promise<void> {
|
|
try {
|
|
const elementId: string | undefined = sections.find(function (section: LocationProps): boolean {
|
|
return section.id === sectionId;
|
|
})?.elements[elementIndex].id;
|
|
let success: boolean;
|
|
if (isSeriesMode) {
|
|
success = await apiDelete<boolean>('series/location/element/delete', {
|
|
elementId: elementId
|
|
}, userToken, lang);
|
|
} else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
success = await tauri.deleteLocationElement(elementId ?? '', book?.bookId ?? '', Date.now());
|
|
} else {
|
|
success = await apiDelete<boolean>('location/element/delete', {
|
|
elementId: elementId,
|
|
}, userToken, lang);
|
|
}
|
|
if (!success) {
|
|
errorMessage(t('locationComponent.errorUnknownDeleteElement'));
|
|
return;
|
|
}
|
|
setSections(function (prev: LocationProps[]): LocationProps[] {
|
|
const updated: LocationProps[] = [...prev];
|
|
const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean {
|
|
return section.id === sectionId;
|
|
});
|
|
updated[sectionIndex].elements.splice(elementIndex, 1);
|
|
return updated;
|
|
});
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('locationComponent.errorUnknownDeleteElement'));
|
|
}
|
|
}
|
|
}, [sections, isSeriesMode, userToken, lang, errorMessage, t]);
|
|
|
|
const removeSubElement = useCallback(async function (sectionId: string, elementIndex: number, subElementIndex: number): Promise<void> {
|
|
try {
|
|
const subElementId: string | undefined = sections.find(function (section: LocationProps): boolean {
|
|
return section.id === sectionId;
|
|
})?.elements[elementIndex].subElements[subElementIndex].id;
|
|
let success: boolean;
|
|
if (isSeriesMode) {
|
|
success = await apiDelete<boolean>('series/location/sub-element/delete', {
|
|
subElementId: subElementId
|
|
}, userToken, lang);
|
|
} else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
success = await tauri.deleteLocationSubElement(subElementId ?? '', book?.bookId ?? '', Date.now());
|
|
} else {
|
|
success = await apiDelete<boolean>('location/sub-element/delete', {
|
|
subElementId: subElementId,
|
|
}, userToken, lang);
|
|
}
|
|
if (!success) {
|
|
errorMessage(t('locationComponent.errorUnknownDeleteSubElement'));
|
|
return;
|
|
}
|
|
setSections(function (prev: LocationProps[]): LocationProps[] {
|
|
const updated: LocationProps[] = [...prev];
|
|
const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean {
|
|
return section.id === sectionId;
|
|
});
|
|
updated[sectionIndex].elements[elementIndex].subElements.splice(subElementIndex, 1);
|
|
return updated;
|
|
});
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('locationComponent.errorUnknownDeleteSubElement'));
|
|
}
|
|
}
|
|
}, [sections, isSeriesMode, userToken, lang, errorMessage, t]);
|
|
|
|
const updateElement = useCallback(function (sectionId: string, elementIndex: number, field: keyof Element, value: string): void {
|
|
setSections(function (prev: LocationProps[]): LocationProps[] {
|
|
const updated: LocationProps[] = [...prev];
|
|
const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean {
|
|
return section.id === sectionId;
|
|
});
|
|
// @ts-ignore
|
|
updated[sectionIndex].elements[elementIndex][field] = value;
|
|
return updated;
|
|
});
|
|
}, []);
|
|
|
|
const updateSubElement = useCallback(function (sectionId: string, elementIndex: number, subElementIndex: number, field: keyof SubElement, value: string): void {
|
|
setSections(function (prev: LocationProps[]): LocationProps[] {
|
|
const updated: LocationProps[] = [...prev];
|
|
const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean {
|
|
return section.id === sectionId;
|
|
});
|
|
updated[sectionIndex].elements[elementIndex].subElements[subElementIndex][field] = value;
|
|
return updated;
|
|
});
|
|
}, []);
|
|
|
|
const saveLocations = useCallback(async function (): Promise<void> {
|
|
try {
|
|
let response: boolean;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.updateLocations(sections) as boolean;
|
|
} else {
|
|
response = await apiPost<boolean>('location/update', {
|
|
locations: sections,
|
|
}, userToken, lang);
|
|
}
|
|
if (!response) {
|
|
errorMessage(t('locationComponent.errorUnknownSave'));
|
|
return;
|
|
}
|
|
successMessage(t('locationComponent.successSave'));
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('locationComponent.errorUnknownSave'));
|
|
}
|
|
}
|
|
}, [sections, userToken, lang, errorMessage, successMessage, t]);
|
|
|
|
const exportToSeries = useCallback(async function (section: LocationProps): Promise<void> {
|
|
if (!bookSeriesId) return;
|
|
|
|
try {
|
|
const seriesLocationId: string = await apiPost<string>('series/location/section/add', {
|
|
seriesId: bookSeriesId,
|
|
name: section.name,
|
|
}, userToken, lang);
|
|
|
|
if (seriesLocationId) {
|
|
const updateResponse: boolean = await apiPost<boolean>('location/section/update', {
|
|
sectionId: section.id,
|
|
sectionName: section.name,
|
|
seriesLocationId: seriesLocationId,
|
|
}, userToken, lang);
|
|
|
|
if (updateResponse) {
|
|
setSections(function (prev: LocationProps[]): LocationProps[] {
|
|
return prev.map(function (s: LocationProps): LocationProps {
|
|
return s.id === section.id ? {...s, seriesLocationId: seriesLocationId} : s;
|
|
});
|
|
});
|
|
const newSeriesLocation: SeriesLocationItem = {
|
|
id: seriesLocationId,
|
|
name: section.name,
|
|
elements: [],
|
|
};
|
|
setSeriesLocations(function (prev: SeriesLocationItem[]): SeriesLocationItem[] {
|
|
return [...prev, newSeriesLocation];
|
|
});
|
|
successMessage(t("locationComponent.exportSuccess"));
|
|
}
|
|
}
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
}
|
|
}
|
|
}, [bookSeriesId, userToken, lang, successMessage, errorMessage, t]);
|
|
|
|
const importFromSeries = useCallback(async function (seriesLocationId: string): Promise<void> {
|
|
const seriesLocation: SeriesLocationItem | undefined = seriesLocations.find(function (location: SeriesLocationItem): boolean {
|
|
return location.id === seriesLocationId;
|
|
});
|
|
if (!seriesLocation) return;
|
|
|
|
try {
|
|
let sectionId: string;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
sectionId = await tauri.addLocationSection(seriesLocation.name, entityId, undefined, seriesLocationId);
|
|
} else {
|
|
sectionId = await apiPost<string>('location/section/add', {
|
|
bookId: entityId,
|
|
locationName: seriesLocation.name,
|
|
seriesLocationId: seriesLocationId,
|
|
}, userToken, lang);
|
|
}
|
|
|
|
if (!sectionId) {
|
|
errorMessage(t('locationComponent.importError'));
|
|
return;
|
|
}
|
|
|
|
const importedElements: Element[] = [];
|
|
|
|
for (const seriesElement of seriesLocation.elements) {
|
|
let elementId: string;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
elementId = await tauri.addLocationElement(sectionId, seriesElement.name);
|
|
} else {
|
|
elementId = await apiPost<string>('location/element/add', {
|
|
bookId: entityId,
|
|
locationId: sectionId,
|
|
elementName: seriesElement.name,
|
|
}, userToken, lang);
|
|
}
|
|
|
|
if (!elementId) continue;
|
|
|
|
const importedSubElements: SubElement[] = [];
|
|
|
|
for (const seriesSubElement of seriesElement.subElements) {
|
|
let subElementId: string;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
subElementId = await tauri.addLocationSubElement(elementId, seriesSubElement.name);
|
|
} else {
|
|
subElementId = await apiPost<string>('location/sub-element/add', {
|
|
elementId: elementId,
|
|
subElementName: seriesSubElement.name,
|
|
}, userToken, lang);
|
|
}
|
|
|
|
if (subElementId) {
|
|
importedSubElements.push({
|
|
id: subElementId,
|
|
name: seriesSubElement.name,
|
|
description: seriesSubElement.description,
|
|
});
|
|
}
|
|
}
|
|
|
|
importedElements.push({
|
|
id: elementId,
|
|
name: seriesElement.name,
|
|
description: seriesElement.description,
|
|
subElements: importedSubElements,
|
|
});
|
|
}
|
|
|
|
const newLocation: LocationProps = {
|
|
id: sectionId,
|
|
name: seriesLocation.name,
|
|
elements: importedElements,
|
|
seriesLocationId: seriesLocationId,
|
|
};
|
|
setSections(function (prev: LocationProps[]): LocationProps[] {
|
|
return [...prev, newLocation];
|
|
});
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
}
|
|
}
|
|
}, [seriesLocations, entityId, userToken, lang, errorMessage, successMessage, t]);
|
|
|
|
// Navigation functions
|
|
const enterDetailMode = useCallback(function (sectionIndex: number): void {
|
|
setSelectedSectionIndex(sectionIndex);
|
|
setViewMode('detail');
|
|
setSectionsBackup(null);
|
|
}, []);
|
|
|
|
const enterEditMode = useCallback(function (): void {
|
|
setSectionsBackup(sections.map(function (section: LocationProps): LocationProps {
|
|
return {
|
|
...section,
|
|
elements: section.elements.map(function (element: Element): Element {
|
|
return {
|
|
...element,
|
|
subElements: [...element.subElements]
|
|
};
|
|
})
|
|
};
|
|
}));
|
|
setViewMode('edit');
|
|
}, [sections]);
|
|
|
|
const exitEditMode = useCallback(async function (save: boolean): Promise<void> {
|
|
if (save) {
|
|
await saveLocations();
|
|
setViewMode('detail');
|
|
} else {
|
|
if (sectionsBackup) {
|
|
setSections(sectionsBackup);
|
|
setViewMode('detail');
|
|
} else {
|
|
setViewMode('list');
|
|
}
|
|
}
|
|
setSectionsBackup(null);
|
|
}, [saveLocations, sectionsBackup]);
|
|
|
|
const backToList = useCallback(function (): void {
|
|
setSelectedSectionIndex(-1);
|
|
setSectionsBackup(null);
|
|
setViewMode('list');
|
|
}, []);
|
|
|
|
return {
|
|
// State
|
|
sections,
|
|
seriesLocations,
|
|
toolEnabled,
|
|
isLoading,
|
|
isSeriesMode,
|
|
bookSeriesId,
|
|
newSectionName,
|
|
newElementNames,
|
|
newSubElementNames,
|
|
|
|
// Navigation state
|
|
viewMode,
|
|
selectedSectionIndex,
|
|
sectionsBackup,
|
|
|
|
// Actions
|
|
addSection,
|
|
addElement,
|
|
addSubElement,
|
|
removeSection,
|
|
removeElement,
|
|
removeSubElement,
|
|
updateElement,
|
|
updateSubElement,
|
|
saveLocations,
|
|
toggleTool,
|
|
importFromSeries,
|
|
exportToSeries,
|
|
refreshLocations,
|
|
refreshSeriesLocations,
|
|
setNewSectionName,
|
|
setNewElementNames,
|
|
setNewSubElementNames,
|
|
|
|
// Navigation actions
|
|
enterDetailMode,
|
|
enterEditMode,
|
|
exitEditMode,
|
|
backToList,
|
|
};
|
|
}
|