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:
natreex
2026-03-24 22:45:10 -04:00
parent a114592ac9
commit cfd08e3261
23 changed files with 410 additions and 409 deletions

View File

@@ -284,62 +284,6 @@ body {
padding-bottom: 10px; 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 */ /* Styles pour l'éditeur principal avec classes dynamiques */
.editor-content .tiptap > p:first-child, .editor-content .tiptap > p:first-child,
.editor-content .tiptap > h1:first-child, .editor-content .tiptap > h1:first-child,
@@ -350,14 +294,17 @@ body {
.editor-content .tiptap p { .editor-content .tiptap p {
color: var(--color-editor-text); color: var(--color-editor-text);
text-indent: 1.25rem;
overflow-wrap: anywhere;
margin-top: 0.7em; margin-top: 0.7em;
margin-bottom: 0.7em; margin-bottom: 0.7em;
} }
.editor-content .tiptap p { .no-text-indent .editor-content .tiptap p {
text-indent: inherit; text-indent: 0;
} }
.editor-content .tiptap p strong { .editor-content .tiptap p strong {
font-weight: 900; font-weight: 900;
color: var(--color-editor-bold); color: var(--color-editor-bold);
@@ -628,11 +575,17 @@ body {
.tiptap-draft p { .tiptap-draft p {
font-family: 'Lora', sans-serif; font-family: 'Lora', sans-serif;
text-indent: 30px; text-indent: 1.25rem;
overflow-wrap: anywhere;
margin-top: 0.7em; margin-top: 0.7em;
margin-bottom: 0.7em; margin-bottom: 0.7em;
} }
.no-text-indent .tiptap-draft p {
text-indent: 0;
}
/* Form input base */ /* Form input base */
.input-base { .input-base {
@apply w-full text-text-primary bg-dark-background px-4 py-2.5 rounded-xl @apply w-full text-text-primary bg-dark-background px-4 py-2.5 rounded-xl

View File

@@ -18,23 +18,21 @@ export default function BookCard({book, onClickCallback, index, syncStatus}: Boo
return ( return (
<div <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"> 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" {book.coverImage ? (
type="button"> <img
{book.coverImage ? ( src={book.coverImage}
<img alt={book.title || t("bookCard.noCoverAlt")}
src={book.coverImage} className="w-full h-full object-cover"
alt={book.title || t("bookCard.noCoverAlt")} />
className="w-full h-full object-cover" ) : (
/> <div className="w-full h-full bg-secondary flex items-center justify-center">
) : ( <span className="text-muted text-5xl font-['ADLaM_Display']">
<div className="w-full h-full bg-secondary flex items-center justify-center"> {book.title.charAt(0).toUpperCase()}
<span className="text-muted text-5xl font-['ADLaM_Display']"> </span>
{book.title.charAt(0).toUpperCase()} </div>
</span> )}
</div>
)}
</button>
{isDesktop && syncStatus && ( {isDesktop && syncStatus && (
<div className="absolute top-2 left-2 cursor-default" onClick={(e: React.MouseEvent): void => e.stopPropagation()}> <div className="absolute top-2 left-2 cursor-default" onClick={(e: React.MouseEvent): void => e.stopPropagation()}>

View File

@@ -10,7 +10,10 @@ import {BookContext, BookContextProps} from "@/context/BookContext";
import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {SessionContext, SessionContextProps} from "@/context/SessionContext";
import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import {LangContext, LangContextProps} from "@/context/LangContext"; import {LangContext, LangContextProps} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {isDesktop} from "@/lib/configs";
import {apiPatch} from "@/lib/api/client"; import {apiPatch} from "@/lib/api/client";
import {updateBookToolSetting} from "@/lib/tauri";
import {SettingRef} from "@/lib/types/settings"; import {SettingRef} from "@/lib/types/settings";
import {BookProps} from "@/lib/types/book"; import {BookProps} from "@/lib/types/book";
@@ -73,6 +76,7 @@ export default function BookSettingOption({setting}: BookSettingOptionProps): Re
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext); const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {isCurrentlyOffline} = useContext(OfflineContext);
const userToken: string = session?.accessToken ?? ''; const userToken: string = session?.accessToken ?? '';
const isToggleable: boolean = setting in toggleableSettings; const isToggleable: boolean = setting in toggleableSettings;
@@ -107,13 +111,20 @@ export default function BookSettingOption({setting}: BookSettingOptionProps): Re
async function handleToggleTool(enabled: boolean): Promise<void> { async function handleToggleTool(enabled: boolean): Promise<void> {
const toolName: ToolName | undefined = toggleableSettings[setting]; 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 { try {
const result: boolean = await apiPatch<boolean>('book/tool-setting', { const result: boolean = useLocal
bookId: book?.bookId, ? await updateBookToolSetting(book.bookId, toolName, enabled)
toolName: toolName, : await apiPatch<boolean>('book/tool-setting', {
enabled: enabled bookId: book.bookId,
}, userToken, lang); toolName: toolName,
enabled: enabled
}, userToken, lang);
if (result && setBook && book) { if (result && setBook && book) {
setToolEnabled(enabled); setToolEnabled(enabled);
if (toolName === 'quillsense') { if (toolName === 'quillsense') {

View File

@@ -10,7 +10,11 @@ import AvatarIcon from '@/components/ui/AvatarIcon';
import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext';
import {LangContext, LangContextProps} from '@/context/LangContext'; 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 {apiGet} from '@/lib/api/client';
import {getCharacterAttributes} from '@/lib/tauri';
type AttributeResponse = { type: string; values: Attribute[] }[]; type AttributeResponse = { type: string; values: Attribute[] }[];
@@ -34,6 +38,8 @@ export default function CharacterEditorDetail({
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext); const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
const {isCurrentlyOffline} = useContext(OfflineContext);
useEffect(function (): void { useEffect(function (): void {
if (character?.id !== null) { if (character?.id !== null) {
@@ -43,12 +49,15 @@ export default function CharacterEditorDetail({
async function getAttributes(): Promise<void> { async function getAttributes(): Promise<void> {
try { try {
const response: AttributeResponse = await apiGet<AttributeResponse>( const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook);
'character/attribute', const response: AttributeResponse = useLocal
session.accessToken, ? await getCharacterAttributes(character.id!) as AttributeResponse
lang, : await apiGet<AttributeResponse>(
{characterId: character?.id} 'character/attribute',
); session.accessToken,
lang,
{characterId: character?.id}
);
if (response && onLoadAttributes) { if (response && onLoadAttributes) {
const attributes: CharacterAttribute = {}; const attributes: CharacterAttribute = {};
response.forEach(function (item: { type: string; values: Attribute[] }): void { response.forEach(function (item: { type: string; values: Attribute[] }): void {

View File

@@ -28,7 +28,11 @@ import {useTranslations} from '@/lib/i18n';
import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext';
import {LangContext, LangContextProps} from '@/context/LangContext'; 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 {apiGet} from '@/lib/api/client';
import {getCharacterAttributes} from '@/lib/tauri';
type AttributeResponse = { type: string; values: Attribute[] }[]; type AttributeResponse = { type: string; values: Attribute[] }[];
@@ -59,6 +63,8 @@ export default function CharacterEditorEdit({
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext); const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
const {isCurrentlyOffline} = useContext(OfflineContext);
const [showAdvanced, setShowAdvanced] = useState<boolean>(false); const [showAdvanced, setShowAdvanced] = useState<boolean>(false);
useEffect(function (): void { useEffect(function (): void {
@@ -69,12 +75,15 @@ export default function CharacterEditorEdit({
async function getAttributes(): Promise<void> { async function getAttributes(): Promise<void> {
try { try {
const response: AttributeResponse = await apiGet<AttributeResponse>( const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook);
'character/attribute', const response: AttributeResponse = useLocal
session.accessToken, ? await getCharacterAttributes(character.id!) as AttributeResponse
lang, : await apiGet<AttributeResponse>(
{characterId: character?.id} 'character/attribute',
); session.accessToken,
lang,
{characterId: character?.id}
);
if (response) { if (response) {
const attributes: CharacterAttribute = {}; const attributes: CharacterAttribute = {};
response.forEach(function (item: { type: string; values: Attribute[] }): void { response.forEach(function (item: { type: string; values: Attribute[] }): void {

View File

@@ -33,7 +33,11 @@ import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext';
import {dynamicBg} from '@/lib/utils/dynamicStyles'; import {dynamicBg} from '@/lib/utils/dynamicStyles';
import {LangContext, LangContextProps} from '@/context/LangContext'; 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 {apiGet} from '@/lib/api/client';
import {getCharacterAttributes} from '@/lib/tauri';
type AttributeResponse = { type: string; values: Attribute[] }[]; type AttributeResponse = { type: string; values: Attribute[] }[];
@@ -52,6 +56,8 @@ export default function CharacterSettingsDetail({
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext); const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
const {isCurrentlyOffline} = useContext(OfflineContext);
const [showAdvanced, setShowAdvanced] = useState<boolean>(false); const [showAdvanced, setShowAdvanced] = useState<boolean>(false);
useEffect(function (): void { useEffect(function (): void {
@@ -62,12 +68,15 @@ export default function CharacterSettingsDetail({
async function getAttributes(): Promise<void> { async function getAttributes(): Promise<void> {
try { try {
const response: AttributeResponse = await apiGet<AttributeResponse>( const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook);
'character/attribute', const response: AttributeResponse = useLocal
session.accessToken, ? await getCharacterAttributes(character.id!) as AttributeResponse
lang, : await apiGet<AttributeResponse>(
{characterId: character?.id} 'character/attribute',
); session.accessToken,
lang,
{characterId: character?.id}
);
if (response && onLoadAttributes) { if (response && onLoadAttributes) {
const attributes: CharacterAttribute = {}; const attributes: CharacterAttribute = {};
response.forEach(function (item: { type: string; values: Attribute[] }): void { response.forEach(function (item: { type: string; values: Attribute[] }): void {

View File

@@ -29,7 +29,11 @@ import {useTranslations} from '@/lib/i18n';
import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext';
import {LangContext, LangContextProps} from '@/context/LangContext'; 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 {apiGet} from '@/lib/api/client';
import {getCharacterAttributes} from '@/lib/tauri';
type AttributeResponse = { type: string; values: Attribute[] }[]; type AttributeResponse = { type: string; values: Attribute[] }[];
@@ -61,6 +65,8 @@ export default function CharacterSettingsEdit({
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext); const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
const {isCurrentlyOffline} = useContext(OfflineContext);
const [showAdvanced, setShowAdvanced] = useState<boolean>(false); const [showAdvanced, setShowAdvanced] = useState<boolean>(false);
useEffect(function (): void { useEffect(function (): void {
@@ -71,12 +77,15 @@ export default function CharacterSettingsEdit({
async function getAttributes(): Promise<void> { async function getAttributes(): Promise<void> {
try { try {
const response: AttributeResponse = await apiGet<AttributeResponse>( const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook);
'character/attribute', const response: AttributeResponse = useLocal
session.accessToken, ? await getCharacterAttributes(character.id!) as AttributeResponse
lang, : await apiGet<AttributeResponse>(
{characterId: character?.id} 'character/attribute',
); session.accessToken,
lang,
{characterId: character?.id}
);
if (response) { if (response) {
const attributes: CharacterAttribute = {}; const attributes: CharacterAttribute = {};
response.forEach(function (item: { type: string; values: Attribute[] }): void { response.forEach(function (item: { type: string; values: Attribute[] }): void {

View File

@@ -4,7 +4,10 @@ import React, {ChangeEvent, forwardRef, useContext, useEffect, useImperativeHand
import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {SessionContext, SessionContextProps} from "@/context/SessionContext";
import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import {BookContext, BookContextProps} from "@/context/BookContext"; 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 {apiDelete, apiGet, apiPatch, apiPost} from '@/lib/api/client';
import * as tauri from '@/lib/tauri';
import InputField from "@/components/form/InputField"; import InputField from "@/components/form/InputField";
import TextInput from '@/components/form/TextInput'; import TextInput from '@/components/form/TextInput';
import TextAreaInput from "@/components/form/TextAreaInput"; 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 {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {successMessage, errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {successMessage, errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext); const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext);
const {isCurrentlyOffline} = useContext(OfflineContext);
const currentEntityId: string = entityId || book?.bookId || ''; const currentEntityId: string = entityId || book?.bookId || '';
const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook);
const isSeriesMode: boolean = entityType === 'series'; const isSeriesMode: boolean = entityType === 'series';
const token: string = session.accessToken; const token: string = session.accessToken;
@@ -87,12 +92,14 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
async function getSeriesLocations(): Promise<void> { async function getSeriesLocations(): Promise<void> {
if (!bookSeriesId) return; if (!bookSeriesId) return;
try { try {
const response: SeriesLocationItem[] = await apiGet<SeriesLocationItem[]>( const response: SeriesLocationItem[] = useLocal
'series/location/list', ? await tauri.getSeriesLocationList(bookSeriesId) as SeriesLocationItem[]
token, : await apiGet<SeriesLocationItem[]>(
lang, 'series/location/list',
{seriesid: bookSeriesId} token,
); lang,
{seriesid: bookSeriesId}
);
if (response) { if (response) {
setSeriesLocations(response); setSeriesLocations(response);
} }
@@ -106,11 +113,13 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
async function handleToggleTool(enabled: boolean): Promise<void> { async function handleToggleTool(enabled: boolean): Promise<void> {
if (isSeriesMode) return; if (isSeriesMode) return;
try { try {
const response: boolean = await apiPatch<boolean>('book/tool-setting', { const response: boolean = useLocal
bookId: currentEntityId, ? await tauri.updateBookToolSetting(currentEntityId, 'locations', enabled)
toolName: 'locations', : await apiPatch<boolean>('book/tool-setting', {
enabled: enabled bookId: currentEntityId,
}, token, lang); toolName: 'locations',
enabled: enabled
}, token, lang);
if (response && setBook && book) { if (response && setBook && book) {
setToolEnabled(enabled); setToolEnabled(enabled);
setBook({ setBook({
@@ -132,12 +141,14 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
async function getAllLocations(): Promise<void> { async function getAllLocations(): Promise<void> {
try { try {
if (isSeriesMode) { if (isSeriesMode) {
const response: SeriesLocationItem[] = await apiGet<SeriesLocationItem[]>( const response: SeriesLocationItem[] = useLocal
'series/location/list', ? await tauri.getSeriesLocationList(currentEntityId) as SeriesLocationItem[]
token, : await apiGet<SeriesLocationItem[]>(
lang, 'series/location/list',
{seriesid: currentEntityId} token,
); lang,
{seriesid: currentEntityId}
);
if (response) { if (response) {
const mappedLocations: LocationProps[] = response.map((loc: SeriesLocationItem): LocationProps => ({ const mappedLocations: LocationProps[] = response.map((loc: SeriesLocationItem): LocationProps => ({
id: loc.id, id: loc.id,
@@ -156,12 +167,14 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
setSections(mappedLocations); setSections(mappedLocations);
} }
} else { } else {
const response: LocationListResponse = await apiGet<LocationListResponse>( const response: LocationListResponse = useLocal
'location/all', ? await tauri.getAllLocations(currentEntityId, true) as LocationListResponse
token, : await apiGet<LocationListResponse>(
lang, 'location/all',
{bookid: currentEntityId} token,
); lang,
{bookid: currentEntityId}
);
if (response) { if (response) {
setSections(response.locations); setSections(response.locations);
setToolEnabled(response.enabled); setToolEnabled(response.enabled);
@@ -194,24 +207,17 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
try { try {
let sectionId: string; let sectionId: string;
if (isSeriesMode) { if (isSeriesMode) {
sectionId = await apiPost<string>( sectionId = useLocal
'series/location/section/add', ? await tauri.addSeriesLocationSection({seriesId: currentEntityId, name: newSectionName})
{ : await apiPost<string>('series/location/section/add', {seriesId: currentEntityId, name: newSectionName}, token, lang);
seriesId: currentEntityId,
name: newSectionName,
},
token,
lang
);
if (!sectionId) { if (!sectionId) {
errorMessage(t('locationComponent.errorUnknownAddSection')); errorMessage(t('locationComponent.errorUnknownAddSection'));
return; return;
} }
} else { } else {
sectionId = await apiPost<string>('location/section/add', { sectionId = useLocal
bookId: currentEntityId, ? await tauri.addLocationSection(newSectionName, currentEntityId)
locationName: newSectionName, : await apiPost<string>('location/section/add', {bookId: currentEntityId, locationName: newSectionName}, token, lang);
}, token, lang);
if (!sectionId) { if (!sectionId) {
errorMessage(t('locationComponent.errorUnknownAddSection')); errorMessage(t('locationComponent.errorUnknownAddSection'));
return; return;
@@ -241,25 +247,17 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
try { try {
let elementId: string; let elementId: string;
if (isSeriesMode) { if (isSeriesMode) {
elementId = await apiPost<string>( elementId = useLocal
'series/location/element/add', ? await tauri.addSeriesLocationElement({locationId: sectionId, name: newElementNames[sectionId]})
{ : await apiPost<string>('series/location/element/add', {locationId: sectionId, name: newElementNames[sectionId]}, token, lang);
locationId: sectionId,
name: newElementNames[sectionId],
},
token,
lang
);
if (!elementId) { if (!elementId) {
errorMessage(t('locationComponent.errorUnknownAddElement')); errorMessage(t('locationComponent.errorUnknownAddElement'));
return; return;
} }
} else { } else {
elementId = await apiPost<string>('location/element/add', { elementId = useLocal
bookId: currentEntityId, ? await tauri.addLocationElement(sectionId, newElementNames[sectionId])
locationId: sectionId, : await apiPost<string>('location/element/add', {bookId: currentEntityId, locationId: sectionId, elementName: newElementNames[sectionId]}, token, lang);
elementName: newElementNames[sectionId],
}, token, lang);
if (!elementId) { if (!elementId) {
errorMessage(t('locationComponent.errorUnknownAddElement')); errorMessage(t('locationComponent.errorUnknownAddElement'));
return; return;
@@ -314,25 +312,19 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
); );
try { try {
let subElementId: string; let subElementId: string;
const parentElementId: string = sections[sectionIndex].elements[elementIndex].id;
if (isSeriesMode) { if (isSeriesMode) {
subElementId = await apiPost<string>( subElementId = useLocal
'series/location/sub-element/add', ? await tauri.addSeriesLocationSubElement({elementId: parentElementId, name: newSubElementNames[elementIndex]})
{ : await apiPost<string>('series/location/sub-element/add', {elementId: parentElementId, name: newSubElementNames[elementIndex]}, token, lang);
elementId: sections[sectionIndex].elements[elementIndex].id,
name: newSubElementNames[elementIndex],
},
token,
lang
);
if (!subElementId) { if (!subElementId) {
errorMessage(t('locationComponent.errorUnknownAddSubElement')); errorMessage(t('locationComponent.errorUnknownAddSubElement'));
return; return;
} }
} else { } else {
subElementId = await apiPost<string>('location/sub-element/add', { subElementId = useLocal
elementId: sections[sectionIndex].elements[elementIndex].id, ? await tauri.addLocationSubElement(parentElementId, newSubElementNames[elementIndex])
subElementName: newSubElementNames[elementIndex], : await apiPost<string>('location/sub-element/add', {elementId: parentElementId, subElementName: newSubElementNames[elementIndex]}, token, lang);
}, token, lang);
if (!subElementId) { if (!subElementId) {
errorMessage(t('locationComponent.errorUnknownAddSubElement')); errorMessage(t('locationComponent.errorUnknownAddSubElement'));
return; return;
@@ -379,15 +371,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
try { try {
const elementId: string | undefined = sections.find((section: LocationProps): boolean => section.id === sectionId) const elementId: string | undefined = sections.find((section: LocationProps): boolean => section.id === sectionId)
?.elements[elementIndex].id; ?.elements[elementIndex].id;
const deletedAt: number = Math.floor(Date.now() / 1000);
let success: boolean; let success: boolean;
if (isSeriesMode) { if (isSeriesMode) {
success = await apiDelete<boolean>('series/location/element/delete', { success = useLocal
elementId: elementId ? await tauri.deleteSeriesLocationElement(elementId!, deletedAt)
}, token, lang); : await apiDelete<boolean>('series/location/element/delete', {elementId: elementId}, token, lang);
} else { } else {
success = await apiDelete<boolean>('location/element/delete', { success = useLocal
elementId: elementId, ? await tauri.deleteLocationElement(elementId!, currentEntityId, deletedAt)
}, token, lang); : await apiDelete<boolean>('location/element/delete', {elementId: elementId}, token, lang);
} }
if (!success) { if (!success) {
errorMessage(t('locationComponent.errorUnknownDeleteElement')); errorMessage(t('locationComponent.errorUnknownDeleteElement'));
@@ -414,15 +407,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
try { try {
const subElementId: string | undefined = sections.find((section: LocationProps): boolean => section.id === sectionId) const subElementId: string | undefined = sections.find((section: LocationProps): boolean => section.id === sectionId)
?.elements[elementIndex].subElements[subElementIndex].id; ?.elements[elementIndex].subElements[subElementIndex].id;
const deletedAt: number = Math.floor(Date.now() / 1000);
let success: boolean; let success: boolean;
if (isSeriesMode) { if (isSeriesMode) {
success = await apiDelete<boolean>('series/location/sub-element/delete', { success = useLocal
subElementId: subElementId ? await tauri.deleteSeriesLocationSubElement(subElementId!, deletedAt)
}, token, lang); : await apiDelete<boolean>('series/location/sub-element/delete', {subElementId: subElementId}, token, lang);
} else { } else {
success = await apiDelete<boolean>('location/sub-element/delete', { success = useLocal
subElementId: subElementId, ? await tauri.deleteLocationSubElement(subElementId!, currentEntityId, deletedAt)
}, token, lang); : await apiDelete<boolean>('location/sub-element/delete', {subElementId: subElementId}, token, lang);
} }
if (!success) { if (!success) {
errorMessage(t('locationComponent.errorUnknownDeleteSubElement')); errorMessage(t('locationComponent.errorUnknownDeleteSubElement'));
@@ -443,15 +437,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
async function handleRemoveSection(sectionId: string): Promise<void> { async function handleRemoveSection(sectionId: string): Promise<void> {
try { try {
const deletedAt: number = Math.floor(Date.now() / 1000);
let success: boolean; let success: boolean;
if (isSeriesMode) { if (isSeriesMode) {
success = await apiDelete<boolean>('series/location/delete', { success = useLocal
locationId: sectionId ? await tauri.deleteSeriesLocation(sectionId, deletedAt)
}, token, lang); : await apiDelete<boolean>('series/location/delete', {locationId: sectionId}, token, lang);
} else { } else {
success = await apiDelete<boolean>('location/delete', { success = useLocal
locationId: sectionId, ? await tauri.deleteLocationSection(sectionId, currentEntityId, deletedAt)
}, token, lang); : await apiDelete<boolean>('location/delete', {locationId: sectionId}, token, lang);
} }
if (!success) { if (!success) {
errorMessage(t('locationComponent.errorUnknownDeleteSection')); errorMessage(t('locationComponent.errorUnknownDeleteSection'));
@@ -470,9 +465,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
async function handleSave(): Promise<void> { async function handleSave(): Promise<void> {
try { try {
const response: boolean = await apiPost<boolean>(`location/update`, { const response: boolean = useLocal
locations: sections, ? await tauri.updateLocations(sections) as boolean
}, token, lang); : await apiPost<boolean>(`location/update`, {locations: sections}, token, lang);
if (!response) { if (!response) {
errorMessage(t('locationComponent.errorUnknownSave')); errorMessage(t('locationComponent.errorUnknownSave'));
return; return;
@@ -491,17 +486,14 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
if (!bookSeriesId) return; if (!bookSeriesId) return;
try { try {
const seriesLocationId: string = await apiPost<string>('series/location/section/add', { const seriesLocationId: string = useLocal
seriesId: bookSeriesId, ? await tauri.addSeriesLocationSection({seriesId: bookSeriesId, name: section.name})
name: section.name, : await apiPost<string>('series/location/section/add', {seriesId: bookSeriesId, name: section.name}, token, lang);
}, token, lang);
if (seriesLocationId) { if (seriesLocationId) {
const updateResponse: boolean = await apiPost<boolean>('location/section/update', { const updateResponse: boolean = useLocal
sectionId: section.id, ? await tauri.updateLocationSectionWithSeriesLink(section.id, section.name, seriesLocationId)
sectionName: section.name, : await apiPost<boolean>('location/section/update', {sectionId: section.id, sectionName: section.name, seriesLocationId: seriesLocationId}, token, lang);
seriesLocationId: seriesLocationId,
}, token, lang);
if (updateResponse) { if (updateResponse) {
setSections(sections.map((s: LocationProps): LocationProps => setSections(sections.map((s: LocationProps): LocationProps =>
@@ -523,11 +515,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
if (!seriesLocation) return; if (!seriesLocation) return;
try { try {
const sectionId: string = await apiPost<string>('location/section/add', { const sectionId: string = useLocal
bookId: currentEntityId, ? await tauri.addLocationSection(seriesLocation.name, currentEntityId, undefined, seriesLocationId)
locationName: seriesLocation.name, : await apiPost<string>('location/section/add', {bookId: currentEntityId, locationName: seriesLocation.name, seriesLocationId: seriesLocationId}, token, lang);
seriesLocationId: seriesLocationId,
}, token, lang);
if (!sectionId) { if (!sectionId) {
errorMessage(t('locationComponent.importError')); errorMessage(t('locationComponent.importError'));
@@ -537,21 +527,18 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
const importedElements: Element[] = []; const importedElements: Element[] = [];
for (const seriesElement of seriesLocation.elements) { for (const seriesElement of seriesLocation.elements) {
const elementId: string = await apiPost<string>('location/element/add', { const elementId: string = useLocal
bookId: currentEntityId, ? await tauri.addLocationElement(sectionId, seriesElement.name)
locationId: sectionId, : await apiPost<string>('location/element/add', {bookId: currentEntityId, locationId: sectionId, elementName: seriesElement.name}, token, lang);
elementName: seriesElement.name,
}, token, lang);
if (!elementId) continue; if (!elementId) continue;
const importedSubElements: SubElement[] = []; const importedSubElements: SubElement[] = [];
for (const seriesSubElement of seriesElement.subElements) { for (const seriesSubElement of seriesElement.subElements) {
const subElementId: string = await apiPost<string>('location/sub-element/add', { const subElementId: string = useLocal
elementId: elementId, ? await tauri.addLocationSubElement(elementId, seriesSubElement.name)
subElementName: seriesSubElement.name, : await apiPost<string>('location/sub-element/add', {elementId: elementId, subElementName: seriesSubElement.name}, token, lang);
}, token, lang);
if (subElementId) { if (subElementId) {
importedSubElements.push({ importedSubElements.push({

View File

@@ -161,7 +161,16 @@ export default function LocationSettings({
{viewMode === 'detail' && selectedSection && ( {viewMode === 'detail' && selectedSection && (
<div className="p-4"> <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> </div>
)} )}

View File

@@ -29,6 +29,7 @@ export default function LocationSettingsDetail({
className="text-center py-12 text-text-secondary"> className="text-center py-12 text-text-secondary">
<MapPin className="w-8 h-8 mb-3 opacity-50" strokeWidth={1.75}/> <MapPin className="w-8 h-8 mb-3 opacity-50" strokeWidth={1.75}/>
<p>{t("locationComponent.noElementAvailable")}</p> <p>{t("locationComponent.noElementAvailable")}</p>
<p className="text-sm mt-2 text-text-dimmed">{t("locationComponent.editToAdd")}</p>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">

View File

@@ -31,9 +31,14 @@ import {SessionContext, SessionContextProps} from "@/context/SessionContext";
import DraftCompanion from "@/components/editor/DraftCompanion"; import DraftCompanion from "@/components/editor/DraftCompanion";
import GhostWriter from "@/components/ghostwriter/GhostWriter"; import GhostWriter from "@/components/ghostwriter/GhostWriter";
import IconButton from "@/components/ui/IconButton"; import IconButton from "@/components/ui/IconButton";
import Button from "@/components/ui/Button";
import UserEditorSettings, {EditorDisplaySettings} from "@/components/editor/UserEditorSetting"; import UserEditorSettings, {EditorDisplaySettings} from "@/components/editor/UserEditorSetting";
import {useTranslations} from '@/lib/i18n'; import {useTranslations} from '@/lib/i18n';
import {LangContext, LangContextProps} from "@/context/LangContext"; 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 { interface ToolbarButton {
action: () => void; action: () => void;
@@ -48,14 +53,12 @@ interface EditorClasses {
h3: string; h3: string;
container: string; container: string;
theme: string; theme: string;
paragraph: string;
lists: string; lists: string;
listItems: string; listItems: string;
} }
const defaultEditorSettings: EditorDisplaySettings = { const defaultEditorSettings: EditorDisplaySettings = {
zoomLevel: 3, zoomLevel: 3,
indent: 30,
lineHeight: 1.5, lineHeight: 1.5,
theme: 'sombre', theme: 'sombre',
fontFamily: 'lora', fontFamily: 'lora',
@@ -148,6 +151,8 @@ export default function TextEditor() {
const [showUserSettings, setShowUserSettings] = useState<boolean>(false); const [showUserSettings, setShowUserSettings] = useState<boolean>(false);
const [isSaving, setIsSaving] = useState<boolean>(false); const [isSaving, setIsSaving] = useState<boolean>(false);
const [editorSettings, setEditorSettings] = useState<EditorDisplaySettings>(defaultEditorSettings); 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>({ const [editorClasses, setEditorClasses] = useState<EditorClasses>({
base: 'text-lg font-serif leading-normal', base: 'text-lg font-serif leading-normal',
h1: 'text-3xl font-bold', h1: 'text-3xl font-bold',
@@ -155,7 +160,6 @@ export default function TextEditor() {
h3: 'text-xl font-bold', h3: 'text-xl font-bold',
container: 'max-w-3xl', container: 'max-w-3xl',
theme: 'bg-tertiary text-text-primary', theme: 'bg-tertiary text-text-primary',
paragraph: 'indent-6',
lists: 'pl-10', lists: 'pl-10',
listItems: 'text-lg' listItems: 'text-lg'
}); });
@@ -170,14 +174,12 @@ export default function TextEditor() {
const fontFamily: string = fontFamilyClasses[settings.fontFamily] || fontFamilyClasses['lora']; const fontFamily: string = fontFamilyClasses[settings.fontFamily] || fontFamilyClasses['lora'];
const lineHeight: string = lineHeightClasses[lineHeightKey]; const lineHeight: string = lineHeightClasses[lineHeightKey];
const indentClass: string = `indent-${Math.round(settings.indent / 4)}`;
const baseClass: string = `${fontSizeClasses[zoomKey]} ${fontFamily} ${lineHeight}`; const baseClass: string = `${fontSizeClasses[zoomKey]} ${fontFamily} ${lineHeight}`;
const h1Class: string = `${h1SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`; const h1Class: string = `${h1SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`;
const h2Class: string = `${h2SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`; const h2Class: string = `${h2SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`;
const h3Class: string = `${h3SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`; const h3Class: string = `${h3SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`;
const containerClass: string = maxWidthClasses[maxWidthKey]; const containerClass: string = maxWidthClasses[maxWidthKey];
const listsClass: string = `pl-${Math.round((settings.indent + 20) / 4)}`; const listsClass: string = 'pl-12';
let themeClass: string = ''; let themeClass: string = '';
switch (settings.theme) { switch (settings.theme) {
@@ -198,7 +200,6 @@ export default function TextEditor() {
h3: h3Class, h3: h3Class,
container: containerClass, container: containerClass,
theme: themeClass, theme: themeClass,
paragraph: indentClass,
lists: listsClass, lists: listsClass,
listItems: baseClass listItems: baseClass
}); });
@@ -336,24 +337,16 @@ export default function TextEditor() {
setShowGhostWriter(false); setShowGhostWriter(false);
}, []); }, []);
useEffect((): void => { const handleCloseIndentModal: () => void = useCallback((): void => {
if (!editor) return; setCookie('indent_notice_seen', 'true', 365);
setShowIndentModal(false);
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]);
useEffect((): void => { useEffect((): void => {
updateEditorClasses(editorSettings); updateEditorClasses(editorSettings);
}, [editorSettings, updateEditorClasses]); }, [editorSettings, updateEditorClasses]);
useEffect((): () => void => { useEffect((): () => void => {
function startTimer(): void { function startTimer(): void {
if (timerRef.current === null) { if (timerRef.current === null) {
@@ -437,7 +430,7 @@ export default function TextEditor() {
} }
return ( 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 <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' : ''}`}> 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"> <div className="flex flex-wrap gap-1">
@@ -481,6 +474,15 @@ export default function TextEditor() {
tooltip={t("textEditor.draftCompanion")} tooltip={t("textEditor.draftCompanion")}
/> />
)} )}
{indentDisabled && (
<IconButton
icon={Info}
variant="ghost"
shape="square"
onClick={(): void => setShowIndentModal(true)}
tooltip={t("textEditor.indentDisabled")}
/>
)}
<IconButton <IconButton
icon={Save} icon={Save}
variant="ghost" variant="ghost"
@@ -520,6 +522,23 @@ export default function TextEditor() {
</div> </div>
)} )}
</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> </div>
); );
} }

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import React, {ChangeEvent, useCallback, useContext, useEffect, useMemo} from 'react'; 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 {useTranslations} from '@/lib/i18n';
import SelectBox from "@/components/form/SelectBox"; import SelectBox from "@/components/form/SelectBox";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
@@ -13,7 +13,6 @@ interface UserEditorSettingsProps {
export interface EditorDisplaySettings { export interface EditorDisplaySettings {
zoomLevel: number; zoomLevel: number;
indent: number;
lineHeight: number; lineHeight: number;
theme: 'clair' | 'sombre' | 'sépia'; theme: 'clair' | 'sombre' | 'sépia';
fontFamily: 'lora' | 'serif' | 'sans-serif' | 'monospace'; fontFamily: 'lora' | 'serif' | 'sans-serif' | 'monospace';
@@ -31,7 +30,6 @@ function isValidFontFamily(value: string): value is EditorDisplaySettings['fontF
const defaultSettings: EditorDisplaySettings = { const defaultSettings: EditorDisplaySettings = {
zoomLevel: 3, zoomLevel: 3,
indent: 30,
lineHeight: 1.5, lineHeight: 1.5,
theme: 'sombre', theme: 'sombre',
fontFamily: 'lora', fontFamily: 'lora',
@@ -130,29 +128,6 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi
/> />
</div> </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> <div>
<label className="flex items-center gap-2 mb-2 text-text-primary"> <label className="flex items-center gap-2 mb-2 text-text-primary">
<Baseline className="text-muted w-5 h-5" strokeWidth={1.75}/> <Baseline className="text-muted w-5 h-5" strokeWidth={1.75}/>

View File

@@ -1,7 +1,10 @@
'use client'; 'use client';
import React, {ChangeEvent, Dispatch, SetStateAction, useContext, useState} from "react"; import React, {ChangeEvent, Dispatch, SetStateAction, useContext, useState} from "react";
import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {isDesktop} from '@/lib/configs';
import {apiPost} from '@/lib/api/client'; import {apiPost} from '@/lib/api/client';
import {createSeries} from '@/lib/tauri';
import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {SessionContext, SessionContextProps} from "@/context/SessionContext";
import {Book, Check, Layers, Pencil} from 'lucide-react'; import {Book, Check, Layers, Pencil} from 'lucide-react';
import InputField from "@/components/form/InputField"; import InputField from "@/components/form/InputField";
@@ -24,6 +27,7 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext); const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline} = useContext(OfflineContext);
const { const {
serverSyncedBooks, serverSyncedBooks,
setServerSyncedBooks setServerSyncedBooks
@@ -60,16 +64,10 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
setIsAddingSeries(true); setIsAddingSeries(true);
try { try {
const response: string = await apiPost<string>( const useLocal: boolean = isDesktop && isCurrentlyOffline();
'series/add', const response: string = useLocal
{ ? await createSeries({name, description: description || null, bookIds: selectedBookIds})
name: name, : await apiPost<string>('series/add', {name, description: description || null, bookIds: selectedBookIds}, token, lang);
description: description || null,
bookIds: selectedBookIds,
},
token,
lang
);
if (!response) { if (!response) {
errorMessage(t('addNewSeriesForm.error.addingSeries')); errorMessage(t('addNewSeriesForm.error.addingSeries'));
setIsAddingSeries(false); setIsAddingSeries(false);

View File

@@ -6,7 +6,10 @@ import AlertBox from "@/components/ui/AlertBox";
import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {SessionContext, SessionContextProps} from "@/context/SessionContext";
import {LangContext, LangContextProps} from "@/context/LangContext"; import {LangContext, LangContextProps} from "@/context/LangContext";
import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {isDesktop} from '@/lib/configs';
import {apiDelete} from '@/lib/api/client'; import {apiDelete} from '@/lib/api/client';
import {deleteSeries} from '@/lib/tauri';
interface SeriesSettingOption { interface SeriesSettingOption {
id: string; id: string;
@@ -32,18 +35,18 @@ export default function SeriesSettingSidebar(
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext); const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext); const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline} = useContext(OfflineContext);
const userToken: string = session?.accessToken ? session?.accessToken : ''; const userToken: string = session?.accessToken ? session?.accessToken : '';
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
async function handleDeleteSeries(): Promise<void> { async function handleDeleteSeries(): Promise<void> {
try { try {
const success: boolean = await apiDelete<boolean>( const useLocal: boolean = isDesktop && isCurrentlyOffline();
'series/delete', const deletedAt: number = Math.floor(Date.now() / 1000);
{seriesId: seriesId}, const success: boolean = useLocal
userToken, ? await deleteSeries(seriesId, deletedAt)
lang : await apiDelete<boolean>('series/delete', {seriesId: seriesId}, userToken, lang);
);
if (success) { if (success) {
successMessage(t('seriesSetting.deleteSuccess')); successMessage(t('seriesSetting.deleteSuccess'));
onClose(); onClose();

View File

@@ -1,6 +1,9 @@
'use client' 'use client'
import {ChangeEvent, forwardRef, useContext, useEffect, useImperativeHandle, useState} from "react"; 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 {apiGet, apiPut} from '@/lib/api/client';
import {getSeriesDetail, updateSeries} from '@/lib/tauri';
import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {SessionContext, SessionContextProps} from "@/context/SessionContext";
import TextInput from "@/components/form/TextInput"; 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 {seriesId}: SeriesContextProps = useContext<SeriesContextProps>(SeriesContext);
const userToken: string = session?.accessToken ? session?.accessToken : ''; const userToken: string = session?.accessToken ? session?.accessToken : '';
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline} = useContext(OfflineContext);
const useLocal: boolean = isDesktop && isCurrentlyOffline();
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [name, setName] = useState<string>(''); const [name, setName] = useState<string>('');
@@ -34,12 +39,9 @@ function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () =
async function loadSeriesData(): Promise<void> { async function loadSeriesData(): Promise<void> {
setIsLoading(true); setIsLoading(true);
try { try {
const response: SeriesDetailResponse = await apiGet<SeriesDetailResponse>( const response: SeriesDetailResponse = useLocal
'series/detail', ? await getSeriesDetail(seriesId) as SeriesDetailResponse
userToken, : await apiGet<SeriesDetailResponse>('series/detail', userToken, lang, {seriesid: seriesId});
lang,
{seriesid: seriesId}
);
if (response) { if (response) {
setName(response.name); setName(response.name);
setDescription(response.description || ''); setDescription(response.description || '');
@@ -67,11 +69,9 @@ function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () =
return; return;
} }
try { try {
const response: SeriesUpdateResponse = await apiPut<SeriesUpdateResponse>('series/update', { const response: SeriesUpdateResponse = useLocal
seriesId: seriesId, ? {success: await updateSeries({seriesId, name, description})} as SeriesUpdateResponse
name: name, : await apiPut<SeriesUpdateResponse>('series/update', {seriesId, name, description}, userToken, lang);
description: description
}, userToken, lang);
if (!response.success) { if (!response.success) {
errorMessage(t('seriesBasicInformation.error.update')); errorMessage(t('seriesBasicInformation.error.update'));
return; return;

View File

@@ -1,6 +1,9 @@
'use client' 'use client'
import {forwardRef, useContext, useEffect, useImperativeHandle, useState} from "react"; 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 {apiDelete, apiGet, apiPost, apiPut} from '@/lib/api/client';
import * as tauri from '@/lib/tauri';
import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {SessionContext, SessionContextProps} from "@/context/SessionContext";
import {useTranslations} from '@/lib/i18n'; import {useTranslations} from '@/lib/i18n';
@@ -30,6 +33,8 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext); }: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const userToken: string = session?.accessToken ? session?.accessToken : ''; const userToken: string = session?.accessToken ? session?.accessToken : '';
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext); const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline} = useContext(OfflineContext);
const useLocal: boolean = isDesktop && isCurrentlyOffline();
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [seriesBooks, setSeriesBooks] = useState<SeriesBookProps[]>([]); const [seriesBooks, setSeriesBooks] = useState<SeriesBookProps[]>([]);
@@ -53,12 +58,9 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
async function loadSeriesBooks(): Promise<void> { async function loadSeriesBooks(): Promise<void> {
setIsLoading(true); setIsLoading(true);
try { try {
const response: SeriesBookProps[] = await apiGet<SeriesBookProps[]>( const response: SeriesBookProps[] = useLocal
'series/book/list', ? await tauri.getSeriesBooks(seriesId) as SeriesBookProps[]
userToken, : await apiGet<SeriesBookProps[]>('series/book/list', userToken, lang, {seriesid: seriesId});
lang,
{seriesid: seriesId}
);
if (response) { if (response) {
setSeriesBooks(response); setSeriesBooks(response);
} }
@@ -90,15 +92,9 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
} }
try { try {
const response: boolean = await apiPost<boolean>( const response: boolean = useLocal
'series/book/add', ? await tauri.addBookToSeries(seriesId, selectedBookToAdd)
{ : await apiPost<boolean>('series/book/add', {seriesId: seriesId, bookId: selectedBookToAdd}, userToken, lang);
seriesId: seriesId,
bookId: selectedBookToAdd
},
userToken,
lang
);
if (response) { if (response) {
const addedBook: SyncedBook | undefined = serverSyncedBooks.find( 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> { async function handleRemoveBook(bookId: string): Promise<void> {
try { try {
const response: boolean = await apiDelete<boolean>( const deletedAt: number = Math.floor(Date.now() / 1000);
'series/book/remove', const response: boolean = useLocal
{ ? await tauri.removeBookFromSeries(seriesId, bookId, deletedAt)
seriesId: seriesId, : await apiDelete<boolean>('series/book/remove', {seriesId: seriesId, bookId: bookId}, userToken, lang);
bookId: bookId
},
userToken,
lang
);
if (response) { if (response) {
const updatedBooks: SeriesBookProps[] = seriesBooks const updatedBooks: SeriesBookProps[] = seriesBooks
@@ -187,18 +178,10 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
})); }));
try { try {
const response: boolean = await apiPut<boolean>( const bookIds: string[] = updatedBooks.map((book: SeriesBookProps) => book.bookId);
'series/book/reorder', const response: boolean = useLocal
{ ? await tauri.reorderSeriesBooks(seriesId, bookIds)
seriesId: seriesId, : await apiPut<boolean>('series/book/reorder', {seriesId: seriesId, booksOrder: updatedBooks.map((book: SeriesBookProps) => ({bookId: book.bookId, order: book.order}))}, userToken, lang);
booksOrder: updatedBooks.map((book: SeriesBookProps) => ({
bookId: book.bookId,
order: book.order
}))
},
userToken,
lang
);
if (response) { if (response) {
setSeriesBooks(updatedBooks); setSeriesBooks(updatedBooks);

View File

@@ -339,17 +339,18 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
} }
} }
setSections(function (prev: LocationProps[]): LocationProps[] { setSections(function (prev: LocationProps[]): LocationProps[] {
const updated: LocationProps[] = [...prev]; return prev.map(function (section: LocationProps): LocationProps {
const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean { if (section.id !== sectionId) return section;
return section.id === sectionId; return {
...section,
elements: [...section.elements, {
id: elementId,
name: newElementNames[sectionId],
description: '',
subElements: [],
}],
};
}); });
updated[sectionIndex].elements.push({
id: elementId,
name: newElementNames[sectionId],
description: '',
subElements: [],
});
return updated;
}); });
setNewElementNames(function (prev: { [key: string]: string }): { [key: string]: string } { setNewElementNames(function (prev: { [key: string]: string }): { [key: string]: string } {
return {...prev, [sectionId]: ''}; return {...prev, [sectionId]: ''};
@@ -404,13 +405,23 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
} }
} }
setSections(function (prev: LocationProps[]): LocationProps[] { setSections(function (prev: LocationProps[]): LocationProps[] {
const updated: LocationProps[] = [...prev]; return prev.map(function (section: LocationProps, i: number): LocationProps {
updated[sectionIndex].elements[elementIndex].subElements.push({ if (i !== sectionIndex) return section;
id: subElementId, return {
name: newSubElementNames[elementIndex], ...section,
description: '', 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 } { setNewSubElementNames(function (prev: { [key: string]: string }): { [key: string]: string } {
return {...prev, [elementIndex]: ''}; return {...prev, [elementIndex]: ''};

View File

@@ -1,5 +1,5 @@
import {useContext} from 'react'; 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 {SessionContext} from '@/context/SessionContext';
import {LangContext} from '@/context/LangContext'; import {LangContext} from '@/context/LangContext';
import {AlertContext} from '@/context/AlertContext'; import {AlertContext} from '@/context/AlertContext';

View File

@@ -106,7 +106,7 @@
}, },
"chapters": { "chapters": {
"title": "Chapters", "title": "Chapters",
"detected": "{count} chapter(s) detected", "detected": "{{count}} chapter(s) detected",
"selectAll": "Select all", "selectAll": "Select all",
"deselectAll": "Deselect all", "deselectAll": "Deselect all",
"words": "words" "words": "words"
@@ -133,7 +133,7 @@
"errorChapterUpdate": "An error occurred while updating the chapter.", "errorChapterUpdate": "An error occurred while updating the chapter.",
"errorChapterDelete": "An error occurred while deleting the chapter.", "errorChapterDelete": "An error occurred while deleting the chapter.",
"errorChapterNameRequired": "Chapter name is required.", "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.", "errorFetchChapter": "Error while fetching chapter.",
"errorFetchChapters": "Error while fetching chapters.", "errorFetchChapters": "Error while fetching chapters.",
"successUpdate": "Chapter updated successfully.", "successUpdate": "Chapter updated successfully.",
@@ -226,8 +226,8 @@
"inputPlaceholder": "Enter a word...", "inputPlaceholder": "Enter a word...",
"enterWordError": "Please enter a word to search.", "enterWordError": "Please enter a word to search.",
"loading": "Searching...", "loading": "Searching...",
"resultSynonyms": "Synonyms for \"{word}\"", "resultSynonyms": "Synonyms for \"{{word}}\"",
"resultAntonyms": "Antonyms for \"{word}\"", "resultAntonyms": "Antonyms for \"{{word}}\"",
"emptySynonymsTitle": "Synonym Search", "emptySynonymsTitle": "Synonym Search",
"emptyAntonymsTitle": "Antonym Search", "emptyAntonymsTitle": "Antonym Search",
"emptySynonymsDescription": "Enter a word to find synonyms suitable for different writing contexts.", "emptySynonymsDescription": "Enter a word to find synonyms suitable for different writing contexts.",
@@ -305,7 +305,7 @@
"search": "Search for a world...", "search": "Search for a world...",
"newWorld": "New world", "newWorld": "New world",
"deleteTitle": "Delete 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", "worldName": "World name",
"worldNamePlaceholder": "Enter the world name", "worldNamePlaceholder": "Enter the world name",
"worldHistory": "World history", "worldHistory": "World history",
@@ -338,7 +338,7 @@
"noSectionDescription": "Create your first section to organize your story's locations.", "noSectionDescription": "Create your first section to organize your story's locations.",
"newSection": "New section", "newSection": "New section",
"deleteTitle": "Delete 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", "elementName": "Element name",
"elementNamePlaceholder": "Element name", "elementNamePlaceholder": "Element name",
"elementDescriptionPlaceholder": "Element description", "elementDescriptionPlaceholder": "Element description",
@@ -346,11 +346,12 @@
"subElementNamePlaceholder": "Sub-element name", "subElementNamePlaceholder": "Sub-element name",
"subElementDescriptionPlaceholder": "Sub-element description", "subElementDescriptionPlaceholder": "Sub-element description",
"newSubElementPlaceholder": "New sub-element", "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", "newElementPlaceholder": "New element",
"noSectionAvailable": "No section available.", "noSectionAvailable": "No section available.",
"createSectionLabel": "Create section", "createSectionLabel": "Create section",
"elementsCount": "{count} elements", "elementsCount": "{{count}} elements",
"element": "Element", "element": "Element",
"addElement": "Add an element", "addElement": "Add an element",
"addSubElement": "Add a sub-element", "addSubElement": "Add a sub-element",
@@ -438,7 +439,7 @@
"save": "Save", "save": "Save",
"delete": "Delete", "delete": "Delete",
"deleteTitle": "Delete spell", "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", "basicInfo": "Basic information",
"name": "Spell name", "name": "Spell name",
"namePlaceholder": "Enter spell name", "namePlaceholder": "Enter spell name",
@@ -448,7 +449,7 @@
"appearancePlaceholder": "Describe the visual appearance of the spell", "appearancePlaceholder": "Describe the visual appearance of the spell",
"tags": "Tags", "tags": "Tags",
"addTag": "Add a tag...", "addTag": "Add a tag...",
"createTag": "Create \"{name}\"", "createTag": "Create \"{{name}}\"",
"powerLevel": "Power level", "powerLevel": "Power level",
"components": "Components", "components": "Components",
"componentsPlaceholder": "Ingredients, gestures, incantations required...", "componentsPlaceholder": "Ingredients, gestures, incantations required...",
@@ -487,7 +488,7 @@
"newCharacter": "New character", "newCharacter": "New character",
"exportToSeries": "Export to series", "exportToSeries": "Export to series",
"deleteTitle": "Delete character", "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", "basicInfo": "Basic information",
"name": "Name", "name": "Name",
"namePlaceholder": "Enter a name", "namePlaceholder": "Enter a name",
@@ -545,7 +546,7 @@
"noCharactersDescription": "Add your first character to get started." "noCharactersDescription": "Add your first character to get started."
}, },
"characterSectionElement": { "characterSectionElement": {
"newItem": "New {item}" "newItem": "New {{item}}"
}, },
"aboutEditors": { "aboutEditors": {
"title": "About Scribe", "title": "About Scribe",
@@ -572,7 +573,8 @@
"bookGoals": "Book goals", "bookGoals": "Book goals",
"save": "Save", "save": "Save",
"notAvailable": "Option not available", "notAvailable": "Option not available",
"unknownError": "An unknown error occurred." "unknownError": "An unknown error occurred.",
"quillsenseOffline": "QuillSense is not available offline."
}, },
"noBookHome": { "noBookHome": {
"title": "Your work is waiting for its first words", "title": "Your work is waiting for its first words",
@@ -583,14 +585,18 @@
"preferences": "Preferences", "preferences": "Preferences",
"ghostWriter": "Ghost Writer", "ghostWriter": "Ghost Writer",
"draftCompanion": "Draft Companion", "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": { "draftCompanion": {
"noPreviousVersion": "No previous version of this chapter", "noPreviousVersion": "No previous version of this chapter",
"errorFetchDraft": "Error while fetching draft content.", "errorFetchDraft": "Error while fetching draft content.",
"unknownError": "An unknown error has occurred", "unknownError": "An unknown error has occurred",
"errorRefineText": "Error refining text", "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", "unknownErrorRefineDraft": "An unknown error occurred while refining the draft",
"successInsert": "Correction successfully inserted", "successInsert": "Correction successfully inserted",
"selectVersion": "Select a version", "selectVersion": "Select a version",
@@ -882,10 +888,10 @@
"errorDelete": "Error deleting the element.", "errorDelete": "Error deleting the element.",
"errorAdd": "Error adding the element.", "errorAdd": "Error adding the element.",
"errorUnknown": "An unexpected error occurred.", "errorUnknown": "An unexpected error occurred.",
"emptyField": "The {section} field is empty.", "emptyField": "The {{section}} field is empty.",
"namePlaceholder": "Name of {section}", "namePlaceholder": "Name of {{section}}",
"descriptionPlaceholder": "Description of {section}", "descriptionPlaceholder": "Description of {{section}}",
"newPlaceholder": "New {section}" "newPlaceholder": "New {{section}}"
}, },
"bookTypes": { "bookTypes": {
"short": "Short Story", "short": "Short Story",
@@ -1093,7 +1099,7 @@
"homePage": { "homePage": {
"loading": "Loading...", "loading": "Loading...",
"guide": { "guide": {
"welcome": "Welcome {name}", "welcome": "Welcome {{name}}",
"step0": { "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.", "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." "description2": "This guide will help you get started. You can skip it if you wish."
@@ -1195,7 +1201,7 @@
} }
}, },
"syncField": { "syncField": {
"uploadSuccess": "{count} element(s) updated successfully.", "uploadSuccess": "{{count}} element(s) updated successfully.",
"uploadTooltip": "Push to series", "uploadTooltip": "Push to series",
"downloadTooltip": "Pull from series" "downloadTooltip": "Pull from series"
}, },
@@ -1210,7 +1216,7 @@
"exporting": "Exporting...", "exporting": "Exporting...",
"noBookSelected": "No book selected.", "noBookSelected": "No book selected.",
"noChaptersSelected": "Please select at least one chapter.", "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.", "downloadError": "Download failed.",
"serverError": "Server error during export.", "serverError": "Server error during export.",
"unknownError": "An unknown error occurred." "unknownError": "An unknown error occurred."

View File

@@ -106,7 +106,7 @@
}, },
"chapters": { "chapters": {
"title": "Chapitres", "title": "Chapitres",
"detected": "{count} chapitre(s) détecté(s)", "detected": "{{count}} chapitre(s) détecté(s)",
"selectAll": "Tout sélectionner", "selectAll": "Tout sélectionner",
"deselectAll": "Tout désélectionner", "deselectAll": "Tout désélectionner",
"words": "mots" "words": "mots"
@@ -133,7 +133,7 @@
"errorChapterUpdate": "Une erreur est survenue lors de la mise à jour du chapitre.", "errorChapterUpdate": "Une erreur est survenue lors de la mise à jour du chapitre.",
"errorChapterDelete": "Une erreur est survenue lors de la suppression du chapitre.", "errorChapterDelete": "Une erreur est survenue lors de la suppression du chapitre.",
"errorChapterNameRequired": "Le nom du chapitre est requis.", "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.", "errorFetchChapter": "Erreur lors de la récupération du chapitre.",
"errorFetchChapters": "Erreur lors de la récupération des chapitres.", "errorFetchChapters": "Erreur lors de la récupération des chapitres.",
"successUpdate": "Chapitre mis à jour avec succès.", "successUpdate": "Chapitre mis à jour avec succès.",
@@ -226,8 +226,8 @@
"inputPlaceholder": "Entrez un mot...", "inputPlaceholder": "Entrez un mot...",
"enterWordError": "Veuillez entrer un mot à rechercher.", "enterWordError": "Veuillez entrer un mot à rechercher.",
"loading": "Recherche en cours...", "loading": "Recherche en cours...",
"resultSynonyms": "Synonymes de \"{word}\"", "resultSynonyms": "Synonymes de \"{{word}}\"",
"resultAntonyms": "Antonymes de \"{word}\"", "resultAntonyms": "Antonymes de \"{{word}}\"",
"emptySynonymsTitle": "Recherche de synonymes", "emptySynonymsTitle": "Recherche de synonymes",
"emptyAntonymsTitle": "Recherche d'antonymes", "emptyAntonymsTitle": "Recherche d'antonymes",
"emptySynonymsDescription": "Entrez un mot pour trouver des synonymes adaptés à différents contextes d'écriture.", "emptySynonymsDescription": "Entrez un mot pour trouver des synonymes adaptés à différents contextes d'écriture.",
@@ -305,7 +305,7 @@
"search": "Rechercher un monde...", "search": "Rechercher un monde...",
"newWorld": "Nouveau monde", "newWorld": "Nouveau monde",
"deleteTitle": "Supprimer le 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", "worldName": "Nom du monde",
"worldNamePlaceholder": "Entrez le nom du monde", "worldNamePlaceholder": "Entrez le nom du monde",
"worldHistory": "Histoire du monde", "worldHistory": "Histoire du monde",
@@ -338,7 +338,7 @@
"noSectionDescription": "Créez votre première section pour organiser les lieux de votre histoire.", "noSectionDescription": "Créez votre première section pour organiser les lieux de votre histoire.",
"newSection": "Nouvelle section", "newSection": "Nouvelle section",
"deleteTitle": "Supprimer la 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", "elementName": "Nom de l'élément",
"elementNamePlaceholder": "Nom de l'élément", "elementNamePlaceholder": "Nom de l'élément",
"elementDescriptionPlaceholder": "Description de l'élément", "elementDescriptionPlaceholder": "Description de l'élément",
@@ -346,11 +346,12 @@
"subElementNamePlaceholder": "Nom du sous-élément", "subElementNamePlaceholder": "Nom du sous-élément",
"subElementDescriptionPlaceholder": "Description du sous-élément", "subElementDescriptionPlaceholder": "Description du sous-élément",
"newSubElementPlaceholder": "Nouveau 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", "newElementPlaceholder": "Nouvel élément",
"noSectionAvailable": "Aucune section disponible.", "noSectionAvailable": "Aucune section disponible.",
"createSectionLabel": "Créer une section", "createSectionLabel": "Créer une section",
"elementsCount": "{count} éléments", "elementsCount": "{{count}} éléments",
"element": "Élément", "element": "Élément",
"addElement": "Ajouter un élément", "addElement": "Ajouter un élément",
"addSubElement": "Ajouter un sous-élément", "addSubElement": "Ajouter un sous-élément",
@@ -438,7 +439,7 @@
"save": "Enregistrer", "save": "Enregistrer",
"delete": "Supprimer", "delete": "Supprimer",
"deleteTitle": "Supprimer le sort", "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", "basicInfo": "Informations de base",
"name": "Nom du sort", "name": "Nom du sort",
"namePlaceholder": "Entrez le nom du sort", "namePlaceholder": "Entrez le nom du sort",
@@ -448,7 +449,7 @@
"appearancePlaceholder": "Décrivez l'apparence visuelle du sort", "appearancePlaceholder": "Décrivez l'apparence visuelle du sort",
"tags": "Tags", "tags": "Tags",
"addTag": "Ajouter un tag...", "addTag": "Ajouter un tag...",
"createTag": "Créer \"{name}\"", "createTag": "Créer \"{{name}}\"",
"powerLevel": "Niveau de puissance", "powerLevel": "Niveau de puissance",
"components": "Composantes", "components": "Composantes",
"componentsPlaceholder": "Ingrédients, gestes, incantations nécessaires...", "componentsPlaceholder": "Ingrédients, gestes, incantations nécessaires...",
@@ -487,7 +488,7 @@
"newCharacter": "Nouveau personnage", "newCharacter": "Nouveau personnage",
"exportToSeries": "Exporter vers la série", "exportToSeries": "Exporter vers la série",
"deleteTitle": "Supprimer le personnage", "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", "basicInfo": "Informations de base",
"name": "Nom", "name": "Nom",
"namePlaceholder": "Entrer un nom", "namePlaceholder": "Entrer un nom",
@@ -545,7 +546,7 @@
"noCharactersDescription": "Ajoutez votre premier personnage pour commencer." "noCharactersDescription": "Ajoutez votre premier personnage pour commencer."
}, },
"characterSectionElement": { "characterSectionElement": {
"newItem": "Nouveau {item}" "newItem": "Nouveau {{item}}"
}, },
"aboutEditors": { "aboutEditors": {
"title": "À propos de Scribe", "title": "À propos de Scribe",
@@ -572,7 +573,8 @@
"bookGoals": "Objectifs du livre", "bookGoals": "Objectifs du livre",
"save": "Sauvegarder", "save": "Sauvegarder",
"notAvailable": "Option non disponible", "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": { "noBookHome": {
"title": "Votre œuvre attend ses premiers mots", "title": "Votre œuvre attend ses premiers mots",
@@ -583,14 +585,18 @@
"preferences": "Préférences", "preferences": "Préférences",
"ghostWriter": "Écrivain Fantôme", "ghostWriter": "Écrivain Fantôme",
"draftCompanion": "Draft Companion", "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": { "draftCompanion": {
"noPreviousVersion": "Aucune version antérieure de ce chapitre", "noPreviousVersion": "Aucune version antérieure de ce chapitre",
"errorFetchDraft": "Erreur lors de la récupération du contenu du brouillon.", "errorFetchDraft": "Erreur lors de la récupération du contenu du brouillon.",
"unknownError": "Une erreur inconnue s'est produite", "unknownError": "Une erreur inconnue s'est produite",
"errorRefineText": "Erreur lors de la correction du texte", "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", "unknownErrorRefineDraft": "Une erreur inconnue s'est produite lors de la correction du brouillon",
"successInsert": "Correction insérée avec succès", "successInsert": "Correction insérée avec succès",
"selectVersion": "Sélectionner une version", "selectVersion": "Sélectionner une version",
@@ -881,10 +887,10 @@
"errorDelete": "Erreur lors de la suppression de l'élément.", "errorDelete": "Erreur lors de la suppression de l'élément.",
"errorAdd": "Erreur lors de l'ajout de l'élément.", "errorAdd": "Erreur lors de l'ajout de l'élément.",
"errorUnknown": "Une erreur inattendue s'est produite.", "errorUnknown": "Une erreur inattendue s'est produite.",
"emptyField": "Le champ {section} est vide.", "emptyField": "Le champ {{section}} est vide.",
"namePlaceholder": "Nom {section}", "namePlaceholder": "Nom {{section}}",
"descriptionPlaceholder": "Description de {section}", "descriptionPlaceholder": "Description de {{section}}",
"newPlaceholder": "Nouveau {section}" "newPlaceholder": "Nouveau {{section}}"
}, },
"bookTypes": { "bookTypes": {
"short": "Nouvelle", "short": "Nouvelle",
@@ -1092,7 +1098,7 @@
"homePage": { "homePage": {
"loading": "Chargement en cours...", "loading": "Chargement en cours...",
"guide": { "guide": {
"welcome": "Bienvenue {name}", "welcome": "Bienvenue {{name}}",
"step0": { "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.", "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." "description2": "Ce guide vous accompagnera dans vos premiers pas. Vous pouvez naturellement le passer si vous le souhaitez."
@@ -1194,7 +1200,7 @@
} }
}, },
"syncField": { "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", "uploadTooltip": "Envoyer vers la série",
"downloadTooltip": "Récupérer depuis la série" "downloadTooltip": "Récupérer depuis la série"
}, },
@@ -1209,7 +1215,7 @@
"exporting": "Exportation en cours...", "exporting": "Exportation en cours...",
"noBookSelected": "Aucun livre sélectionné.", "noBookSelected": "Aucun livre sélectionné.",
"noChaptersSelected": "Veuillez sélectionner au moins un chapitre.", "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.", "downloadError": "Échec du téléchargement.",
"serverError": "Erreur du serveur lors de l'export.", "serverError": "Erreur du serveur lors de l'export.",
"unknownError": "Une erreur inconnue est survenue." "unknownError": "Une erreur inconnue est survenue."

View File

@@ -1,7 +1,7 @@
{ {
"name": "eritorsscribe", "name": "eritorsscribe",
"productName": "ERitors Scribe", "productName": "ERitors Scribe",
"version": "0.4.1", "version": "0.5.0",
"type": "module", "type": "module",
"main": "dist/electron/main.js", "main": "dist/electron/main.js",
"scripts": { "scripts": {

View File

@@ -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| { .query_row(params![version, chapter_id, user_id], |query_row| {
Ok(ChapterContentQueryResult { Ok(ChapterContentQueryResult {
chapter_id: query_row.get(0)?, title: query_row.get(1)?, 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(), content: query_row.get::<_, Option<String>>(4)?.unwrap_or_default(),
version: query_row.get::<_, Option<i64>>(5)?.unwrap_or(2), version: query_row.get::<_, Option<i64>>(5)?.unwrap_or(2),
}) })

View File

@@ -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 { let element_idx: usize = match element_index {
Some(idx) => idx, Some(idx) => idx,
None => { None => {
let decrypted_name: String = decrypt_data_with_user_key(record.element_name.as_deref().unwrap_or(""), &user_key)?; let decrypted_name: String = match record.element_name.as_deref() {
let decrypted_description: String = if let Some(ref element_description) = record.element_description { Some(name) if !name.is_empty() => decrypt_data_with_user_key(name, &user_key)?,
decrypt_data_with_user_key(element_description, &user_key)? _ => String::new(),
} 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(),
}; };
location_array[location_idx].elements.push(Element { location_array[location_idx].elements.push(Element {
id: element_id.clone(), 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); .any(|sub| sub.id == *sub_element_id);
if !sub_element_exists { 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_name: String = match record.sub_elem_name.as_deref() {
let decrypted_description: String = if let Some(ref sub_elem_description) = record.sub_elem_description { Some(name) if !name.is_empty() => decrypt_data_with_user_key(name, &user_key)?,
decrypt_data_with_user_key(sub_elem_description, &user_key)? _ => String::new(),
} else { };
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 { location_array[location_idx].elements[element_idx].sub_elements.push(SubElement {
id: sub_element_id.clone(), 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) { if processed_ids.contains(sub_element_id) {
continue; continue;
} }
let decrypted_name: String = decrypt_data_with_user_key(record.sub_elem_name.as_deref().unwrap_or(""), &user_key)?; let decrypted_name: String = match record.sub_elem_name.as_deref() {
let decrypted_description: String = if let Some(ref sub_elem_description) = record.sub_elem_description { Some(name) if !name.is_empty() => decrypt_data_with_user_key(name, &user_key)?,
decrypt_data_with_user_key(sub_elem_description, &user_key)? _ => String::new(),
} else { };
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 { sub_elements.push(SubElement {
id: sub_element_id.clone(), id: sub_element_id.clone(),
@@ -343,10 +349,9 @@ pub fn get_location_tags(conn: &Connection, user_id: &str, book_id: &str, lang:
continue; continue;
} }
let decrypted_name: String = decrypt_data_with_user_key(&record.element_name, &user_key)?; 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 { let decrypted_description: String = match record.element_description.as_deref() {
decrypt_data_with_user_key(element_description, &user_key)? Some(description) if !description.is_empty() => decrypt_data_with_user_key(description, &user_key)?,
} else { _ => String::new(),
String::new()
}; };
sub_elements.push(SubElement { sub_elements.push(SubElement {
id: record.element_id.clone(), id: record.element_id.clone(),