Bump app version to 0.5.0 and implement offline mode support across components
- 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.
This commit is contained in:
@@ -284,62 +284,6 @@ body {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.indent-0 {
|
||||
text-indent: 0px !important;
|
||||
}
|
||||
|
||||
.indent-1 {
|
||||
text-indent: 4px !important;
|
||||
}
|
||||
|
||||
.indent-2 {
|
||||
text-indent: 8px !important;
|
||||
}
|
||||
|
||||
.indent-3 {
|
||||
text-indent: 12px !important;
|
||||
}
|
||||
|
||||
.indent-4 {
|
||||
text-indent: 16px !important;
|
||||
}
|
||||
|
||||
.indent-5 {
|
||||
text-indent: 20px !important;
|
||||
}
|
||||
|
||||
.indent-6 {
|
||||
text-indent: 24px !important;
|
||||
}
|
||||
|
||||
.indent-7 {
|
||||
text-indent: 28px !important;
|
||||
}
|
||||
|
||||
.indent-8 {
|
||||
text-indent: 32px !important;
|
||||
}
|
||||
|
||||
.indent-9 {
|
||||
text-indent: 36px !important;
|
||||
}
|
||||
|
||||
.indent-10 {
|
||||
text-indent: 40px !important;
|
||||
}
|
||||
|
||||
.indent-11 {
|
||||
text-indent: 44px !important;
|
||||
}
|
||||
|
||||
.indent-12 {
|
||||
text-indent: 48px !important;
|
||||
}
|
||||
|
||||
.indent-13 {
|
||||
text-indent: 52px !important;
|
||||
}
|
||||
|
||||
/* Styles pour l'éditeur principal avec classes dynamiques */
|
||||
.editor-content .tiptap > p:first-child,
|
||||
.editor-content .tiptap > h1:first-child,
|
||||
@@ -350,14 +294,17 @@ body {
|
||||
|
||||
.editor-content .tiptap p {
|
||||
color: var(--color-editor-text);
|
||||
text-indent: 1.25rem;
|
||||
overflow-wrap: anywhere;
|
||||
margin-top: 0.7em;
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
|
||||
.editor-content .tiptap p {
|
||||
text-indent: inherit;
|
||||
.no-text-indent .editor-content .tiptap p {
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
|
||||
.editor-content .tiptap p strong {
|
||||
font-weight: 900;
|
||||
color: var(--color-editor-bold);
|
||||
@@ -628,11 +575,17 @@ body {
|
||||
|
||||
.tiptap-draft p {
|
||||
font-family: 'Lora', sans-serif;
|
||||
text-indent: 30px;
|
||||
text-indent: 1.25rem;
|
||||
overflow-wrap: anywhere;
|
||||
margin-top: 0.7em;
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
|
||||
.no-text-indent .tiptap-draft p {
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Form input base */
|
||||
.input-base {
|
||||
@apply w-full text-text-primary bg-dark-background px-4 py-2.5 rounded-xl
|
||||
|
||||
@@ -18,9 +18,8 @@ export default function BookCard({book, onClickCallback, index, syncStatus}: Boo
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={(): void => onClickCallback(book.bookId)}
|
||||
className="group relative aspect-[2/3] rounded-xl overflow-hidden cursor-pointer transition-all duration-300 hover:ring-1 hover:ring-text-primary/20">
|
||||
<button onClick={(): void => onClickCallback(book.bookId)} className="w-full h-full text-left block"
|
||||
type="button">
|
||||
{book.coverImage ? (
|
||||
<img
|
||||
src={book.coverImage}
|
||||
@@ -34,7 +33,6 @@ export default function BookCard({book, onClickCallback, index, syncStatus}: Boo
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isDesktop && syncStatus && (
|
||||
<div className="absolute top-2 left-2 cursor-default" onClick={(e: React.MouseEvent): void => e.stopPropagation()}>
|
||||
|
||||
@@ -10,7 +10,10 @@ import {BookContext, BookContextProps} from "@/context/BookContext";
|
||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {isDesktop} from "@/lib/configs";
|
||||
import {apiPatch} from "@/lib/api/client";
|
||||
import {updateBookToolSetting} from "@/lib/tauri";
|
||||
import {SettingRef} from "@/lib/types/settings";
|
||||
import {BookProps} from "@/lib/types/book";
|
||||
|
||||
@@ -73,6 +76,7 @@ export default function BookSettingOption({setting}: BookSettingOptionProps): Re
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
const userToken: string = session?.accessToken ?? '';
|
||||
|
||||
const isToggleable: boolean = setting in toggleableSettings;
|
||||
@@ -107,10 +111,17 @@ export default function BookSettingOption({setting}: BookSettingOptionProps): Re
|
||||
|
||||
async function handleToggleTool(enabled: boolean): Promise<void> {
|
||||
const toolName: ToolName | undefined = toggleableSettings[setting];
|
||||
if (!toolName) return;
|
||||
if (!toolName || !book?.bookId) return;
|
||||
const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book.localBook);
|
||||
if (useLocal && toolName === 'quillsense') {
|
||||
errorMessage(t('bookSettingOption.quillsenseOffline'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result: boolean = await apiPatch<boolean>('book/tool-setting', {
|
||||
bookId: book?.bookId,
|
||||
const result: boolean = useLocal
|
||||
? await updateBookToolSetting(book.bookId, toolName, enabled)
|
||||
: await apiPatch<boolean>('book/tool-setting', {
|
||||
bookId: book.bookId,
|
||||
toolName: toolName,
|
||||
enabled: enabled
|
||||
}, userToken, lang);
|
||||
|
||||
@@ -10,7 +10,11 @@ import AvatarIcon from '@/components/ui/AvatarIcon';
|
||||
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
import {LangContext, LangContextProps} from '@/context/LangContext';
|
||||
import {BookContext, BookContextProps} from '@/context/BookContext';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import {apiGet} from '@/lib/api/client';
|
||||
import {getCharacterAttributes} from '@/lib/tauri';
|
||||
|
||||
type AttributeResponse = { type: string; values: Attribute[] }[];
|
||||
|
||||
@@ -34,6 +38,8 @@ export default function CharacterEditorDetail({
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
|
||||
useEffect(function (): void {
|
||||
if (character?.id !== null) {
|
||||
@@ -43,7 +49,10 @@ export default function CharacterEditorDetail({
|
||||
|
||||
async function getAttributes(): Promise<void> {
|
||||
try {
|
||||
const response: AttributeResponse = await apiGet<AttributeResponse>(
|
||||
const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook);
|
||||
const response: AttributeResponse = useLocal
|
||||
? await getCharacterAttributes(character.id!) as AttributeResponse
|
||||
: await apiGet<AttributeResponse>(
|
||||
'character/attribute',
|
||||
session.accessToken,
|
||||
lang,
|
||||
|
||||
@@ -28,7 +28,11 @@ import {useTranslations} from '@/lib/i18n';
|
||||
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
import {LangContext, LangContextProps} from '@/context/LangContext';
|
||||
import {BookContext, BookContextProps} from '@/context/BookContext';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import {apiGet} from '@/lib/api/client';
|
||||
import {getCharacterAttributes} from '@/lib/tauri';
|
||||
|
||||
type AttributeResponse = { type: string; values: Attribute[] }[];
|
||||
|
||||
@@ -59,6 +63,8 @@ export default function CharacterEditorEdit({
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false);
|
||||
|
||||
useEffect(function (): void {
|
||||
@@ -69,7 +75,10 @@ export default function CharacterEditorEdit({
|
||||
|
||||
async function getAttributes(): Promise<void> {
|
||||
try {
|
||||
const response: AttributeResponse = await apiGet<AttributeResponse>(
|
||||
const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook);
|
||||
const response: AttributeResponse = useLocal
|
||||
? await getCharacterAttributes(character.id!) as AttributeResponse
|
||||
: await apiGet<AttributeResponse>(
|
||||
'character/attribute',
|
||||
session.accessToken,
|
||||
lang,
|
||||
|
||||
@@ -33,7 +33,11 @@ import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
import {dynamicBg} from '@/lib/utils/dynamicStyles';
|
||||
import {LangContext, LangContextProps} from '@/context/LangContext';
|
||||
import {BookContext, BookContextProps} from '@/context/BookContext';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import {apiGet} from '@/lib/api/client';
|
||||
import {getCharacterAttributes} from '@/lib/tauri';
|
||||
|
||||
type AttributeResponse = { type: string; values: Attribute[] }[];
|
||||
|
||||
@@ -52,6 +56,8 @@ export default function CharacterSettingsDetail({
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false);
|
||||
|
||||
useEffect(function (): void {
|
||||
@@ -62,7 +68,10 @@ export default function CharacterSettingsDetail({
|
||||
|
||||
async function getAttributes(): Promise<void> {
|
||||
try {
|
||||
const response: AttributeResponse = await apiGet<AttributeResponse>(
|
||||
const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook);
|
||||
const response: AttributeResponse = useLocal
|
||||
? await getCharacterAttributes(character.id!) as AttributeResponse
|
||||
: await apiGet<AttributeResponse>(
|
||||
'character/attribute',
|
||||
session.accessToken,
|
||||
lang,
|
||||
|
||||
@@ -29,7 +29,11 @@ import {useTranslations} from '@/lib/i18n';
|
||||
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
import {LangContext, LangContextProps} from '@/context/LangContext';
|
||||
import {BookContext, BookContextProps} from '@/context/BookContext';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import {apiGet} from '@/lib/api/client';
|
||||
import {getCharacterAttributes} from '@/lib/tauri';
|
||||
|
||||
type AttributeResponse = { type: string; values: Attribute[] }[];
|
||||
|
||||
@@ -61,6 +65,8 @@ export default function CharacterSettingsEdit({
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false);
|
||||
|
||||
useEffect(function (): void {
|
||||
@@ -71,7 +77,10 @@ export default function CharacterSettingsEdit({
|
||||
|
||||
async function getAttributes(): Promise<void> {
|
||||
try {
|
||||
const response: AttributeResponse = await apiGet<AttributeResponse>(
|
||||
const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook);
|
||||
const response: AttributeResponse = useLocal
|
||||
? await getCharacterAttributes(character.id!) as AttributeResponse
|
||||
: await apiGet<AttributeResponse>(
|
||||
'character/attribute',
|
||||
session.accessToken,
|
||||
lang,
|
||||
|
||||
@@ -4,7 +4,10 @@ import React, {ChangeEvent, forwardRef, useContext, useEffect, useImperativeHand
|
||||
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 {isDesktop} from '@/lib/configs';
|
||||
import {apiDelete, apiGet, apiPatch, apiPost} from '@/lib/api/client';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import InputField from "@/components/form/InputField";
|
||||
import TextInput from '@/components/form/TextInput';
|
||||
import TextAreaInput from "@/components/form/TextAreaInput";
|
||||
@@ -53,8 +56,10 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {successMessage, errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
|
||||
const currentEntityId: string = entityId || book?.bookId || '';
|
||||
const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook);
|
||||
const isSeriesMode: boolean = entityType === 'series';
|
||||
const token: string = session.accessToken;
|
||||
|
||||
@@ -87,7 +92,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
async function getSeriesLocations(): Promise<void> {
|
||||
if (!bookSeriesId) return;
|
||||
try {
|
||||
const response: SeriesLocationItem[] = await apiGet<SeriesLocationItem[]>(
|
||||
const response: SeriesLocationItem[] = useLocal
|
||||
? await tauri.getSeriesLocationList(bookSeriesId) as SeriesLocationItem[]
|
||||
: await apiGet<SeriesLocationItem[]>(
|
||||
'series/location/list',
|
||||
token,
|
||||
lang,
|
||||
@@ -106,7 +113,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
async function handleToggleTool(enabled: boolean): Promise<void> {
|
||||
if (isSeriesMode) return;
|
||||
try {
|
||||
const response: boolean = await apiPatch<boolean>('book/tool-setting', {
|
||||
const response: boolean = useLocal
|
||||
? await tauri.updateBookToolSetting(currentEntityId, 'locations', enabled)
|
||||
: await apiPatch<boolean>('book/tool-setting', {
|
||||
bookId: currentEntityId,
|
||||
toolName: 'locations',
|
||||
enabled: enabled
|
||||
@@ -132,7 +141,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
async function getAllLocations(): Promise<void> {
|
||||
try {
|
||||
if (isSeriesMode) {
|
||||
const response: SeriesLocationItem[] = await apiGet<SeriesLocationItem[]>(
|
||||
const response: SeriesLocationItem[] = useLocal
|
||||
? await tauri.getSeriesLocationList(currentEntityId) as SeriesLocationItem[]
|
||||
: await apiGet<SeriesLocationItem[]>(
|
||||
'series/location/list',
|
||||
token,
|
||||
lang,
|
||||
@@ -156,7 +167,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
setSections(mappedLocations);
|
||||
}
|
||||
} else {
|
||||
const response: LocationListResponse = await apiGet<LocationListResponse>(
|
||||
const response: LocationListResponse = useLocal
|
||||
? await tauri.getAllLocations(currentEntityId, true) as LocationListResponse
|
||||
: await apiGet<LocationListResponse>(
|
||||
'location/all',
|
||||
token,
|
||||
lang,
|
||||
@@ -194,24 +207,17 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
try {
|
||||
let sectionId: string;
|
||||
if (isSeriesMode) {
|
||||
sectionId = await apiPost<string>(
|
||||
'series/location/section/add',
|
||||
{
|
||||
seriesId: currentEntityId,
|
||||
name: newSectionName,
|
||||
},
|
||||
token,
|
||||
lang
|
||||
);
|
||||
sectionId = useLocal
|
||||
? await tauri.addSeriesLocationSection({seriesId: currentEntityId, name: newSectionName})
|
||||
: await apiPost<string>('series/location/section/add', {seriesId: currentEntityId, name: newSectionName}, token, lang);
|
||||
if (!sectionId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSection'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
sectionId = await apiPost<string>('location/section/add', {
|
||||
bookId: currentEntityId,
|
||||
locationName: newSectionName,
|
||||
}, token, lang);
|
||||
sectionId = useLocal
|
||||
? await tauri.addLocationSection(newSectionName, currentEntityId)
|
||||
: await apiPost<string>('location/section/add', {bookId: currentEntityId, locationName: newSectionName}, token, lang);
|
||||
if (!sectionId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSection'));
|
||||
return;
|
||||
@@ -241,25 +247,17 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
try {
|
||||
let elementId: string;
|
||||
if (isSeriesMode) {
|
||||
elementId = await apiPost<string>(
|
||||
'series/location/element/add',
|
||||
{
|
||||
locationId: sectionId,
|
||||
name: newElementNames[sectionId],
|
||||
},
|
||||
token,
|
||||
lang
|
||||
);
|
||||
elementId = useLocal
|
||||
? await tauri.addSeriesLocationElement({locationId: sectionId, name: newElementNames[sectionId]})
|
||||
: await apiPost<string>('series/location/element/add', {locationId: sectionId, name: newElementNames[sectionId]}, token, lang);
|
||||
if (!elementId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddElement'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
elementId = await apiPost<string>('location/element/add', {
|
||||
bookId: currentEntityId,
|
||||
locationId: sectionId,
|
||||
elementName: newElementNames[sectionId],
|
||||
}, token, lang);
|
||||
elementId = useLocal
|
||||
? await tauri.addLocationElement(sectionId, newElementNames[sectionId])
|
||||
: await apiPost<string>('location/element/add', {bookId: currentEntityId, locationId: sectionId, elementName: newElementNames[sectionId]}, token, lang);
|
||||
if (!elementId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddElement'));
|
||||
return;
|
||||
@@ -314,25 +312,19 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
);
|
||||
try {
|
||||
let subElementId: string;
|
||||
const parentElementId: string = sections[sectionIndex].elements[elementIndex].id;
|
||||
if (isSeriesMode) {
|
||||
subElementId = await apiPost<string>(
|
||||
'series/location/sub-element/add',
|
||||
{
|
||||
elementId: sections[sectionIndex].elements[elementIndex].id,
|
||||
name: newSubElementNames[elementIndex],
|
||||
},
|
||||
token,
|
||||
lang
|
||||
);
|
||||
subElementId = useLocal
|
||||
? await tauri.addSeriesLocationSubElement({elementId: parentElementId, name: newSubElementNames[elementIndex]})
|
||||
: await apiPost<string>('series/location/sub-element/add', {elementId: parentElementId, name: newSubElementNames[elementIndex]}, token, lang);
|
||||
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],
|
||||
}, token, lang);
|
||||
subElementId = useLocal
|
||||
? await tauri.addLocationSubElement(parentElementId, newSubElementNames[elementIndex])
|
||||
: await apiPost<string>('location/sub-element/add', {elementId: parentElementId, subElementName: newSubElementNames[elementIndex]}, token, lang);
|
||||
if (!subElementId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSubElement'));
|
||||
return;
|
||||
@@ -379,15 +371,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
try {
|
||||
const elementId: string | undefined = sections.find((section: LocationProps): boolean => section.id === sectionId)
|
||||
?.elements[elementIndex].id;
|
||||
const deletedAt: number = Math.floor(Date.now() / 1000);
|
||||
let success: boolean;
|
||||
if (isSeriesMode) {
|
||||
success = await apiDelete<boolean>('series/location/element/delete', {
|
||||
elementId: elementId
|
||||
}, token, lang);
|
||||
success = useLocal
|
||||
? await tauri.deleteSeriesLocationElement(elementId!, deletedAt)
|
||||
: await apiDelete<boolean>('series/location/element/delete', {elementId: elementId}, token, lang);
|
||||
} else {
|
||||
success = await apiDelete<boolean>('location/element/delete', {
|
||||
elementId: elementId,
|
||||
}, token, lang);
|
||||
success = useLocal
|
||||
? await tauri.deleteLocationElement(elementId!, currentEntityId, deletedAt)
|
||||
: await apiDelete<boolean>('location/element/delete', {elementId: elementId}, token, lang);
|
||||
}
|
||||
if (!success) {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteElement'));
|
||||
@@ -414,15 +407,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
try {
|
||||
const subElementId: string | undefined = sections.find((section: LocationProps): boolean => section.id === sectionId)
|
||||
?.elements[elementIndex].subElements[subElementIndex].id;
|
||||
const deletedAt: number = Math.floor(Date.now() / 1000);
|
||||
let success: boolean;
|
||||
if (isSeriesMode) {
|
||||
success = await apiDelete<boolean>('series/location/sub-element/delete', {
|
||||
subElementId: subElementId
|
||||
}, token, lang);
|
||||
success = useLocal
|
||||
? await tauri.deleteSeriesLocationSubElement(subElementId!, deletedAt)
|
||||
: await apiDelete<boolean>('series/location/sub-element/delete', {subElementId: subElementId}, token, lang);
|
||||
} else {
|
||||
success = await apiDelete<boolean>('location/sub-element/delete', {
|
||||
subElementId: subElementId,
|
||||
}, token, lang);
|
||||
success = useLocal
|
||||
? await tauri.deleteLocationSubElement(subElementId!, currentEntityId, deletedAt)
|
||||
: await apiDelete<boolean>('location/sub-element/delete', {subElementId: subElementId}, token, lang);
|
||||
}
|
||||
if (!success) {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteSubElement'));
|
||||
@@ -443,15 +437,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
|
||||
async function handleRemoveSection(sectionId: string): Promise<void> {
|
||||
try {
|
||||
const deletedAt: number = Math.floor(Date.now() / 1000);
|
||||
let success: boolean;
|
||||
if (isSeriesMode) {
|
||||
success = await apiDelete<boolean>('series/location/delete', {
|
||||
locationId: sectionId
|
||||
}, token, lang);
|
||||
success = useLocal
|
||||
? await tauri.deleteSeriesLocation(sectionId, deletedAt)
|
||||
: await apiDelete<boolean>('series/location/delete', {locationId: sectionId}, token, lang);
|
||||
} else {
|
||||
success = await apiDelete<boolean>('location/delete', {
|
||||
locationId: sectionId,
|
||||
}, token, lang);
|
||||
success = useLocal
|
||||
? await tauri.deleteLocationSection(sectionId, currentEntityId, deletedAt)
|
||||
: await apiDelete<boolean>('location/delete', {locationId: sectionId}, token, lang);
|
||||
}
|
||||
if (!success) {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteSection'));
|
||||
@@ -470,9 +465,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
|
||||
async function handleSave(): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await apiPost<boolean>(`location/update`, {
|
||||
locations: sections,
|
||||
}, token, lang);
|
||||
const response: boolean = useLocal
|
||||
? await tauri.updateLocations(sections) as boolean
|
||||
: await apiPost<boolean>(`location/update`, {locations: sections}, token, lang);
|
||||
if (!response) {
|
||||
errorMessage(t('locationComponent.errorUnknownSave'));
|
||||
return;
|
||||
@@ -491,17 +486,14 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
if (!bookSeriesId) return;
|
||||
|
||||
try {
|
||||
const seriesLocationId: string = await apiPost<string>('series/location/section/add', {
|
||||
seriesId: bookSeriesId,
|
||||
name: section.name,
|
||||
}, token, lang);
|
||||
const seriesLocationId: string = useLocal
|
||||
? await tauri.addSeriesLocationSection({seriesId: bookSeriesId, name: section.name})
|
||||
: await apiPost<string>('series/location/section/add', {seriesId: bookSeriesId, name: section.name}, token, lang);
|
||||
|
||||
if (seriesLocationId) {
|
||||
const updateResponse: boolean = await apiPost<boolean>('location/section/update', {
|
||||
sectionId: section.id,
|
||||
sectionName: section.name,
|
||||
seriesLocationId: seriesLocationId,
|
||||
}, token, lang);
|
||||
const updateResponse: boolean = useLocal
|
||||
? await tauri.updateLocationSectionWithSeriesLink(section.id, section.name, seriesLocationId)
|
||||
: await apiPost<boolean>('location/section/update', {sectionId: section.id, sectionName: section.name, seriesLocationId: seriesLocationId}, token, lang);
|
||||
|
||||
if (updateResponse) {
|
||||
setSections(sections.map((s: LocationProps): LocationProps =>
|
||||
@@ -523,11 +515,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
if (!seriesLocation) return;
|
||||
|
||||
try {
|
||||
const sectionId: string = await apiPost<string>('location/section/add', {
|
||||
bookId: currentEntityId,
|
||||
locationName: seriesLocation.name,
|
||||
seriesLocationId: seriesLocationId,
|
||||
}, token, lang);
|
||||
const sectionId: string = useLocal
|
||||
? await tauri.addLocationSection(seriesLocation.name, currentEntityId, undefined, seriesLocationId)
|
||||
: await apiPost<string>('location/section/add', {bookId: currentEntityId, locationName: seriesLocation.name, seriesLocationId: seriesLocationId}, token, lang);
|
||||
|
||||
if (!sectionId) {
|
||||
errorMessage(t('locationComponent.importError'));
|
||||
@@ -537,21 +527,18 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
const importedElements: Element[] = [];
|
||||
|
||||
for (const seriesElement of seriesLocation.elements) {
|
||||
const elementId: string = await apiPost<string>('location/element/add', {
|
||||
bookId: currentEntityId,
|
||||
locationId: sectionId,
|
||||
elementName: seriesElement.name,
|
||||
}, token, lang);
|
||||
const elementId: string = useLocal
|
||||
? await tauri.addLocationElement(sectionId, seriesElement.name)
|
||||
: await apiPost<string>('location/element/add', {bookId: currentEntityId, locationId: sectionId, elementName: seriesElement.name}, token, lang);
|
||||
|
||||
if (!elementId) continue;
|
||||
|
||||
const importedSubElements: SubElement[] = [];
|
||||
|
||||
for (const seriesSubElement of seriesElement.subElements) {
|
||||
const subElementId: string = await apiPost<string>('location/sub-element/add', {
|
||||
elementId: elementId,
|
||||
subElementName: seriesSubElement.name,
|
||||
}, token, lang);
|
||||
const subElementId: string = useLocal
|
||||
? await tauri.addLocationSubElement(elementId, seriesSubElement.name)
|
||||
: await apiPost<string>('location/sub-element/add', {elementId: elementId, subElementName: seriesSubElement.name}, token, lang);
|
||||
|
||||
if (subElementId) {
|
||||
importedSubElements.push({
|
||||
|
||||
@@ -161,7 +161,16 @@ export default function LocationSettings({
|
||||
|
||||
{viewMode === 'detail' && selectedSection && (
|
||||
<div className="p-4">
|
||||
<LocationSettingsDetail section={selectedSection}/>
|
||||
<LocationSettingsDetail
|
||||
section={selectedSection}
|
||||
newElementName={newElementNames[selectedSection.id] || ''}
|
||||
onNewElementNameChange={function (name: string): void {
|
||||
setNewElementNames({...newElementNames, [selectedSection.id]: name});
|
||||
}}
|
||||
onAddElement={function (): Promise<void> {
|
||||
return addElement(selectedSection.id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function LocationSettingsDetail({
|
||||
className="text-center py-12 text-text-secondary">
|
||||
<MapPin className="w-8 h-8 mb-3 opacity-50" strokeWidth={1.75}/>
|
||||
<p>{t("locationComponent.noElementAvailable")}</p>
|
||||
<p className="text-sm mt-2 text-text-dimmed">{t("locationComponent.editToAdd")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
@@ -31,9 +31,14 @@ import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import DraftCompanion from "@/components/editor/DraftCompanion";
|
||||
import GhostWriter from "@/components/ghostwriter/GhostWriter";
|
||||
import IconButton from "@/components/ui/IconButton";
|
||||
import Button from "@/components/ui/Button";
|
||||
import UserEditorSettings, {EditorDisplaySettings} from "@/components/editor/UserEditorSetting";
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
import {isWebKitWithoutIndentFix} from "@/lib/utils/webkitDetect";
|
||||
import {getCookie, setCookie} from "@/lib/utils/cookies";
|
||||
import Modal from "@/components/ui/Modal";
|
||||
import {Info} from 'lucide-react';
|
||||
|
||||
interface ToolbarButton {
|
||||
action: () => void;
|
||||
@@ -48,14 +53,12 @@ interface EditorClasses {
|
||||
h3: string;
|
||||
container: string;
|
||||
theme: string;
|
||||
paragraph: string;
|
||||
lists: string;
|
||||
listItems: string;
|
||||
}
|
||||
|
||||
const defaultEditorSettings: EditorDisplaySettings = {
|
||||
zoomLevel: 3,
|
||||
indent: 30,
|
||||
lineHeight: 1.5,
|
||||
theme: 'sombre',
|
||||
fontFamily: 'lora',
|
||||
@@ -148,6 +151,8 @@ export default function TextEditor() {
|
||||
const [showUserSettings, setShowUserSettings] = useState<boolean>(false);
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [editorSettings, setEditorSettings] = useState<EditorDisplaySettings>(defaultEditorSettings);
|
||||
const [indentDisabled] = useState<boolean>(() => isWebKitWithoutIndentFix());
|
||||
const [showIndentModal, setShowIndentModal] = useState<boolean>(() => isWebKitWithoutIndentFix() && !getCookie('indent_notice_seen'));
|
||||
const [editorClasses, setEditorClasses] = useState<EditorClasses>({
|
||||
base: 'text-lg font-serif leading-normal',
|
||||
h1: 'text-3xl font-bold',
|
||||
@@ -155,7 +160,6 @@ export default function TextEditor() {
|
||||
h3: 'text-xl font-bold',
|
||||
container: 'max-w-3xl',
|
||||
theme: 'bg-tertiary text-text-primary',
|
||||
paragraph: 'indent-6',
|
||||
lists: 'pl-10',
|
||||
listItems: 'text-lg'
|
||||
});
|
||||
@@ -170,14 +174,12 @@ export default function TextEditor() {
|
||||
|
||||
const fontFamily: string = fontFamilyClasses[settings.fontFamily] || fontFamilyClasses['lora'];
|
||||
const lineHeight: string = lineHeightClasses[lineHeightKey];
|
||||
const indentClass: string = `indent-${Math.round(settings.indent / 4)}`;
|
||||
|
||||
const baseClass: string = `${fontSizeClasses[zoomKey]} ${fontFamily} ${lineHeight}`;
|
||||
const h1Class: string = `${h1SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`;
|
||||
const h2Class: string = `${h2SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`;
|
||||
const h3Class: string = `${h3SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`;
|
||||
const containerClass: string = maxWidthClasses[maxWidthKey];
|
||||
const listsClass: string = `pl-${Math.round((settings.indent + 20) / 4)}`;
|
||||
const listsClass: string = 'pl-12';
|
||||
|
||||
let themeClass: string = '';
|
||||
switch (settings.theme) {
|
||||
@@ -198,7 +200,6 @@ export default function TextEditor() {
|
||||
h3: h3Class,
|
||||
container: containerClass,
|
||||
theme: themeClass,
|
||||
paragraph: indentClass,
|
||||
lists: listsClass,
|
||||
listItems: baseClass
|
||||
});
|
||||
@@ -336,24 +337,16 @@ export default function TextEditor() {
|
||||
setShowGhostWriter(false);
|
||||
}, []);
|
||||
|
||||
useEffect((): void => {
|
||||
if (!editor) return;
|
||||
|
||||
const editorElement: HTMLElement = editor.view.dom;
|
||||
if (editorElement) {
|
||||
const indentClasses: string[] = Array.from({length: 21}, (_: unknown, i: number): string => `indent-${i}`);
|
||||
editorElement.classList.remove(...indentClasses);
|
||||
|
||||
if (editorClasses.paragraph) {
|
||||
editorElement.classList.add(editorClasses.paragraph);
|
||||
}
|
||||
}
|
||||
}, [editor, editorClasses.paragraph]);
|
||||
const handleCloseIndentModal: () => void = useCallback((): void => {
|
||||
setCookie('indent_notice_seen', 'true', 365);
|
||||
setShowIndentModal(false);
|
||||
}, []);
|
||||
|
||||
useEffect((): void => {
|
||||
updateEditorClasses(editorSettings);
|
||||
}, [editorSettings, updateEditorClasses]);
|
||||
|
||||
|
||||
useEffect((): () => void => {
|
||||
function startTimer(): void {
|
||||
if (timerRef.current === null) {
|
||||
@@ -437,7 +430,7 @@ export default function TextEditor() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 w-full h-full bg-tertiary">
|
||||
<div className={`flex flex-col flex-1 w-full h-full bg-tertiary ${indentDisabled ? 'no-text-indent' : ''}`}>
|
||||
<div
|
||||
className={`flex justify-between items-center gap-3 rounded-xl mx-1 mb-1 px-4 py-2 bg-darkest-background transition-opacity duration-300 ${editorSettings.focusMode ? 'opacity-70 hover:opacity-100' : ''}`}>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
@@ -481,6 +474,15 @@ export default function TextEditor() {
|
||||
tooltip={t("textEditor.draftCompanion")}
|
||||
/>
|
||||
)}
|
||||
{indentDisabled && (
|
||||
<IconButton
|
||||
icon={Info}
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
onClick={(): void => setShowIndentModal(true)}
|
||||
tooltip={t("textEditor.indentDisabled")}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon={Save}
|
||||
variant="ghost"
|
||||
@@ -520,6 +522,23 @@ export default function TextEditor() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showIndentModal && (
|
||||
<Modal
|
||||
title={t("textEditor.indentDisabledTitle")}
|
||||
icon={Info}
|
||||
size="sm"
|
||||
onClose={handleCloseIndentModal}
|
||||
footer={
|
||||
<Button variant="primary" onClick={handleCloseIndentModal}>
|
||||
{t("textEditor.indentDisabledUnderstood")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<p className="text-text-secondary leading-relaxed">
|
||||
{t("textEditor.indentDisabledDescription")}
|
||||
</p>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import React, {ChangeEvent, useCallback, useContext, useEffect, useMemo} from 'react';
|
||||
import {Baseline, CaseSensitive, Eye, Indent, Palette, Type} from 'lucide-react';
|
||||
import {Baseline, CaseSensitive, Eye, Palette, Type} from 'lucide-react';
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
import SelectBox from "@/components/form/SelectBox";
|
||||
import Button from "@/components/ui/Button";
|
||||
@@ -13,7 +13,6 @@ interface UserEditorSettingsProps {
|
||||
|
||||
export interface EditorDisplaySettings {
|
||||
zoomLevel: number;
|
||||
indent: number;
|
||||
lineHeight: number;
|
||||
theme: 'clair' | 'sombre' | 'sépia';
|
||||
fontFamily: 'lora' | 'serif' | 'sans-serif' | 'monospace';
|
||||
@@ -31,7 +30,6 @@ function isValidFontFamily(value: string): value is EditorDisplaySettings['fontF
|
||||
|
||||
const defaultSettings: EditorDisplaySettings = {
|
||||
zoomLevel: 3,
|
||||
indent: 30,
|
||||
lineHeight: 1.5,
|
||||
theme: 'sombre',
|
||||
fontFamily: 'lora',
|
||||
@@ -130,29 +128,6 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 mb-2 text-text-primary">
|
||||
<Indent className="text-muted w-5 h-5" strokeWidth={1.75}/>
|
||||
{t("userEditorSettings.indent")}
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={50}
|
||||
step={5}
|
||||
value={settings.indent}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => handleSettingChange('indent', Number(e.target.value))}
|
||||
className="w-full accent-primary"
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-muted">
|
||||
<span>{t("userEditorSettings.indentNone")}</span>
|
||||
<span className="text-text-primary font-medium">{settings.indent}px</span>
|
||||
<span>{t("userEditorSettings.indentMax")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 mb-2 text-text-primary">
|
||||
<Baseline className="text-muted w-5 h-5" strokeWidth={1.75}/>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
'use client';
|
||||
import React, {ChangeEvent, Dispatch, SetStateAction, useContext, useState} from "react";
|
||||
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import {apiPost} from '@/lib/api/client';
|
||||
import {createSeries} from '@/lib/tauri';
|
||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import {Book, Check, Layers, Pencil} from 'lucide-react';
|
||||
import InputField from "@/components/form/InputField";
|
||||
@@ -24,6 +27,7 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
const {
|
||||
serverSyncedBooks,
|
||||
setServerSyncedBooks
|
||||
@@ -60,16 +64,10 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
|
||||
|
||||
setIsAddingSeries(true);
|
||||
try {
|
||||
const response: string = await apiPost<string>(
|
||||
'series/add',
|
||||
{
|
||||
name: name,
|
||||
description: description || null,
|
||||
bookIds: selectedBookIds,
|
||||
},
|
||||
token,
|
||||
lang
|
||||
);
|
||||
const useLocal: boolean = isDesktop && isCurrentlyOffline();
|
||||
const response: string = useLocal
|
||||
? await createSeries({name, description: description || null, bookIds: selectedBookIds})
|
||||
: await apiPost<string>('series/add', {name, description: description || null, bookIds: selectedBookIds}, token, lang);
|
||||
if (!response) {
|
||||
errorMessage(t('addNewSeriesForm.error.addingSeries'));
|
||||
setIsAddingSeries(false);
|
||||
|
||||
@@ -6,7 +6,10 @@ import AlertBox from "@/components/ui/AlertBox";
|
||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import {apiDelete} from '@/lib/api/client';
|
||||
import {deleteSeries} from '@/lib/tauri';
|
||||
|
||||
interface SeriesSettingOption {
|
||||
id: string;
|
||||
@@ -32,18 +35,18 @@ export default function SeriesSettingSidebar(
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
const userToken: string = session?.accessToken ? session?.accessToken : '';
|
||||
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
|
||||
|
||||
async function handleDeleteSeries(): Promise<void> {
|
||||
try {
|
||||
const success: boolean = await apiDelete<boolean>(
|
||||
'series/delete',
|
||||
{seriesId: seriesId},
|
||||
userToken,
|
||||
lang
|
||||
);
|
||||
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);
|
||||
if (success) {
|
||||
successMessage(t('seriesSetting.deleteSuccess'));
|
||||
onClose();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
'use client'
|
||||
import {ChangeEvent, forwardRef, useContext, useEffect, useImperativeHandle, useState} from "react";
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import {apiGet, apiPut} from '@/lib/api/client';
|
||||
import {getSeriesDetail, updateSeries} from '@/lib/tauri';
|
||||
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import TextInput from "@/components/form/TextInput";
|
||||
@@ -20,6 +23,8 @@ function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () =
|
||||
const {seriesId}: SeriesContextProps = useContext<SeriesContextProps>(SeriesContext);
|
||||
const userToken: string = session?.accessToken ? session?.accessToken : '';
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
const useLocal: boolean = isDesktop && isCurrentlyOffline();
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [name, setName] = useState<string>('');
|
||||
@@ -34,12 +39,9 @@ function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () =
|
||||
async function loadSeriesData(): Promise<void> {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response: SeriesDetailResponse = await apiGet<SeriesDetailResponse>(
|
||||
'series/detail',
|
||||
userToken,
|
||||
lang,
|
||||
{seriesid: seriesId}
|
||||
);
|
||||
const response: SeriesDetailResponse = useLocal
|
||||
? await getSeriesDetail(seriesId) as SeriesDetailResponse
|
||||
: await apiGet<SeriesDetailResponse>('series/detail', userToken, lang, {seriesid: seriesId});
|
||||
if (response) {
|
||||
setName(response.name);
|
||||
setDescription(response.description || '');
|
||||
@@ -67,11 +69,9 @@ function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () =
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response: SeriesUpdateResponse = await apiPut<SeriesUpdateResponse>('series/update', {
|
||||
seriesId: seriesId,
|
||||
name: name,
|
||||
description: description
|
||||
}, userToken, lang);
|
||||
const response: SeriesUpdateResponse = useLocal
|
||||
? {success: await updateSeries({seriesId, name, description})} as SeriesUpdateResponse
|
||||
: await apiPut<SeriesUpdateResponse>('series/update', {seriesId, name, description}, userToken, lang);
|
||||
if (!response.success) {
|
||||
errorMessage(t('seriesBasicInformation.error.update'));
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
'use client'
|
||||
import {forwardRef, useContext, useEffect, useImperativeHandle, useState} from "react";
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import {apiDelete, apiGet, apiPost, apiPut} from '@/lib/api/client';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
@@ -30,6 +33,8 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
const userToken: string = session?.accessToken ? session?.accessToken : '';
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline} = useContext(OfflineContext);
|
||||
const useLocal: boolean = isDesktop && isCurrentlyOffline();
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [seriesBooks, setSeriesBooks] = useState<SeriesBookProps[]>([]);
|
||||
@@ -53,12 +58,9 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
async function loadSeriesBooks(): Promise<void> {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response: SeriesBookProps[] = await apiGet<SeriesBookProps[]>(
|
||||
'series/book/list',
|
||||
userToken,
|
||||
lang,
|
||||
{seriesid: seriesId}
|
||||
);
|
||||
const response: SeriesBookProps[] = useLocal
|
||||
? await tauri.getSeriesBooks(seriesId) as SeriesBookProps[]
|
||||
: await apiGet<SeriesBookProps[]>('series/book/list', userToken, lang, {seriesid: seriesId});
|
||||
if (response) {
|
||||
setSeriesBooks(response);
|
||||
}
|
||||
@@ -90,15 +92,9 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
}
|
||||
|
||||
try {
|
||||
const response: boolean = await apiPost<boolean>(
|
||||
'series/book/add',
|
||||
{
|
||||
seriesId: seriesId,
|
||||
bookId: selectedBookToAdd
|
||||
},
|
||||
userToken,
|
||||
lang
|
||||
);
|
||||
const response: boolean = useLocal
|
||||
? await tauri.addBookToSeries(seriesId, selectedBookToAdd)
|
||||
: await apiPost<boolean>('series/book/add', {seriesId: seriesId, bookId: selectedBookToAdd}, userToken, lang);
|
||||
|
||||
if (response) {
|
||||
const addedBook: SyncedBook | undefined = serverSyncedBooks.find(
|
||||
@@ -134,15 +130,10 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
|
||||
async function handleRemoveBook(bookId: string): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await apiDelete<boolean>(
|
||||
'series/book/remove',
|
||||
{
|
||||
seriesId: seriesId,
|
||||
bookId: bookId
|
||||
},
|
||||
userToken,
|
||||
lang
|
||||
);
|
||||
const deletedAt: number = Math.floor(Date.now() / 1000);
|
||||
const response: boolean = useLocal
|
||||
? await tauri.removeBookFromSeries(seriesId, bookId, deletedAt)
|
||||
: await apiDelete<boolean>('series/book/remove', {seriesId: seriesId, bookId: bookId}, userToken, lang);
|
||||
|
||||
if (response) {
|
||||
const updatedBooks: SeriesBookProps[] = seriesBooks
|
||||
@@ -187,18 +178,10 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
}));
|
||||
|
||||
try {
|
||||
const response: boolean = await apiPut<boolean>(
|
||||
'series/book/reorder',
|
||||
{
|
||||
seriesId: seriesId,
|
||||
booksOrder: updatedBooks.map((book: SeriesBookProps) => ({
|
||||
bookId: book.bookId,
|
||||
order: book.order
|
||||
}))
|
||||
},
|
||||
userToken,
|
||||
lang
|
||||
);
|
||||
const bookIds: string[] = updatedBooks.map((book: SeriesBookProps) => book.bookId);
|
||||
const response: boolean = useLocal
|
||||
? await tauri.reorderSeriesBooks(seriesId, bookIds)
|
||||
: await apiPut<boolean>('series/book/reorder', {seriesId: seriesId, booksOrder: updatedBooks.map((book: SeriesBookProps) => ({bookId: book.bookId, order: book.order}))}, userToken, lang);
|
||||
|
||||
if (response) {
|
||||
setSeriesBooks(updatedBooks);
|
||||
|
||||
@@ -339,17 +339,18 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
}
|
||||
}
|
||||
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.push({
|
||||
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: [],
|
||||
}],
|
||||
};
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
setNewElementNames(function (prev: { [key: string]: string }): { [key: string]: string } {
|
||||
return {...prev, [sectionId]: ''};
|
||||
@@ -404,13 +405,23 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
}
|
||||
}
|
||||
setSections(function (prev: LocationProps[]): LocationProps[] {
|
||||
const updated: LocationProps[] = [...prev];
|
||||
updated[sectionIndex].elements[elementIndex].subElements.push({
|
||||
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: '',
|
||||
}],
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
setNewSubElementNames(function (prev: { [key: string]: string }): { [key: string]: string } {
|
||||
return {...prev, [elementIndex]: ''};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useContext} from 'react';
|
||||
import {apiGet, apiPost} from '@/lib/api/client';
|
||||
import {apiGet, apiPatch, apiPost} from '@/lib/api/client';
|
||||
import {SessionContext} from '@/context/SessionContext';
|
||||
import {LangContext} from '@/context/LangContext';
|
||||
import {AlertContext} from '@/context/AlertContext';
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
},
|
||||
"chapters": {
|
||||
"title": "Chapters",
|
||||
"detected": "{count} chapter(s) detected",
|
||||
"detected": "{{count}} chapter(s) detected",
|
||||
"selectAll": "Select all",
|
||||
"deselectAll": "Deselect all",
|
||||
"words": "words"
|
||||
@@ -133,7 +133,7 @@
|
||||
"errorChapterUpdate": "An error occurred while updating the chapter.",
|
||||
"errorChapterDelete": "An error occurred while deleting the chapter.",
|
||||
"errorChapterNameRequired": "Chapter name is required.",
|
||||
"errorChapterSubmit": "Error while trying to submit {chapterName} chapter.",
|
||||
"errorChapterSubmit": "Error while trying to submit {{chapterName}} chapter.",
|
||||
"errorFetchChapter": "Error while fetching chapter.",
|
||||
"errorFetchChapters": "Error while fetching chapters.",
|
||||
"successUpdate": "Chapter updated successfully.",
|
||||
@@ -226,8 +226,8 @@
|
||||
"inputPlaceholder": "Enter a word...",
|
||||
"enterWordError": "Please enter a word to search.",
|
||||
"loading": "Searching...",
|
||||
"resultSynonyms": "Synonyms for \"{word}\"",
|
||||
"resultAntonyms": "Antonyms for \"{word}\"",
|
||||
"resultSynonyms": "Synonyms for \"{{word}}\"",
|
||||
"resultAntonyms": "Antonyms for \"{{word}}\"",
|
||||
"emptySynonymsTitle": "Synonym Search",
|
||||
"emptyAntonymsTitle": "Antonym Search",
|
||||
"emptySynonymsDescription": "Enter a word to find synonyms suitable for different writing contexts.",
|
||||
@@ -305,7 +305,7 @@
|
||||
"search": "Search for a world...",
|
||||
"newWorld": "New world",
|
||||
"deleteTitle": "Delete world",
|
||||
"deleteMessage": "You are about to permanently delete the world \"{name}\".",
|
||||
"deleteMessage": "You are about to permanently delete the world \"{{name}}\".",
|
||||
"worldName": "World name",
|
||||
"worldNamePlaceholder": "Enter the world name",
|
||||
"worldHistory": "World history",
|
||||
@@ -338,7 +338,7 @@
|
||||
"noSectionDescription": "Create your first section to organize your story's locations.",
|
||||
"newSection": "New section",
|
||||
"deleteTitle": "Delete section",
|
||||
"deleteMessage": "You are about to permanently delete the section \"{name}\".",
|
||||
"deleteMessage": "You are about to permanently delete the section \"{{name}}\".",
|
||||
"elementName": "Element name",
|
||||
"elementNamePlaceholder": "Element name",
|
||||
"elementDescriptionPlaceholder": "Element description",
|
||||
@@ -346,11 +346,12 @@
|
||||
"subElementNamePlaceholder": "Sub-element name",
|
||||
"subElementDescriptionPlaceholder": "Sub-element description",
|
||||
"newSubElementPlaceholder": "New sub-element",
|
||||
"noElementAvailable": "No element available. Add one below!",
|
||||
"noElementAvailable": "No element available.",
|
||||
"editToAdd": "Click the edit icon to add elements.",
|
||||
"newElementPlaceholder": "New element",
|
||||
"noSectionAvailable": "No section available.",
|
||||
"createSectionLabel": "Create section",
|
||||
"elementsCount": "{count} elements",
|
||||
"elementsCount": "{{count}} elements",
|
||||
"element": "Element",
|
||||
"addElement": "Add an element",
|
||||
"addSubElement": "Add a sub-element",
|
||||
@@ -438,7 +439,7 @@
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"deleteTitle": "Delete spell",
|
||||
"deleteMessage": "You are about to permanently delete the spell \"{name}\".",
|
||||
"deleteMessage": "You are about to permanently delete the spell \"{{name}}\".",
|
||||
"basicInfo": "Basic information",
|
||||
"name": "Spell name",
|
||||
"namePlaceholder": "Enter spell name",
|
||||
@@ -448,7 +449,7 @@
|
||||
"appearancePlaceholder": "Describe the visual appearance of the spell",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add a tag...",
|
||||
"createTag": "Create \"{name}\"",
|
||||
"createTag": "Create \"{{name}}\"",
|
||||
"powerLevel": "Power level",
|
||||
"components": "Components",
|
||||
"componentsPlaceholder": "Ingredients, gestures, incantations required...",
|
||||
@@ -487,7 +488,7 @@
|
||||
"newCharacter": "New character",
|
||||
"exportToSeries": "Export to series",
|
||||
"deleteTitle": "Delete character",
|
||||
"deleteMessage": "You are about to permanently delete the character \"{name}\".",
|
||||
"deleteMessage": "You are about to permanently delete the character \"{{name}}\".",
|
||||
"basicInfo": "Basic information",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "Enter a name",
|
||||
@@ -545,7 +546,7 @@
|
||||
"noCharactersDescription": "Add your first character to get started."
|
||||
},
|
||||
"characterSectionElement": {
|
||||
"newItem": "New {item}"
|
||||
"newItem": "New {{item}}"
|
||||
},
|
||||
"aboutEditors": {
|
||||
"title": "About Scribe",
|
||||
@@ -572,7 +573,8 @@
|
||||
"bookGoals": "Book goals",
|
||||
"save": "Save",
|
||||
"notAvailable": "Option not available",
|
||||
"unknownError": "An unknown error occurred."
|
||||
"unknownError": "An unknown error occurred.",
|
||||
"quillsenseOffline": "QuillSense is not available offline."
|
||||
},
|
||||
"noBookHome": {
|
||||
"title": "Your work is waiting for its first words",
|
||||
@@ -583,14 +585,18 @@
|
||||
"preferences": "Preferences",
|
||||
"ghostWriter": "Ghost Writer",
|
||||
"draftCompanion": "Draft Companion",
|
||||
"save": "Save"
|
||||
"save": "Save",
|
||||
"indentDisabled": "Indentation disabled",
|
||||
"indentDisabledTitle": "Paragraph indentation disabled",
|
||||
"indentDisabledDescription": "First-line paragraph indentation is disabled due to a display bug in Apple's WebKit engine. A fix is being integrated and will be available in an upcoming Safari update. Indentation will be automatically re-enabled once the fix is available on your system.",
|
||||
"indentDisabledUnderstood": "Got it"
|
||||
},
|
||||
"draftCompanion": {
|
||||
"noPreviousVersion": "No previous version of this chapter",
|
||||
"errorFetchDraft": "Error while fetching draft content.",
|
||||
"unknownError": "An unknown error has occurred",
|
||||
"errorRefineText": "Error refining text",
|
||||
"errorRefineDraft": "Error while refining draft: {message}",
|
||||
"errorRefineDraft": "Error while refining draft: {{message}}",
|
||||
"unknownErrorRefineDraft": "An unknown error occurred while refining the draft",
|
||||
"successInsert": "Correction successfully inserted",
|
||||
"selectVersion": "Select a version",
|
||||
@@ -882,10 +888,10 @@
|
||||
"errorDelete": "Error deleting the element.",
|
||||
"errorAdd": "Error adding the element.",
|
||||
"errorUnknown": "An unexpected error occurred.",
|
||||
"emptyField": "The {section} field is empty.",
|
||||
"namePlaceholder": "Name of {section}",
|
||||
"descriptionPlaceholder": "Description of {section}",
|
||||
"newPlaceholder": "New {section}"
|
||||
"emptyField": "The {{section}} field is empty.",
|
||||
"namePlaceholder": "Name of {{section}}",
|
||||
"descriptionPlaceholder": "Description of {{section}}",
|
||||
"newPlaceholder": "New {{section}}"
|
||||
},
|
||||
"bookTypes": {
|
||||
"short": "Short Story",
|
||||
@@ -1093,7 +1099,7 @@
|
||||
"homePage": {
|
||||
"loading": "Loading...",
|
||||
"guide": {
|
||||
"welcome": "Welcome {name}",
|
||||
"welcome": "Welcome {{name}}",
|
||||
"step0": {
|
||||
"description1": "ERitors is AI-powered software (Anthropic and OpenAI APIs) integrated under the name QuillSense. Its goal is to provide you with all the necessary tools to create a complete work from A to Z.",
|
||||
"description2": "This guide will help you get started. You can skip it if you wish."
|
||||
@@ -1195,7 +1201,7 @@
|
||||
}
|
||||
},
|
||||
"syncField": {
|
||||
"uploadSuccess": "{count} element(s) updated successfully.",
|
||||
"uploadSuccess": "{{count}} element(s) updated successfully.",
|
||||
"uploadTooltip": "Push to series",
|
||||
"downloadTooltip": "Pull from series"
|
||||
},
|
||||
@@ -1210,7 +1216,7 @@
|
||||
"exporting": "Exporting...",
|
||||
"noBookSelected": "No book selected.",
|
||||
"noChaptersSelected": "Please select at least one chapter.",
|
||||
"downloadSuccess": "Your {format} file has been downloaded successfully.",
|
||||
"downloadSuccess": "Your {{format}} file has been downloaded successfully.",
|
||||
"downloadError": "Download failed.",
|
||||
"serverError": "Server error during export.",
|
||||
"unknownError": "An unknown error occurred."
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
},
|
||||
"chapters": {
|
||||
"title": "Chapitres",
|
||||
"detected": "{count} chapitre(s) détecté(s)",
|
||||
"detected": "{{count}} chapitre(s) détecté(s)",
|
||||
"selectAll": "Tout sélectionner",
|
||||
"deselectAll": "Tout désélectionner",
|
||||
"words": "mots"
|
||||
@@ -133,7 +133,7 @@
|
||||
"errorChapterUpdate": "Une erreur est survenue lors de la mise à jour du chapitre.",
|
||||
"errorChapterDelete": "Une erreur est survenue lors de la suppression du chapitre.",
|
||||
"errorChapterNameRequired": "Le nom du chapitre est requis.",
|
||||
"errorChapterSubmit": "Erreur lors de la soumission du chapitre {chapterName}.",
|
||||
"errorChapterSubmit": "Erreur lors de la soumission du chapitre {{chapterName}}.",
|
||||
"errorFetchChapter": "Erreur lors de la récupération du chapitre.",
|
||||
"errorFetchChapters": "Erreur lors de la récupération des chapitres.",
|
||||
"successUpdate": "Chapitre mis à jour avec succès.",
|
||||
@@ -226,8 +226,8 @@
|
||||
"inputPlaceholder": "Entrez un mot...",
|
||||
"enterWordError": "Veuillez entrer un mot à rechercher.",
|
||||
"loading": "Recherche en cours...",
|
||||
"resultSynonyms": "Synonymes de \"{word}\"",
|
||||
"resultAntonyms": "Antonymes de \"{word}\"",
|
||||
"resultSynonyms": "Synonymes de \"{{word}}\"",
|
||||
"resultAntonyms": "Antonymes de \"{{word}}\"",
|
||||
"emptySynonymsTitle": "Recherche de synonymes",
|
||||
"emptyAntonymsTitle": "Recherche d'antonymes",
|
||||
"emptySynonymsDescription": "Entrez un mot pour trouver des synonymes adaptés à différents contextes d'écriture.",
|
||||
@@ -305,7 +305,7 @@
|
||||
"search": "Rechercher un monde...",
|
||||
"newWorld": "Nouveau monde",
|
||||
"deleteTitle": "Supprimer le monde",
|
||||
"deleteMessage": "Vous êtes sur le point de supprimer le monde « {name} » définitivement.",
|
||||
"deleteMessage": "Vous êtes sur le point de supprimer le monde « {{name}} » définitivement.",
|
||||
"worldName": "Nom du monde",
|
||||
"worldNamePlaceholder": "Entrez le nom du monde",
|
||||
"worldHistory": "Histoire du monde",
|
||||
@@ -338,7 +338,7 @@
|
||||
"noSectionDescription": "Créez votre première section pour organiser les lieux de votre histoire.",
|
||||
"newSection": "Nouvelle section",
|
||||
"deleteTitle": "Supprimer la section",
|
||||
"deleteMessage": "Vous êtes sur le point de supprimer la section « {name} » définitivement.",
|
||||
"deleteMessage": "Vous êtes sur le point de supprimer la section « {{name}} » définitivement.",
|
||||
"elementName": "Nom de l'élément",
|
||||
"elementNamePlaceholder": "Nom de l'élément",
|
||||
"elementDescriptionPlaceholder": "Description de l'élément",
|
||||
@@ -346,11 +346,12 @@
|
||||
"subElementNamePlaceholder": "Nom du sous-élément",
|
||||
"subElementDescriptionPlaceholder": "Description du sous-élément",
|
||||
"newSubElementPlaceholder": "Nouveau sous-élément",
|
||||
"noElementAvailable": "Aucun élément disponible. Ajoutez-en un ci-dessous!",
|
||||
"noElementAvailable": "Aucun élément disponible.",
|
||||
"editToAdd": "Cliquez sur l'icône de modification pour ajouter des éléments.",
|
||||
"newElementPlaceholder": "Nouvel élément",
|
||||
"noSectionAvailable": "Aucune section disponible.",
|
||||
"createSectionLabel": "Créer une section",
|
||||
"elementsCount": "{count} éléments",
|
||||
"elementsCount": "{{count}} éléments",
|
||||
"element": "Élément",
|
||||
"addElement": "Ajouter un élément",
|
||||
"addSubElement": "Ajouter un sous-élément",
|
||||
@@ -438,7 +439,7 @@
|
||||
"save": "Enregistrer",
|
||||
"delete": "Supprimer",
|
||||
"deleteTitle": "Supprimer le sort",
|
||||
"deleteMessage": "Vous êtes sur le point de supprimer le sort « {name} » définitivement.",
|
||||
"deleteMessage": "Vous êtes sur le point de supprimer le sort « {{name}} » définitivement.",
|
||||
"basicInfo": "Informations de base",
|
||||
"name": "Nom du sort",
|
||||
"namePlaceholder": "Entrez le nom du sort",
|
||||
@@ -448,7 +449,7 @@
|
||||
"appearancePlaceholder": "Décrivez l'apparence visuelle du sort",
|
||||
"tags": "Tags",
|
||||
"addTag": "Ajouter un tag...",
|
||||
"createTag": "Créer \"{name}\"",
|
||||
"createTag": "Créer \"{{name}}\"",
|
||||
"powerLevel": "Niveau de puissance",
|
||||
"components": "Composantes",
|
||||
"componentsPlaceholder": "Ingrédients, gestes, incantations nécessaires...",
|
||||
@@ -487,7 +488,7 @@
|
||||
"newCharacter": "Nouveau personnage",
|
||||
"exportToSeries": "Exporter vers la série",
|
||||
"deleteTitle": "Supprimer le personnage",
|
||||
"deleteMessage": "Vous êtes sur le point de supprimer le personnage « {name} » définitivement.",
|
||||
"deleteMessage": "Vous êtes sur le point de supprimer le personnage « {{name}} » définitivement.",
|
||||
"basicInfo": "Informations de base",
|
||||
"name": "Nom",
|
||||
"namePlaceholder": "Entrer un nom",
|
||||
@@ -545,7 +546,7 @@
|
||||
"noCharactersDescription": "Ajoutez votre premier personnage pour commencer."
|
||||
},
|
||||
"characterSectionElement": {
|
||||
"newItem": "Nouveau {item}"
|
||||
"newItem": "Nouveau {{item}}"
|
||||
},
|
||||
"aboutEditors": {
|
||||
"title": "À propos de Scribe",
|
||||
@@ -572,7 +573,8 @@
|
||||
"bookGoals": "Objectifs du livre",
|
||||
"save": "Sauvegarder",
|
||||
"notAvailable": "Option non disponible",
|
||||
"unknownError": "Une erreur inconnue est survenue."
|
||||
"unknownError": "Une erreur inconnue est survenue.",
|
||||
"quillsenseOffline": "QuillSense n'est pas disponible hors ligne."
|
||||
},
|
||||
"noBookHome": {
|
||||
"title": "Votre œuvre attend ses premiers mots",
|
||||
@@ -583,14 +585,18 @@
|
||||
"preferences": "Préférences",
|
||||
"ghostWriter": "Écrivain Fantôme",
|
||||
"draftCompanion": "Draft Companion",
|
||||
"save": "Enregistrer"
|
||||
"save": "Enregistrer",
|
||||
"indentDisabled": "Indentation désactivée",
|
||||
"indentDisabledTitle": "Indentation de paragraphe désactivée",
|
||||
"indentDisabledDescription": "L'indentation de première ligne des paragraphes est désactivée en raison d'un bug d'affichage dans le moteur WebKit d'Apple. Un correctif est en cours d'intégration et sera disponible dans une prochaine mise à jour de Safari. L'indentation sera automatiquement réactivée dès que le correctif sera disponible sur votre système.",
|
||||
"indentDisabledUnderstood": "Compris"
|
||||
},
|
||||
"draftCompanion": {
|
||||
"noPreviousVersion": "Aucune version antérieure de ce chapitre",
|
||||
"errorFetchDraft": "Erreur lors de la récupération du contenu du brouillon.",
|
||||
"unknownError": "Une erreur inconnue s'est produite",
|
||||
"errorRefineText": "Erreur lors de la correction du texte",
|
||||
"errorRefineDraft": "Erreur lors de la correction du brouillon : {message}",
|
||||
"errorRefineDraft": "Erreur lors de la correction du brouillon : {{message}}",
|
||||
"unknownErrorRefineDraft": "Une erreur inconnue s'est produite lors de la correction du brouillon",
|
||||
"successInsert": "Correction insérée avec succès",
|
||||
"selectVersion": "Sélectionner une version",
|
||||
@@ -881,10 +887,10 @@
|
||||
"errorDelete": "Erreur lors de la suppression de l'élément.",
|
||||
"errorAdd": "Erreur lors de l'ajout de l'élément.",
|
||||
"errorUnknown": "Une erreur inattendue s'est produite.",
|
||||
"emptyField": "Le champ {section} est vide.",
|
||||
"namePlaceholder": "Nom {section}",
|
||||
"descriptionPlaceholder": "Description de {section}",
|
||||
"newPlaceholder": "Nouveau {section}"
|
||||
"emptyField": "Le champ {{section}} est vide.",
|
||||
"namePlaceholder": "Nom {{section}}",
|
||||
"descriptionPlaceholder": "Description de {{section}}",
|
||||
"newPlaceholder": "Nouveau {{section}}"
|
||||
},
|
||||
"bookTypes": {
|
||||
"short": "Nouvelle",
|
||||
@@ -1092,7 +1098,7 @@
|
||||
"homePage": {
|
||||
"loading": "Chargement en cours...",
|
||||
"guide": {
|
||||
"welcome": "Bienvenue {name}",
|
||||
"welcome": "Bienvenue {{name}}",
|
||||
"step0": {
|
||||
"description1": "ERitors est un logiciel assisté par l'intelligence artificielle (API Anthropic et OpenAI) intégrée sous le nom de QuillSense. Son objectif est de vous fournir tous les outils nécessaires pour créer une œuvre complète de A à Z.",
|
||||
"description2": "Ce guide vous accompagnera dans vos premiers pas. Vous pouvez naturellement le passer si vous le souhaitez."
|
||||
@@ -1194,7 +1200,7 @@
|
||||
}
|
||||
},
|
||||
"syncField": {
|
||||
"uploadSuccess": "{count} élément(s) mis à jour avec succès.",
|
||||
"uploadSuccess": "{{count}} élément(s) mis à jour avec succès.",
|
||||
"uploadTooltip": "Envoyer vers la série",
|
||||
"downloadTooltip": "Récupérer depuis la série"
|
||||
},
|
||||
@@ -1209,7 +1215,7 @@
|
||||
"exporting": "Exportation en cours...",
|
||||
"noBookSelected": "Aucun livre sélectionné.",
|
||||
"noChaptersSelected": "Veuillez sélectionner au moins un chapitre.",
|
||||
"downloadSuccess": "Votre fichier {format} a été téléchargé avec succès.",
|
||||
"downloadSuccess": "Votre fichier {{format}} a été téléchargé avec succès.",
|
||||
"downloadError": "Échec du téléchargement.",
|
||||
"serverError": "Erreur du serveur lors de l'export.",
|
||||
"unknownError": "Une erreur inconnue est survenue."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eritorsscribe",
|
||||
"productName": "ERitors Scribe",
|
||||
"version": "0.4.1",
|
||||
"version": "0.5.0",
|
||||
"type": "module",
|
||||
"main": "dist/electron/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -288,7 +288,7 @@ pub fn fetch_whole_chapter(conn: &Connection, user_id: &str, chapter_id: &str, v
|
||||
.query_row(params![version, chapter_id, user_id], |query_row| {
|
||||
Ok(ChapterContentQueryResult {
|
||||
chapter_id: query_row.get(0)?, title: query_row.get(1)?,
|
||||
chapter_order: query_row.get(2)?, words_count: query_row.get(3)?,
|
||||
chapter_order: query_row.get(2)?, words_count: query_row.get::<_, Option<i64>>(3)?.unwrap_or(0),
|
||||
content: query_row.get::<_, Option<String>>(4)?.unwrap_or_default(),
|
||||
version: query_row.get::<_, Option<i64>>(5)?.unwrap_or(2),
|
||||
})
|
||||
|
||||
@@ -82,11 +82,13 @@ pub fn get_all_locations(conn: &Connection, user_id: &str, book_id: &str, lang:
|
||||
let element_idx: usize = match element_index {
|
||||
Some(idx) => idx,
|
||||
None => {
|
||||
let decrypted_name: String = decrypt_data_with_user_key(record.element_name.as_deref().unwrap_or(""), &user_key)?;
|
||||
let decrypted_description: String = if let Some(ref element_description) = record.element_description {
|
||||
decrypt_data_with_user_key(element_description, &user_key)?
|
||||
} else {
|
||||
String::new()
|
||||
let decrypted_name: String = match record.element_name.as_deref() {
|
||||
Some(name) if !name.is_empty() => decrypt_data_with_user_key(name, &user_key)?,
|
||||
_ => String::new(),
|
||||
};
|
||||
let decrypted_description: String = match record.element_description.as_deref() {
|
||||
Some(description) if !description.is_empty() => decrypt_data_with_user_key(description, &user_key)?,
|
||||
_ => String::new(),
|
||||
};
|
||||
location_array[location_idx].elements.push(Element {
|
||||
id: element_id.clone(),
|
||||
@@ -105,11 +107,13 @@ pub fn get_all_locations(conn: &Connection, user_id: &str, book_id: &str, lang:
|
||||
.any(|sub| sub.id == *sub_element_id);
|
||||
|
||||
if !sub_element_exists {
|
||||
let decrypted_name: String = decrypt_data_with_user_key(record.sub_elem_name.as_deref().unwrap_or(""), &user_key)?;
|
||||
let decrypted_description: String = if let Some(ref sub_elem_description) = record.sub_elem_description {
|
||||
decrypt_data_with_user_key(sub_elem_description, &user_key)?
|
||||
} else {
|
||||
String::new()
|
||||
let decrypted_name: String = match record.sub_elem_name.as_deref() {
|
||||
Some(name) if !name.is_empty() => decrypt_data_with_user_key(name, &user_key)?,
|
||||
_ => String::new(),
|
||||
};
|
||||
let decrypted_description: String = match record.sub_elem_description.as_deref() {
|
||||
Some(description) if !description.is_empty() => decrypt_data_with_user_key(description, &user_key)?,
|
||||
_ => String::new(),
|
||||
};
|
||||
location_array[location_idx].elements[element_idx].sub_elements.push(SubElement {
|
||||
id: sub_element_id.clone(),
|
||||
@@ -325,11 +329,13 @@ pub fn get_location_tags(conn: &Connection, user_id: &str, book_id: &str, lang:
|
||||
if processed_ids.contains(sub_element_id) {
|
||||
continue;
|
||||
}
|
||||
let decrypted_name: String = decrypt_data_with_user_key(record.sub_elem_name.as_deref().unwrap_or(""), &user_key)?;
|
||||
let decrypted_description: String = if let Some(ref sub_elem_description) = record.sub_elem_description {
|
||||
decrypt_data_with_user_key(sub_elem_description, &user_key)?
|
||||
} else {
|
||||
String::new()
|
||||
let decrypted_name: String = match record.sub_elem_name.as_deref() {
|
||||
Some(name) if !name.is_empty() => decrypt_data_with_user_key(name, &user_key)?,
|
||||
_ => String::new(),
|
||||
};
|
||||
let decrypted_description: String = match record.sub_elem_description.as_deref() {
|
||||
Some(description) if !description.is_empty() => decrypt_data_with_user_key(description, &user_key)?,
|
||||
_ => String::new(),
|
||||
};
|
||||
sub_elements.push(SubElement {
|
||||
id: sub_element_id.clone(),
|
||||
@@ -343,10 +349,9 @@ pub fn get_location_tags(conn: &Connection, user_id: &str, book_id: &str, lang:
|
||||
continue;
|
||||
}
|
||||
let decrypted_name: String = decrypt_data_with_user_key(&record.element_name, &user_key)?;
|
||||
let decrypted_description: String = if let Some(ref element_description) = record.element_description {
|
||||
decrypt_data_with_user_key(element_description, &user_key)?
|
||||
} else {
|
||||
String::new()
|
||||
let decrypted_description: String = match record.element_description.as_deref() {
|
||||
Some(description) if !description.is_empty() => decrypt_data_with_user_key(description, &user_key)?,
|
||||
_ => String::new(),
|
||||
};
|
||||
sub_elements.push(SubElement {
|
||||
id: record.element_id.clone(),
|
||||
|
||||
Reference in New Issue
Block a user