From cfd08e32612f6d5cb8266eef1f701ff27de1af40 Mon Sep 17 00:00:00 2001 From: natreex Date: Tue, 24 Mar 2026 22:45:10 -0400 Subject: [PATCH] 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. --- app/globals.css | 71 +----- components/book/BookCard.tsx | 30 ++- .../book/settings/BookSettingOption.tsx | 23 +- .../editor/CharacterEditorDetail.tsx | 21 +- .../characters/editor/CharacterEditorEdit.tsx | 21 +- .../settings/CharacterSettingsDetail.tsx | 21 +- .../settings/CharacterSettingsEdit.tsx | 21 +- .../settings/locations/LocationComponent.tsx | 207 ++++++++---------- .../locations/settings/LocationSettings.tsx | 11 +- .../settings/LocationSettingsDetail.tsx | 1 + components/editor/TextEditor.tsx | 63 ++++-- components/editor/UserEditorSetting.tsx | 27 +-- components/series/AddNewSeriesForm.tsx | 18 +- components/series/SeriesSettingSidebar.tsx | 15 +- .../settings/BasicSeriesInformation.tsx | 22 +- .../series/settings/SeriesBooksManager.tsx | 55 ++--- hooks/settings/useLocations.ts | 43 ++-- hooks/useSyncBooks.ts | 2 +- lib/locales/en.json | 50 +++-- lib/locales/fr.json | 50 +++-- package.json | 2 +- src-tauri/src/domains/chapter_content/repo.rs | 2 +- src-tauri/src/domains/location/service.rs | 43 ++-- 23 files changed, 410 insertions(+), 409 deletions(-) diff --git a/app/globals.css b/app/globals.css index 84ff6ef..3533c9a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -284,62 +284,6 @@ body { padding-bottom: 10px; } -.indent-0 { - text-indent: 0px !important; -} - -.indent-1 { - text-indent: 4px !important; -} - -.indent-2 { - text-indent: 8px !important; -} - -.indent-3 { - text-indent: 12px !important; -} - -.indent-4 { - text-indent: 16px !important; -} - -.indent-5 { - text-indent: 20px !important; -} - -.indent-6 { - text-indent: 24px !important; -} - -.indent-7 { - text-indent: 28px !important; -} - -.indent-8 { - text-indent: 32px !important; -} - -.indent-9 { - text-indent: 36px !important; -} - -.indent-10 { - text-indent: 40px !important; -} - -.indent-11 { - text-indent: 44px !important; -} - -.indent-12 { - text-indent: 48px !important; -} - -.indent-13 { - text-indent: 52px !important; -} - /* Styles pour l'éditeur principal avec classes dynamiques */ .editor-content .tiptap > p:first-child, .editor-content .tiptap > h1:first-child, @@ -350,14 +294,17 @@ body { .editor-content .tiptap p { color: var(--color-editor-text); + text-indent: 1.25rem; + overflow-wrap: anywhere; margin-top: 0.7em; margin-bottom: 0.7em; } -.editor-content .tiptap p { - text-indent: inherit; +.no-text-indent .editor-content .tiptap p { + text-indent: 0; } + .editor-content .tiptap p strong { font-weight: 900; color: var(--color-editor-bold); @@ -628,11 +575,17 @@ body { .tiptap-draft p { font-family: 'Lora', sans-serif; - text-indent: 30px; + text-indent: 1.25rem; + overflow-wrap: anywhere; margin-top: 0.7em; margin-bottom: 0.7em; } +.no-text-indent .tiptap-draft p { + text-indent: 0; +} + + /* Form input base */ .input-base { @apply w-full text-text-primary bg-dark-background px-4 py-2.5 rounded-xl diff --git a/components/book/BookCard.tsx b/components/book/BookCard.tsx index 38b9104..41adb69 100644 --- a/components/book/BookCard.tsx +++ b/components/book/BookCard.tsx @@ -18,23 +18,21 @@ export default function BookCard({book, onClickCallback, index, syncStatus}: Boo return (
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"> - + {book.coverImage ? ( + {book.title + ) : ( +
+ + {book.title.charAt(0).toUpperCase()} + +
+ )} {isDesktop && syncStatus && (
e.stopPropagation()}> diff --git a/components/book/settings/BookSettingOption.tsx b/components/book/settings/BookSettingOption.tsx index fb8810f..a555f19 100644 --- a/components/book/settings/BookSettingOption.tsx +++ b/components/book/settings/BookSettingOption.tsx @@ -10,7 +10,10 @@ import {BookContext, BookContextProps} from "@/context/BookContext"; import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {LangContext, LangContextProps} from "@/context/LangContext"; +import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {isDesktop} from "@/lib/configs"; import {apiPatch} from "@/lib/api/client"; +import {updateBookToolSetting} from "@/lib/tauri"; import {SettingRef} from "@/lib/types/settings"; import {BookProps} from "@/lib/types/book"; @@ -73,6 +76,7 @@ export default function BookSettingOption({setting}: BookSettingOptionProps): Re const {session}: SessionContextProps = useContext(SessionContext); const {errorMessage}: AlertContextProps = useContext(AlertContext); const {lang}: LangContextProps = useContext(LangContext); + const {isCurrentlyOffline} = useContext(OfflineContext); const userToken: string = session?.accessToken ?? ''; const isToggleable: boolean = setting in toggleableSettings; @@ -107,13 +111,20 @@ export default function BookSettingOption({setting}: BookSettingOptionProps): Re async function handleToggleTool(enabled: boolean): Promise { const toolName: ToolName | undefined = toggleableSettings[setting]; - if (!toolName) return; + if (!toolName || !book?.bookId) return; + const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book.localBook); + if (useLocal && toolName === 'quillsense') { + errorMessage(t('bookSettingOption.quillsenseOffline')); + return; + } try { - const result: boolean = await apiPatch('book/tool-setting', { - bookId: book?.bookId, - toolName: toolName, - enabled: enabled - }, userToken, lang); + const result: boolean = useLocal + ? await updateBookToolSetting(book.bookId, toolName, enabled) + : await apiPatch('book/tool-setting', { + bookId: book.bookId, + toolName: toolName, + enabled: enabled + }, userToken, lang); if (result && setBook && book) { setToolEnabled(enabled); if (toolName === 'quillsense') { diff --git a/components/book/settings/characters/editor/CharacterEditorDetail.tsx b/components/book/settings/characters/editor/CharacterEditorDetail.tsx index 20708b4..d4c8745 100644 --- a/components/book/settings/characters/editor/CharacterEditorDetail.tsx +++ b/components/book/settings/characters/editor/CharacterEditorDetail.tsx @@ -10,7 +10,11 @@ import AvatarIcon from '@/components/ui/AvatarIcon'; import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {LangContext, LangContextProps} from '@/context/LangContext'; +import {BookContext, BookContextProps} from '@/context/BookContext'; +import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {isDesktop} from '@/lib/configs'; import {apiGet} from '@/lib/api/client'; +import {getCharacterAttributes} from '@/lib/tauri'; type AttributeResponse = { type: string; values: Attribute[] }[]; @@ -34,6 +38,8 @@ export default function CharacterEditorDetail({ const {lang}: LangContextProps = useContext(LangContext); const {session}: SessionContextProps = useContext(SessionContext); const {errorMessage}: AlertContextProps = useContext(AlertContext); + const {book}: BookContextProps = useContext(BookContext); + const {isCurrentlyOffline} = useContext(OfflineContext); useEffect(function (): void { if (character?.id !== null) { @@ -43,12 +49,15 @@ export default function CharacterEditorDetail({ async function getAttributes(): Promise { try { - const response: AttributeResponse = await apiGet( - 'character/attribute', - session.accessToken, - lang, - {characterId: character?.id} - ); + const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook); + const response: AttributeResponse = useLocal + ? await getCharacterAttributes(character.id!) as AttributeResponse + : await apiGet( + 'character/attribute', + session.accessToken, + lang, + {characterId: character?.id} + ); if (response && onLoadAttributes) { const attributes: CharacterAttribute = {}; response.forEach(function (item: { type: string; values: Attribute[] }): void { diff --git a/components/book/settings/characters/editor/CharacterEditorEdit.tsx b/components/book/settings/characters/editor/CharacterEditorEdit.tsx index 647922d..f64ab01 100644 --- a/components/book/settings/characters/editor/CharacterEditorEdit.tsx +++ b/components/book/settings/characters/editor/CharacterEditorEdit.tsx @@ -28,7 +28,11 @@ import {useTranslations} from '@/lib/i18n'; import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {LangContext, LangContextProps} from '@/context/LangContext'; +import {BookContext, BookContextProps} from '@/context/BookContext'; +import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {isDesktop} from '@/lib/configs'; import {apiGet} from '@/lib/api/client'; +import {getCharacterAttributes} from '@/lib/tauri'; type AttributeResponse = { type: string; values: Attribute[] }[]; @@ -59,6 +63,8 @@ export default function CharacterEditorEdit({ const {lang}: LangContextProps = useContext(LangContext); const {session}: SessionContextProps = useContext(SessionContext); const {errorMessage}: AlertContextProps = useContext(AlertContext); + const {book}: BookContextProps = useContext(BookContext); + const {isCurrentlyOffline} = useContext(OfflineContext); const [showAdvanced, setShowAdvanced] = useState(false); useEffect(function (): void { @@ -69,12 +75,15 @@ export default function CharacterEditorEdit({ async function getAttributes(): Promise { try { - const response: AttributeResponse = await apiGet( - 'character/attribute', - session.accessToken, - lang, - {characterId: character?.id} - ); + const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook); + const response: AttributeResponse = useLocal + ? await getCharacterAttributes(character.id!) as AttributeResponse + : await apiGet( + 'character/attribute', + session.accessToken, + lang, + {characterId: character?.id} + ); if (response) { const attributes: CharacterAttribute = {}; response.forEach(function (item: { type: string; values: Attribute[] }): void { diff --git a/components/book/settings/characters/settings/CharacterSettingsDetail.tsx b/components/book/settings/characters/settings/CharacterSettingsDetail.tsx index 3094f54..5930709 100644 --- a/components/book/settings/characters/settings/CharacterSettingsDetail.tsx +++ b/components/book/settings/characters/settings/CharacterSettingsDetail.tsx @@ -33,7 +33,11 @@ import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {dynamicBg} from '@/lib/utils/dynamicStyles'; import {LangContext, LangContextProps} from '@/context/LangContext'; +import {BookContext, BookContextProps} from '@/context/BookContext'; +import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {isDesktop} from '@/lib/configs'; import {apiGet} from '@/lib/api/client'; +import {getCharacterAttributes} from '@/lib/tauri'; type AttributeResponse = { type: string; values: Attribute[] }[]; @@ -52,6 +56,8 @@ export default function CharacterSettingsDetail({ const {lang}: LangContextProps = useContext(LangContext); const {session}: SessionContextProps = useContext(SessionContext); const {errorMessage}: AlertContextProps = useContext(AlertContext); + const {book}: BookContextProps = useContext(BookContext); + const {isCurrentlyOffline} = useContext(OfflineContext); const [showAdvanced, setShowAdvanced] = useState(false); useEffect(function (): void { @@ -62,12 +68,15 @@ export default function CharacterSettingsDetail({ async function getAttributes(): Promise { try { - const response: AttributeResponse = await apiGet( - 'character/attribute', - session.accessToken, - lang, - {characterId: character?.id} - ); + const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook); + const response: AttributeResponse = useLocal + ? await getCharacterAttributes(character.id!) as AttributeResponse + : await apiGet( + 'character/attribute', + session.accessToken, + lang, + {characterId: character?.id} + ); if (response && onLoadAttributes) { const attributes: CharacterAttribute = {}; response.forEach(function (item: { type: string; values: Attribute[] }): void { diff --git a/components/book/settings/characters/settings/CharacterSettingsEdit.tsx b/components/book/settings/characters/settings/CharacterSettingsEdit.tsx index 2efc753..973a113 100644 --- a/components/book/settings/characters/settings/CharacterSettingsEdit.tsx +++ b/components/book/settings/characters/settings/CharacterSettingsEdit.tsx @@ -29,7 +29,11 @@ import {useTranslations} from '@/lib/i18n'; import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {LangContext, LangContextProps} from '@/context/LangContext'; +import {BookContext, BookContextProps} from '@/context/BookContext'; +import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {isDesktop} from '@/lib/configs'; import {apiGet} from '@/lib/api/client'; +import {getCharacterAttributes} from '@/lib/tauri'; type AttributeResponse = { type: string; values: Attribute[] }[]; @@ -61,6 +65,8 @@ export default function CharacterSettingsEdit({ const {lang}: LangContextProps = useContext(LangContext); const {session}: SessionContextProps = useContext(SessionContext); const {errorMessage}: AlertContextProps = useContext(AlertContext); + const {book}: BookContextProps = useContext(BookContext); + const {isCurrentlyOffline} = useContext(OfflineContext); const [showAdvanced, setShowAdvanced] = useState(false); useEffect(function (): void { @@ -71,12 +77,15 @@ export default function CharacterSettingsEdit({ async function getAttributes(): Promise { try { - const response: AttributeResponse = await apiGet( - 'character/attribute', - session.accessToken, - lang, - {characterId: character?.id} - ); + const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook); + const response: AttributeResponse = useLocal + ? await getCharacterAttributes(character.id!) as AttributeResponse + : await apiGet( + 'character/attribute', + session.accessToken, + lang, + {characterId: character?.id} + ); if (response) { const attributes: CharacterAttribute = {}; response.forEach(function (item: { type: string; values: Attribute[] }): void { diff --git a/components/book/settings/locations/LocationComponent.tsx b/components/book/settings/locations/LocationComponent.tsx index c9aaf47..1877dc3 100644 --- a/components/book/settings/locations/LocationComponent.tsx +++ b/components/book/settings/locations/LocationComponent.tsx @@ -4,7 +4,10 @@ import React, {ChangeEvent, forwardRef, useContext, useEffect, useImperativeHand import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {BookContext, BookContextProps} from "@/context/BookContext"; +import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {isDesktop} from '@/lib/configs'; import {apiDelete, apiGet, apiPatch, apiPost} from '@/lib/api/client'; +import * as tauri from '@/lib/tauri'; import InputField from "@/components/form/InputField"; import TextInput from '@/components/form/TextInput'; import TextAreaInput from "@/components/form/TextAreaInput"; @@ -53,8 +56,10 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< const {session}: SessionContextProps = useContext(SessionContext); const {successMessage, errorMessage}: AlertContextProps = useContext(AlertContext); const {book, setBook}: BookContextProps = useContext(BookContext); - + const {isCurrentlyOffline} = useContext(OfflineContext); + const currentEntityId: string = entityId || book?.bookId || ''; + const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook); const isSeriesMode: boolean = entityType === 'series'; const token: string = session.accessToken; @@ -87,12 +92,14 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< async function getSeriesLocations(): Promise { if (!bookSeriesId) return; try { - const response: SeriesLocationItem[] = await apiGet( - 'series/location/list', - token, - lang, - {seriesid: bookSeriesId} - ); + const response: SeriesLocationItem[] = useLocal + ? await tauri.getSeriesLocationList(bookSeriesId) as SeriesLocationItem[] + : await apiGet( + 'series/location/list', + token, + lang, + {seriesid: bookSeriesId} + ); if (response) { setSeriesLocations(response); } @@ -106,11 +113,13 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< async function handleToggleTool(enabled: boolean): Promise { if (isSeriesMode) return; try { - const response: boolean = await apiPatch('book/tool-setting', { - bookId: currentEntityId, - toolName: 'locations', - enabled: enabled - }, token, lang); + const response: boolean = useLocal + ? await tauri.updateBookToolSetting(currentEntityId, 'locations', enabled) + : await apiPatch('book/tool-setting', { + bookId: currentEntityId, + toolName: 'locations', + enabled: enabled + }, token, lang); if (response && setBook && book) { setToolEnabled(enabled); setBook({ @@ -132,12 +141,14 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< async function getAllLocations(): Promise { try { if (isSeriesMode) { - const response: SeriesLocationItem[] = await apiGet( - 'series/location/list', - token, - lang, - {seriesid: currentEntityId} - ); + const response: SeriesLocationItem[] = useLocal + ? await tauri.getSeriesLocationList(currentEntityId) as SeriesLocationItem[] + : await apiGet( + 'series/location/list', + token, + lang, + {seriesid: currentEntityId} + ); if (response) { const mappedLocations: LocationProps[] = response.map((loc: SeriesLocationItem): LocationProps => ({ id: loc.id, @@ -156,12 +167,14 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< setSections(mappedLocations); } } else { - const response: LocationListResponse = await apiGet( - 'location/all', - token, - lang, - {bookid: currentEntityId} - ); + const response: LocationListResponse = useLocal + ? await tauri.getAllLocations(currentEntityId, true) as LocationListResponse + : await apiGet( + 'location/all', + token, + lang, + {bookid: currentEntityId} + ); if (response) { setSections(response.locations); setToolEnabled(response.enabled); @@ -194,24 +207,17 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< try { let sectionId: string; if (isSeriesMode) { - sectionId = await apiPost( - 'series/location/section/add', - { - seriesId: currentEntityId, - name: newSectionName, - }, - token, - lang - ); + sectionId = useLocal + ? await tauri.addSeriesLocationSection({seriesId: currentEntityId, name: newSectionName}) + : await apiPost('series/location/section/add', {seriesId: currentEntityId, name: newSectionName}, token, lang); if (!sectionId) { errorMessage(t('locationComponent.errorUnknownAddSection')); return; } } else { - sectionId = await apiPost('location/section/add', { - bookId: currentEntityId, - locationName: newSectionName, - }, token, lang); + sectionId = useLocal + ? await tauri.addLocationSection(newSectionName, currentEntityId) + : await apiPost('location/section/add', {bookId: currentEntityId, locationName: newSectionName}, token, lang); if (!sectionId) { errorMessage(t('locationComponent.errorUnknownAddSection')); return; @@ -241,25 +247,17 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< try { let elementId: string; if (isSeriesMode) { - elementId = await apiPost( - 'series/location/element/add', - { - locationId: sectionId, - name: newElementNames[sectionId], - }, - token, - lang - ); + elementId = useLocal + ? await tauri.addSeriesLocationElement({locationId: sectionId, name: newElementNames[sectionId]}) + : await apiPost('series/location/element/add', {locationId: sectionId, name: newElementNames[sectionId]}, token, lang); if (!elementId) { errorMessage(t('locationComponent.errorUnknownAddElement')); return; } } else { - elementId = await apiPost('location/element/add', { - bookId: currentEntityId, - locationId: sectionId, - elementName: newElementNames[sectionId], - }, token, lang); + elementId = useLocal + ? await tauri.addLocationElement(sectionId, newElementNames[sectionId]) + : await apiPost('location/element/add', {bookId: currentEntityId, locationId: sectionId, elementName: newElementNames[sectionId]}, token, lang); if (!elementId) { errorMessage(t('locationComponent.errorUnknownAddElement')); return; @@ -314,25 +312,19 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< ); try { let subElementId: string; + const parentElementId: string = sections[sectionIndex].elements[elementIndex].id; if (isSeriesMode) { - subElementId = await apiPost( - 'series/location/sub-element/add', - { - elementId: sections[sectionIndex].elements[elementIndex].id, - name: newSubElementNames[elementIndex], - }, - token, - lang - ); + subElementId = useLocal + ? await tauri.addSeriesLocationSubElement({elementId: parentElementId, name: newSubElementNames[elementIndex]}) + : await apiPost('series/location/sub-element/add', {elementId: parentElementId, name: newSubElementNames[elementIndex]}, token, lang); if (!subElementId) { errorMessage(t('locationComponent.errorUnknownAddSubElement')); return; } } else { - subElementId = await apiPost('location/sub-element/add', { - elementId: sections[sectionIndex].elements[elementIndex].id, - subElementName: newSubElementNames[elementIndex], - }, token, lang); + subElementId = useLocal + ? await tauri.addLocationSubElement(parentElementId, newSubElementNames[elementIndex]) + : await apiPost('location/sub-element/add', {elementId: parentElementId, subElementName: newSubElementNames[elementIndex]}, token, lang); if (!subElementId) { errorMessage(t('locationComponent.errorUnknownAddSubElement')); return; @@ -379,15 +371,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< try { const elementId: string | undefined = sections.find((section: LocationProps): boolean => section.id === sectionId) ?.elements[elementIndex].id; + const deletedAt: number = Math.floor(Date.now() / 1000); let success: boolean; if (isSeriesMode) { - success = await apiDelete('series/location/element/delete', { - elementId: elementId - }, token, lang); + success = useLocal + ? await tauri.deleteSeriesLocationElement(elementId!, deletedAt) + : await apiDelete('series/location/element/delete', {elementId: elementId}, token, lang); } else { - success = await apiDelete('location/element/delete', { - elementId: elementId, - }, token, lang); + success = useLocal + ? await tauri.deleteLocationElement(elementId!, currentEntityId, deletedAt) + : await apiDelete('location/element/delete', {elementId: elementId}, token, lang); } if (!success) { errorMessage(t('locationComponent.errorUnknownDeleteElement')); @@ -414,15 +407,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< try { const subElementId: string | undefined = sections.find((section: LocationProps): boolean => section.id === sectionId) ?.elements[elementIndex].subElements[subElementIndex].id; + const deletedAt: number = Math.floor(Date.now() / 1000); let success: boolean; if (isSeriesMode) { - success = await apiDelete('series/location/sub-element/delete', { - subElementId: subElementId - }, token, lang); + success = useLocal + ? await tauri.deleteSeriesLocationSubElement(subElementId!, deletedAt) + : await apiDelete('series/location/sub-element/delete', {subElementId: subElementId}, token, lang); } else { - success = await apiDelete('location/sub-element/delete', { - subElementId: subElementId, - }, token, lang); + success = useLocal + ? await tauri.deleteLocationSubElement(subElementId!, currentEntityId, deletedAt) + : await apiDelete('location/sub-element/delete', {subElementId: subElementId}, token, lang); } if (!success) { errorMessage(t('locationComponent.errorUnknownDeleteSubElement')); @@ -443,15 +437,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< async function handleRemoveSection(sectionId: string): Promise { try { + const deletedAt: number = Math.floor(Date.now() / 1000); let success: boolean; if (isSeriesMode) { - success = await apiDelete('series/location/delete', { - locationId: sectionId - }, token, lang); + success = useLocal + ? await tauri.deleteSeriesLocation(sectionId, deletedAt) + : await apiDelete('series/location/delete', {locationId: sectionId}, token, lang); } else { - success = await apiDelete('location/delete', { - locationId: sectionId, - }, token, lang); + success = useLocal + ? await tauri.deleteLocationSection(sectionId, currentEntityId, deletedAt) + : await apiDelete('location/delete', {locationId: sectionId}, token, lang); } if (!success) { errorMessage(t('locationComponent.errorUnknownDeleteSection')); @@ -470,9 +465,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< async function handleSave(): Promise { try { - const response: boolean = await apiPost(`location/update`, { - locations: sections, - }, token, lang); + const response: boolean = useLocal + ? await tauri.updateLocations(sections) as boolean + : await apiPost(`location/update`, {locations: sections}, token, lang); if (!response) { errorMessage(t('locationComponent.errorUnknownSave')); return; @@ -489,19 +484,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< async function handleExportToSeries(section: LocationProps): Promise { if (!bookSeriesId) return; - + try { - const seriesLocationId: string = await apiPost('series/location/section/add', { - seriesId: bookSeriesId, - name: section.name, - }, token, lang); - + const seriesLocationId: string = useLocal + ? await tauri.addSeriesLocationSection({seriesId: bookSeriesId, name: section.name}) + : await apiPost('series/location/section/add', {seriesId: bookSeriesId, name: section.name}, token, lang); + if (seriesLocationId) { - const updateResponse: boolean = await apiPost('location/section/update', { - sectionId: section.id, - sectionName: section.name, - seriesLocationId: seriesLocationId, - }, token, lang); + const updateResponse: boolean = useLocal + ? await tauri.updateLocationSectionWithSeriesLink(section.id, section.name, seriesLocationId) + : await apiPost('location/section/update', {sectionId: section.id, sectionName: section.name, seriesLocationId: seriesLocationId}, token, lang); if (updateResponse) { setSections(sections.map((s: LocationProps): LocationProps => @@ -523,11 +515,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< if (!seriesLocation) return; try { - const sectionId: string = await apiPost('location/section/add', { - bookId: currentEntityId, - locationName: seriesLocation.name, - seriesLocationId: seriesLocationId, - }, token, lang); + const sectionId: string = useLocal + ? await tauri.addLocationSection(seriesLocation.name, currentEntityId, undefined, seriesLocationId) + : await apiPost('location/section/add', {bookId: currentEntityId, locationName: seriesLocation.name, seriesLocationId: seriesLocationId}, token, lang); if (!sectionId) { errorMessage(t('locationComponent.importError')); @@ -537,21 +527,18 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< const importedElements: Element[] = []; for (const seriesElement of seriesLocation.elements) { - const elementId: string = await apiPost('location/element/add', { - bookId: currentEntityId, - locationId: sectionId, - elementName: seriesElement.name, - }, token, lang); + const elementId: string = useLocal + ? await tauri.addLocationElement(sectionId, seriesElement.name) + : await apiPost('location/element/add', {bookId: currentEntityId, locationId: sectionId, elementName: seriesElement.name}, token, lang); if (!elementId) continue; const importedSubElements: SubElement[] = []; for (const seriesSubElement of seriesElement.subElements) { - const subElementId: string = await apiPost('location/sub-element/add', { - elementId: elementId, - subElementName: seriesSubElement.name, - }, token, lang); + const subElementId: string = useLocal + ? await tauri.addLocationSubElement(elementId, seriesSubElement.name) + : await apiPost('location/sub-element/add', {elementId: elementId, subElementName: seriesSubElement.name}, token, lang); if (subElementId) { importedSubElements.push({ diff --git a/components/book/settings/locations/settings/LocationSettings.tsx b/components/book/settings/locations/settings/LocationSettings.tsx index 4d68c54..5f4a783 100644 --- a/components/book/settings/locations/settings/LocationSettings.tsx +++ b/components/book/settings/locations/settings/LocationSettings.tsx @@ -161,7 +161,16 @@ export default function LocationSettings({ {viewMode === 'detail' && selectedSection && (
- + { + return addElement(selectedSection.id); + }} + />
)} diff --git a/components/book/settings/locations/settings/LocationSettingsDetail.tsx b/components/book/settings/locations/settings/LocationSettingsDetail.tsx index 6c22d75..c92b970 100644 --- a/components/book/settings/locations/settings/LocationSettingsDetail.tsx +++ b/components/book/settings/locations/settings/LocationSettingsDetail.tsx @@ -29,6 +29,7 @@ export default function LocationSettingsDetail({ className="text-center py-12 text-text-secondary">

{t("locationComponent.noElementAvailable")}

+

{t("locationComponent.editToAdd")}

) : (
diff --git a/components/editor/TextEditor.tsx b/components/editor/TextEditor.tsx index f50b764..f803e49 100644 --- a/components/editor/TextEditor.tsx +++ b/components/editor/TextEditor.tsx @@ -31,9 +31,14 @@ import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import DraftCompanion from "@/components/editor/DraftCompanion"; import GhostWriter from "@/components/ghostwriter/GhostWriter"; import IconButton from "@/components/ui/IconButton"; +import Button from "@/components/ui/Button"; import UserEditorSettings, {EditorDisplaySettings} from "@/components/editor/UserEditorSetting"; import {useTranslations} from '@/lib/i18n'; import {LangContext, LangContextProps} from "@/context/LangContext"; +import {isWebKitWithoutIndentFix} from "@/lib/utils/webkitDetect"; +import {getCookie, setCookie} from "@/lib/utils/cookies"; +import Modal from "@/components/ui/Modal"; +import {Info} from 'lucide-react'; interface ToolbarButton { action: () => void; @@ -48,14 +53,12 @@ interface EditorClasses { h3: string; container: string; theme: string; - paragraph: string; lists: string; listItems: string; } const defaultEditorSettings: EditorDisplaySettings = { zoomLevel: 3, - indent: 30, lineHeight: 1.5, theme: 'sombre', fontFamily: 'lora', @@ -148,6 +151,8 @@ export default function TextEditor() { const [showUserSettings, setShowUserSettings] = useState(false); const [isSaving, setIsSaving] = useState(false); const [editorSettings, setEditorSettings] = useState(defaultEditorSettings); + const [indentDisabled] = useState(() => isWebKitWithoutIndentFix()); + const [showIndentModal, setShowIndentModal] = useState(() => isWebKitWithoutIndentFix() && !getCookie('indent_notice_seen')); const [editorClasses, setEditorClasses] = useState({ base: 'text-lg font-serif leading-normal', h1: 'text-3xl font-bold', @@ -155,7 +160,6 @@ export default function TextEditor() { h3: 'text-xl font-bold', container: 'max-w-3xl', theme: 'bg-tertiary text-text-primary', - paragraph: 'indent-6', lists: 'pl-10', listItems: 'text-lg' }); @@ -170,14 +174,12 @@ export default function TextEditor() { const fontFamily: string = fontFamilyClasses[settings.fontFamily] || fontFamilyClasses['lora']; const lineHeight: string = lineHeightClasses[lineHeightKey]; - const indentClass: string = `indent-${Math.round(settings.indent / 4)}`; - const baseClass: string = `${fontSizeClasses[zoomKey]} ${fontFamily} ${lineHeight}`; const h1Class: string = `${h1SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`; const h2Class: string = `${h2SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`; const h3Class: string = `${h3SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`; const containerClass: string = maxWidthClasses[maxWidthKey]; - const listsClass: string = `pl-${Math.round((settings.indent + 20) / 4)}`; + const listsClass: string = 'pl-12'; let themeClass: string = ''; switch (settings.theme) { @@ -198,7 +200,6 @@ export default function TextEditor() { h3: h3Class, container: containerClass, theme: themeClass, - paragraph: indentClass, lists: listsClass, listItems: baseClass }); @@ -335,24 +336,16 @@ export default function TextEditor() { setShowDraftCompanion(false); setShowGhostWriter(false); }, []); - - useEffect((): void => { - if (!editor) return; - - const editorElement: HTMLElement = editor.view.dom; - if (editorElement) { - const indentClasses: string[] = Array.from({length: 21}, (_: unknown, i: number): string => `indent-${i}`); - editorElement.classList.remove(...indentClasses); - - if (editorClasses.paragraph) { - editorElement.classList.add(editorClasses.paragraph); - } - } - }, [editor, editorClasses.paragraph]); + + const handleCloseIndentModal: () => void = useCallback((): void => { + setCookie('indent_notice_seen', 'true', 365); + setShowIndentModal(false); + }, []); useEffect((): void => { updateEditorClasses(editorSettings); }, [editorSettings, updateEditorClasses]); + useEffect((): () => void => { function startTimer(): void { @@ -437,7 +430,7 @@ export default function TextEditor() { } return ( -
+
@@ -481,6 +474,15 @@ export default function TextEditor() { tooltip={t("textEditor.draftCompanion")} /> )} + {indentDisabled && ( + setShowIndentModal(true)} + tooltip={t("textEditor.indentDisabled")} + /> + )} )}
+ {showIndentModal && ( + + {t("textEditor.indentDisabledUnderstood")} + + } + > +

+ {t("textEditor.indentDisabledDescription")} +

+
+ )}
); } \ No newline at end of file diff --git a/components/editor/UserEditorSetting.tsx b/components/editor/UserEditorSetting.tsx index ed280de..f1ab01b 100644 --- a/components/editor/UserEditorSetting.tsx +++ b/components/editor/UserEditorSetting.tsx @@ -1,6 +1,6 @@ 'use client' import React, {ChangeEvent, useCallback, useContext, useEffect, useMemo} from 'react'; -import {Baseline, CaseSensitive, Eye, Indent, Palette, Type} from 'lucide-react'; +import {Baseline, CaseSensitive, Eye, Palette, Type} from 'lucide-react'; import {useTranslations} from '@/lib/i18n'; import SelectBox from "@/components/form/SelectBox"; import Button from "@/components/ui/Button"; @@ -13,7 +13,6 @@ interface UserEditorSettingsProps { export interface EditorDisplaySettings { zoomLevel: number; - indent: number; lineHeight: number; theme: 'clair' | 'sombre' | 'sépia'; fontFamily: 'lora' | 'serif' | 'sans-serif' | 'monospace'; @@ -31,7 +30,6 @@ function isValidFontFamily(value: string): value is EditorDisplaySettings['fontF const defaultSettings: EditorDisplaySettings = { zoomLevel: 3, - indent: 30, lineHeight: 1.5, theme: 'sombre', fontFamily: 'lora', @@ -130,29 +128,6 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi />
-
- -
- ): void => handleSettingChange('indent', Number(e.target.value))} - className="w-full accent-primary" - /> -
- {t("userEditorSettings.indentNone")} - {settings.indent}px - {t("userEditorSettings.indentMax")} -
-
-
-